552 lines
27 KiB
Rust
552 lines
27 KiB
Rust
use makepad_hal::buffers::{pack_drag_items, read_drag_items, read_items, read_pairs, read_strs, PipelineBlob};
|
|
use makepad_hal::cmds::{sample_cmds, CMD_TABLE};
|
|
use makepad_hal::events::{sample_events, EVENT_TABLE};
|
|
use makepad_hal::session::{Miss, Table, World};
|
|
use makepad_hal::*;
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn open(hal: &Hal, key: u64) -> u32 {
|
|
let info = hal.open(&OpenReq { backend: backend_id::HEADLESS, app_key: key, flags: 0, dpi_hint: 2.0, parent: 0 });
|
|
assert_ne!(info.session, 0);
|
|
assert_eq!(info.dpi, 2.0);
|
|
info.session
|
|
}
|
|
|
|
fn pump(hal: &Hal, store: &mut EventStore, timeout_ns: u64) -> Vec<(u64, String)> {
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(timeout_ns, &mut buf);
|
|
store.reader(n, buf.arena_len).map(|(t, e)| (t, format!("{e:?}"))).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn every_command_round_trips() {
|
|
let mut w = CmdWriter::new(1);
|
|
let expected = sample_cmds(&mut w);
|
|
assert_eq!(expected.len(), CMD_TABLE.len());
|
|
assert_eq!(w.cmds().len(), CMD_TABLE.len());
|
|
// 97 commands: 57 core (including ClipRead, GpuBudget, WindowChrome, WindowCaption and WindowDpiOverride, with
|
|
// timer start/stop counted separately), 6 embedded, 4 domains, 10 media, 3 fonts, 9 direct display/input
|
|
// (incl. DirectDisplayArrangement, DirectDisplayPacing, DirectTapToClick, DirectDisplayVirtual tag 153), 2 GL interop, 3 mobile system UI (MobileUi,
|
|
// SelectionHandles, Haptic), 1 system back (BackUnhandled), 1 font touch (GlyphTouch, tag 150), 3 tray (TraySet,
|
|
// TrayRemove, WindowAtTray tag 154).
|
|
// The media section (tags 120-129, backend/apple/media) has its own budget.
|
|
let media = CMD_TABLE.iter().filter(|t| (120..130).contains(&t.0)).count();
|
|
let fonts = CMD_TABLE.iter().filter(|t| (130..133).contains(&t.0)).count();
|
|
assert_eq!(media, 10);
|
|
assert_eq!(fonts, 3);
|
|
assert!(media <= 15, "the media section is budgeted at 15 commands, it has {media}");
|
|
assert_eq!(CMD_TABLE.len(), 57 + 6 + 4 + media + fonts + 9 + 2 + 4 + 1 + 3);
|
|
for ((cmd, want), (tag, name, _)) in w.cmds().iter().zip(&expected).zip(CMD_TABLE) {
|
|
assert_eq!(cmd.tag, *tag, "{name}");
|
|
let got = cmd.decode(w.arena()).unwrap_or_else(|| panic!("{name} does not decode"));
|
|
assert_eq!(&format!("{got:?}"), want, "{name}");
|
|
}
|
|
let tags: std::collections::HashSet<u16> = CMD_TABLE.iter().map(|t| t.0).collect();
|
|
assert_eq!(tags.len(), CMD_TABLE.len(), "tags are unique");
|
|
assert!(Cmd { tag: 9999, ..Default::default() }.decode(&[]).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn every_event_round_trips() {
|
|
let mut q = EventQueue::new();
|
|
q.set_time(77);
|
|
let expected = sample_events(&mut q);
|
|
assert_eq!(expected.len(), EVENT_TABLE.len());
|
|
assert_eq!(EVENT_TABLE.len(), 96, "55 core (incl. Clipboard, GpuBudget, PayloadSize, MouseCancel, AppMessage, DeviceReset, GpuPressure, Redraw), 5 parent/child (85-89), 13 media, 4 font, 4 direct display/input 1 GL interop and 9 mobile (LongPress, ImeState, ImeAction, ImeResize, PhysicalKeyboard, SelectionHandle, TouchCancel, SafeArea, ImeAnimate) and 2 system back (BackGesture, BackPressed) and 2 tray (TrayMenu, TrayClick) events");
|
|
assert_eq!(EVENT_TABLE.iter().filter(|t| (120..133).contains(&t.0)).count(), 13);
|
|
assert_eq!(EVENT_TABLE.iter().filter(|t| (100..104).contains(&t.0)).count(), 4);
|
|
let tags: std::collections::HashSet<u16> = EVENT_TABLE.iter().map(|t| t.0).collect();
|
|
assert_eq!(tags.len(), EVENT_TABLE.len(), "tags are unique");
|
|
let mut store = EventStore::new(1, 128, 4096);
|
|
let mut buf = store.buf();
|
|
let n = q.drain_into(&mut buf) as usize;
|
|
assert_eq!(n, EVENT_TABLE.len());
|
|
let got: Vec<(u64, String)> = store.reader(n as u32, buf.arena_len).map(|(t, e)| (t, format!("{e:?}"))).collect();
|
|
for (((t, g), want), (_, name, _)) in got.iter().zip(&expected).zip(EVENT_TABLE) {
|
|
assert_eq!(g, want, "{name}");
|
|
assert_eq!(*t, 77);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn small_event_buffer_delivers_in_order_without_loss() {
|
|
let mut q = EventQueue::new();
|
|
for i in 0..10 {
|
|
q.text(Id::new(1, 1), input::text::TYPED, format!("event number {i}").as_bytes());
|
|
}
|
|
let mut store = EventStore::new(1, 3, 40);
|
|
let mut seen = Vec::new();
|
|
while !q.is_empty() {
|
|
let mut buf = store.buf();
|
|
let n = q.drain_into(&mut buf);
|
|
assert!(n > 0);
|
|
seen.extend(store.reader(n, buf.arena_len).map(|(_, e)| format!("{e:?}")));
|
|
}
|
|
assert_eq!(seen.len(), 10);
|
|
assert!(seen[9].contains(&format!("{:?}", b"event number 9".as_slice())));
|
|
}
|
|
|
|
#[test]
|
|
fn arrays_and_blobs_round_trip() {
|
|
let mut w = CmdWriter::new(1);
|
|
let items = [DragItem { kind: 1, str: 2 }, DragItem { kind: 3, str: 4 }];
|
|
let r = w.put_items(&items);
|
|
let back: Vec<DragItem> = read_items(&w.arena()[r.off as usize..(r.off + r.len) as usize], 2).collect();
|
|
assert_eq!(back, items);
|
|
let strs = w.put_strs(&["a.png", "b.txt"]);
|
|
assert_eq!(read_strs(&w.arena()[strs.off as usize..(strs.off + strs.len) as usize], 2), ["a.png", "b.txt"]);
|
|
let desc = PipelineDesc { shader: 9, name: 3, source: 4, instance_stride: 12, varyings: 5, blend: 1, color_formats: [1, 0, 0, 0], ..Default::default() };
|
|
let blob = CmdWriter::pipeline_blob(desc, b"shader code", &[1, 2, 3], &[7, 8]);
|
|
let p = PipelineBlob::parse(&blob).unwrap();
|
|
assert_eq!((p.code, p.bindings.as_slice(), p.vertex_layout.as_slice()), (&b"shader code"[..], &[1, 2, 3][..], &[7, 8][..]));
|
|
assert_eq!((p.desc.shader, p.desc.name, p.desc.instance_stride, p.desc.varyings), (9, 3, 12, 5));
|
|
assert!(PipelineBlob::parse(&blob[..20]).is_none());
|
|
}
|
|
|
|
/// A texture the device cannot hold (no room, past its limits) is reported gone as an
|
|
/// evicted one, so the app makes it again when it next needs it instead of drawing with an id
|
|
/// that never got storage (149: a window maximised on a full GPU then showed only its outline).
|
|
#[test]
|
|
fn a_texture_the_device_cannot_hold_is_reported_evicted() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut store = EventStore::new(1, 128, 4096);
|
|
let mut w = CmdWriter::new(s);
|
|
w.texture_create(Id::new(1, 1), usage::SAMPLED | usage::RENDER_TARGET, format::BGRA8, 1200, 800, [1, 1], Attr::default());
|
|
w.texture_create(Id::new(2, 1), usage::SAMPLED | usage::RENDER_TARGET, format::BGRA8, 40000, 800, [1, 1], Attr::default());
|
|
hal.submit(&w.buf());
|
|
let evs: Vec<String> = pump(&hal, &mut store, 0).into_iter().map(|e| e.1).collect();
|
|
assert_eq!(evs.iter().filter(|e| e.contains("TextureEvicted")).count(), 1, "{evs:?}");
|
|
assert!(evs.iter().any(|e| e.contains("TextureEvicted") && e.contains("index: 2")), "{evs:?}");
|
|
assert_eq!(hal.with_world(|w| w.count(ledger::Kind::Texture)), 1);
|
|
// made again at a size the device holds (the window restored): it is there
|
|
w.clear();
|
|
w.texture_create(Id::new(2, 2), usage::SAMPLED | usage::RENDER_TARGET, format::BGRA8, 1200, 800, [1, 1], Attr::default());
|
|
hal.submit(&w.buf());
|
|
assert!(pump(&hal, &mut store, 0).iter().all(|e| !e.1.contains("TextureEvicted")));
|
|
assert_eq!(hal.with_world(|w| w.count(ledger::Kind::Texture)), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn stale_ids_are_counted_and_ignored() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut w = CmdWriter::new(s);
|
|
let t = Id::new(4, 2);
|
|
w.texture_create(t, usage::SAMPLED, format::RGBA8, 16, 16, [1, 1], Attr::default());
|
|
w.texture_destroy(t);
|
|
w.texture_destroy(t); // stale: already gone
|
|
w.texture_create(Id::new(4, 1), usage::SAMPLED, format::RGBA8, 16, 16, [1, 1], Attr::default()); // older generation than the freed one
|
|
w.texture_update(Id::new(4, 2), 0, [0, 0, 1, 1], 0, &[0; 4]); // dead
|
|
w.buffer_destroy(Id::new(99, 1)); // never existed
|
|
hal.submit(&w.buf());
|
|
let stats = hal.with_world(|w| w.session(s).unwrap().stats);
|
|
assert_eq!(stats.stale_ids, 4);
|
|
assert_eq!(hal.with_world(|w| w.count(ledger::Kind::Texture)), 0);
|
|
// a newer generation of the slot is fine
|
|
w.clear();
|
|
w.texture_create(Id::new(4, 3), usage::SAMPLED, format::RGBA8, 16, 16, [1, 1], Attr::default());
|
|
hal.submit(&w.buf());
|
|
assert_eq!(hal.with_world(|w| w.count(ledger::Kind::Texture)), 1);
|
|
// generic table behaviour
|
|
let mut t: Table<u8> = Table::default();
|
|
t.insert(1, Id::new(0, 5), 1, 7).unwrap();
|
|
assert_eq!(t.get(1, Id::new(0, 4)).err(), Some(Miss::Stale));
|
|
assert_eq!(t.insert(1, Id::new(0, 4), 1, 8).err(), Some(Miss::Stale));
|
|
}
|
|
|
|
#[test]
|
|
fn bad_arguments_are_counted() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut w = CmdWriter::new(s);
|
|
w.texture_create(Id::new(0, 1), usage::SAMPLED, 999, 16, 16, [1, 1], Attr::default());
|
|
w.texture_create(Id::new(1, 1), usage::SAMPLED, format::RGBA8, 0, 16, [1, 1], Attr::default());
|
|
w.texture_create(Id::new(2, 1), usage::SAMPLED, format::RGBA8, 4, 4, [1, 1], Attr::default());
|
|
w.texture_update(Id::new(2, 1), 0, [2, 2, 4, 4], 0, &[0; 64]); // outside
|
|
w.texture_update(Id::new(2, 1), 0, [0, 0, 4, 4], 0, &[0; 10]); // wrong length
|
|
w.texture_update(Id::new(2, 1), 0, [0, 0, 4, 4], 0, &[0; 64]); // fine
|
|
w.push(Cmd { tag: 9999, ..Default::default() });
|
|
hal.submit(&w.buf());
|
|
let st = hal.with_world(|w| w.session(s).unwrap().stats);
|
|
assert_eq!((st.bad_args, st.unknown_cmds, st.stale_ids), (4, 1, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn data_formats_mips_cube_faces_and_blocks_are_validated() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut w = CmdWriter::new(s);
|
|
// a cube (6 layers) of RG16F with 2 mips: level 0 is 8x8x4 bytes, level 1 is 4x4x4 bytes
|
|
w.texture_create(Id::new(0, 1), usage::SAMPLED, format::RG16F, 8, 8, [6, 2], Attr::default());
|
|
w.texture_update(Id::new(0, 1), 0, [0, 0, 8, 8], 5, &[0; 256]); // face 5, level 0
|
|
w.texture_update(Id::new(0, 1), 1, [0, 0, 4, 4], 3, &[0; 64]); // face 3, level 1
|
|
w.texture_update(Id::new(0, 1), 0, [0, 0, 8, 8], 6, &[0; 256]); // face 6 does not exist
|
|
w.texture_update(Id::new(0, 1), 2, [0, 0, 2, 2], 0, &[0; 16]); // level 2 does not exist
|
|
// a BC7 image of 9x5 texels is 3x2 blocks of 16 bytes
|
|
w.texture_create(Id::new(1, 1), usage::SAMPLED, format::BC7, 9, 5, [1, 1], Attr::default());
|
|
w.texture_update(Id::new(1, 1), 0, [0, 0, 9, 5], 0, &[0; 96]);
|
|
w.texture_update(Id::new(1, 1), 0, [0, 0, 9, 5], 0, &[0; 45]); // texel count, not block bytes
|
|
w.texture_create(Id::new(2, 1), usage::RENDER_TARGET, format::R32UINT, 4, 4, [1, 1], Attr::default());
|
|
hal.submit(&w.buf());
|
|
let st = hal.with_world(|w| w.session(s).unwrap().stats);
|
|
assert_eq!((st.bad_args, st.stale_ids), (3, 0));
|
|
assert_eq!(format::level_bytes(format::BC7, 9, 5), 96);
|
|
assert_eq!(headless_bytes(format::BC7, 9, 5, 1, 1), 96);
|
|
assert_eq!(headless_bytes(format::RG16F, 8, 8, 6, 2), 6 * (8 * 8 * 4 + 4 * 4 * 4));
|
|
}
|
|
|
|
#[test]
|
|
fn drag_items_in_events_carry_their_strings() {
|
|
let payload = pack_drag_items(&[(input::drag_item::FILE, "/tmp/a.png"), (input::drag_item::STRING, "héllo")]);
|
|
let mut q = EventQueue::new();
|
|
q.drag_over(Id::new(1, 1), 3.0, 4.0, 0, 2, &payload);
|
|
let mut store = EventStore::new(1, 8, 4096);
|
|
let mut buf = store.buf();
|
|
let n = q.drain_into(&mut buf);
|
|
let evs: Vec<_> = store.reader(n, buf.arena_len).collect();
|
|
match evs[0].1 {
|
|
HalEvent::DragOver { count, items, .. } => {
|
|
let got = read_drag_items(items, count);
|
|
assert_eq!(got, vec![(input::drag_item::FILE, "/tmp/a.png".to_string()), (input::drag_item::STRING, "héllo".to_string())]);
|
|
}
|
|
ref e => panic!("{e:?}"),
|
|
}
|
|
// a truncated payload ends the list instead of reading out of bounds
|
|
assert_eq!(read_drag_items(&payload[..payload.len() - 3], 2).len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn instance_ranges_are_element_ranges() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut w = CmdWriter::new(s);
|
|
w.str_def(1, b"padding that moves the arena off an 8 multiple");
|
|
let r = w.put_pairs(&[[0, 4], [10, 2], [20, 1]]);
|
|
assert_eq!(r.len, 3);
|
|
assert_eq!(read_pairs(w.arena(), r).collect::<Vec<_>>(), vec![[0, 4], [10, 2], [20, 1]]);
|
|
w.pipeline_create(Id::new(0, 1), dialect::NONE, Attr::default(), &CmdWriter::pipeline_blob(PipelineDesc::default(), &[], &[], &[]));
|
|
w.frame_begin(1, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, ..Default::default() });
|
|
w.draw(Draw { pipeline: Id::new(0, 1), inst_ranges: r, ..Default::default() });
|
|
w.frame_end(Id::new(0, 1), target::WINDOW);
|
|
w.window_create(Id::new(0, 1), window_kind::NORMAL, Id::NONE, [0.0, 0.0, 10.0, 10.0], false);
|
|
hal.submit(&w.buf());
|
|
assert_eq!(hal.with_world(|w| w.session(s).unwrap().stats.bad_args), 0);
|
|
}
|
|
|
|
fn headless_bytes(f: u32, w: u32, h: u32, d: u32, m: u32) -> u64 {
|
|
makepad_hal::backend::headless::texture_bytes(f, w, h, d, m)
|
|
}
|
|
|
|
#[test]
|
|
fn session_close_frees_everything_and_keeps_adoptable_windows() {
|
|
let hal = Hal::new();
|
|
let host = hal.with_world(|w| w.host_window([0.0, 0.0, 800.0, 600.0]));
|
|
let s = open(&hal, 42);
|
|
let mut w = CmdWriter::new(s);
|
|
w.window_create(Id::new(0, 1), window_kind::NORMAL, Id::NONE, [0.0, 0.0, 1.0, 1.0], true); // adopts the host window
|
|
w.window_create(Id::new(1, 1), window_kind::NORMAL, Id::NONE, [0.0, 0.0, 100.0, 100.0], false);
|
|
w.texture_create(Id::new(0, 1), usage::RENDER_TARGET, format::RGBA8, 64, 64, [1, 1], Attr::default());
|
|
w.buffer_create(Id::new(0, 1), buffer_kind::VERTEX, 1024, Attr::default());
|
|
w.timer_start(5, 1_000_000, true);
|
|
hal.submit(&w.buf());
|
|
assert_eq!(hal.with_world(|w| w.lent_to(host)), Some(s));
|
|
// the adopted window carries the host's geometry
|
|
let mut store = EventStore::new(s, 32, 1024);
|
|
let evs = pump(&hal, &mut store, 0);
|
|
assert!(evs.iter().any(|(_, e)| e.contains("Geom") && e.contains("800.0")), "{evs:?}");
|
|
assert_eq!(hal.with_world(|w| (w.count(ledger::Kind::Window), w.count(ledger::Kind::Texture), w.count(ledger::Kind::Buffer))), (3, 1, 1));
|
|
// a second open with the same app key = relaunch: the old session is closed
|
|
let s2 = open(&hal, 42);
|
|
assert_ne!(s, s2);
|
|
assert!(hal.with_world(|w| w.session(s).is_none()));
|
|
assert_eq!(hal.with_world(|w| (w.count(ledger::Kind::Texture), w.count(ledger::Kind::Buffer))), (0, 0));
|
|
// the host window survived, lent to nobody
|
|
assert!(hal.with_world(|w| w.host_window_alive(host)));
|
|
assert_eq!(hal.with_world(|w| w.lent_to(host)), None);
|
|
// and can be adopted again; detaching returns it as well
|
|
let mut w2 = CmdWriter::new(s2);
|
|
w2.window_create(Id::new(7, 1), window_kind::NORMAL, Id::NONE, [0.0; 4], true);
|
|
hal.submit(&w2.buf());
|
|
assert_eq!(hal.with_world(|w| w.lent_to(host)), Some(s2));
|
|
w2.clear();
|
|
w2.window_close(Id::new(7, 1));
|
|
hal.submit(&w2.buf());
|
|
assert_eq!(hal.with_world(|w| w.lent_to(host)), None);
|
|
assert!(hal.with_world(|w| w.host_window_alive(host)));
|
|
// nothing to adopt -> a normal window
|
|
let mut w3 = World::new();
|
|
let s3 = w3.open(0);
|
|
assert!(w3.adopt_window(s3, Id::new(0, 1)).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn ledger_totals_and_idle() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut w = CmdWriter::new(s);
|
|
w.str_def(1, b"vj/effects");
|
|
w.str_def(2, b"effects.rs:10:5");
|
|
w.str_def(3, b"main.rs:3:1");
|
|
w.str_def(4, b"explicit-label");
|
|
let a = Attr { owner: 1, site: 2, label: 0, kind: 0 };
|
|
w.texture_create(Id::new(0, 1), usage::RENDER_TARGET, format::RGBA8, 100, 100, [1, 1], a);
|
|
w.texture_create(Id::new(1, 1), usage::RENDER_TARGET, format::RGBA8, 10, 10, [1, 3], Attr { owner: 1, site: 2, label: 4, kind: 0 });
|
|
w.texture_create(Id::new(2, 1), usage::RENDER_TARGET, format::DEPTH32F, 100, 100, [1, 1], Attr { owner: 0, site: 3, label: 0, kind: 0 });
|
|
w.buffer_create(Id::new(0, 1), buffer_kind::INSTANCE, 5000, a);
|
|
hal.submit(&w.buf());
|
|
let r = hal.report(s, 30).unwrap();
|
|
let mip_chain = 10 * 10 * 4 + 5 * 5 * 4 + 2 * 2 * 4;
|
|
assert_eq!(r.total_bytes, 100 * 100 * 4 + mip_chain + 100 * 100 * 4 + 5000);
|
|
let kind = |k: &str| r.by_kind.iter().find(|g| g.key == k).map(|g| (g.count, g.bytes));
|
|
assert_eq!(kind("render target"), Some((2, 40_000 + mip_chain)));
|
|
assert_eq!(kind("depth"), Some((1, 40_000)));
|
|
assert_eq!(kind("instance buffers"), Some((1, 5000)));
|
|
// owner = label, else scope, else site
|
|
let owner = |k: &str| r.by_owner.iter().find(|g| g.key == k).map(|g| g.bytes);
|
|
assert_eq!(owner("explicit-label"), Some(mip_chain));
|
|
assert_eq!(owner("vj/effects"), Some(40_000 + 5000));
|
|
assert_eq!(owner("main.rs:3:1"), Some(40_000));
|
|
assert_eq!(r.by_site.iter().find(|g| g.key == "effects.rs:10:5").map(|g| g.count), Some(3));
|
|
assert_eq!(r.idle.len(), 3, "never used textures are idle");
|
|
// a frame that samples one texture makes it non-idle
|
|
w.clear();
|
|
w.frame_begin(5, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::new(0, 1), Id::NONE, Id::NONE, Id::NONE], depth: Id::NONE, ..Default::default() });
|
|
w.frame_end(Id::new(0, 1), target::TEXTURE);
|
|
hal.submit(&w.buf());
|
|
let r = hal.report(s, 30).unwrap();
|
|
assert_eq!(r.idle.len(), 2);
|
|
assert_eq!(r.items.iter().find(|i| i.index == 0 && i.detail == "render target").unwrap().last_used_frame, 5);
|
|
// destroying removes from the ledger
|
|
w.clear();
|
|
w.texture_destroy(Id::new(0, 1));
|
|
hal.submit(&w.buf());
|
|
assert_eq!(hal.report(s, 30).unwrap().total_bytes, r.total_bytes - 40_000);
|
|
assert_eq!(hal.report_all(30).total_bytes, hal.report(s, 30).unwrap().total_bytes);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_packet_with_two_passes_three_draws_yields_frame_done() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut w = CmdWriter::new(s);
|
|
let win = Id::new(0, 1);
|
|
w.window_create(win, window_kind::NORMAL, Id::NONE, [0.0, 0.0, 640.0, 480.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);
|
|
w.texture_create(Id::new(0, 1), usage::RENDER_TARGET | usage::SAMPLED, format::RGBA8, 64, 64, [1, 1], Attr::default());
|
|
w.buffer_create(Id::new(0, 1), buffer_kind::VERTEX, 256, Attr::default());
|
|
let ranges = w.put_items(&[[0u32, 3]]);
|
|
w.frame_begin(7, FrameMeta { input_ns: 1, ..Default::default() });
|
|
w.pass(Pass { color: [Id::new(0, 1), Id::NONE, Id::NONE, Id::NONE], depth: Id::NONE, viewport: [0.0, 0.0, 64.0, 64.0], ..Default::default() });
|
|
let draw = Draw { pipeline: Id::new(0, 1), vb: Id::new(0, 1), ib: Id::NONE, inst: Inst::inline(Slice::of(&[0.0f32; 8]), 2), inst_ranges: ranges, ubufs: [Id::NONE; 4], ..Default::default() };
|
|
w.draw(draw);
|
|
w.draw(draw);
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, ..Default::default() });
|
|
let tex = w.put_ids(&[Id::new(0, 1)]);
|
|
w.draw(Draw { tex, ..draw });
|
|
w.frame_end(win, target::WINDOW);
|
|
hal.submit(&w.buf());
|
|
let mut store = EventStore::new(s, 64, 4096);
|
|
let evs = pump(&hal, &mut store, 0);
|
|
let done: Vec<_> = evs.iter().filter(|(_, e)| e.starts_with("FrameDone")).collect();
|
|
assert_eq!(done.len(), 1, "{evs:?}");
|
|
assert!(done[0].1.contains("frame: 7"));
|
|
assert!(evs.iter().any(|(_, e)| e.starts_with("PipelineReady")));
|
|
assert!(evs.iter().any(|(_, e)| e.starts_with("Startup")));
|
|
let st = hal.with_world(|w| w.session(s).unwrap().stats);
|
|
assert_eq!((st.frames, st.passes, st.draws, st.skipped_draws, st.stale_ids), (1, 2, 3, 0, 0));
|
|
assert!(hal.await_frame_done(s, 7, Duration::from_millis(10)));
|
|
assert!(!hal.await_frame_done(s, 8, Duration::from_millis(10)));
|
|
// a draw with a dead pipeline is skipped, counted, and the frame still completes
|
|
w.clear();
|
|
w.frame_begin(8, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, ..Default::default() });
|
|
w.draw(Draw { pipeline: Id::new(5, 1), ..draw });
|
|
w.frame_end(win, target::WINDOW);
|
|
hal.submit(&w.buf());
|
|
assert!(hal.await_frame_done(s, 8, Duration::from_millis(10)));
|
|
assert_eq!(hal.with_world(|w| w.session(s).unwrap().stats.skipped_draws), 1);
|
|
// steady state: the writer does not grow
|
|
let cap = w.capacities();
|
|
for f in 9..20 {
|
|
w.clear();
|
|
w.frame_begin(f, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, ..Default::default() });
|
|
w.draw(Draw { pipeline: Id::new(0, 1), ..draw });
|
|
w.frame_end(win, target::WINDOW);
|
|
hal.submit(&w.buf());
|
|
}
|
|
assert_eq!(w.capacities(), cap);
|
|
}
|
|
|
|
#[test]
|
|
fn readback_and_snapshot_say_no_raster() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut w = CmdWriter::new(s);
|
|
w.window_create(Id::new(0, 1), window_kind::NORMAL, Id::NONE, [0.0, 0.0, 320.0, 200.0], false);
|
|
w.texture_create(Id::new(0, 1), usage::RENDER_TARGET, format::RGBA8, 8, 8, [1, 1], Attr::default());
|
|
w.texture_readback(Id::new(0, 1), 11, [0, 0, 8, 8]);
|
|
w.texture_readback(Id::new(9, 1), 12, [0, 0, 8, 8]);
|
|
w.snapshot(Id::new(0, 1), 13);
|
|
hal.submit(&w.buf());
|
|
let mut store = EventStore::new(s, 64, 1024);
|
|
let evs: Vec<String> = pump(&hal, &mut store, 0).into_iter().map(|e| e.1).collect();
|
|
let rb = |req: &str| evs.iter().find(|e| e.starts_with("Readback") && e.contains(req)).cloned().unwrap_or_default();
|
|
assert!(rb("req: 11").contains("status: 2"), "{evs:?}");
|
|
assert!(rb("req: 12").contains("status: 1"));
|
|
assert!(rb("req: 13").contains("status: 2") && rb("req: 13").contains("320"));
|
|
}
|
|
|
|
#[test]
|
|
fn timers_follow_the_manual_clock() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
hal.clock_set(1_000);
|
|
let mut w = CmdWriter::new(s);
|
|
w.timer_start(1, 500, true);
|
|
w.timer_start(2, 700, false);
|
|
hal.submit(&w.buf());
|
|
let mut store = EventStore::new(s, 16, 256);
|
|
assert!(pump(&hal, &mut store, 0).iter().all(|e| !e.1.starts_with("Timer")));
|
|
hal.clock_advance(600);
|
|
let e = pump(&hal, &mut store, 0);
|
|
assert_eq!(e.iter().filter(|e| e.1.starts_with("Timer { timer: 1")).count(), 1);
|
|
assert!(e.iter().all(|e| e.0 == 1_600 || e.1.starts_with("Startup")));
|
|
hal.clock_advance(200);
|
|
let e = pump(&hal, &mut store, 0);
|
|
assert_eq!(e.iter().filter(|e| e.1.starts_with("Timer { timer: 2")).count(), 1);
|
|
assert_eq!(hal.now(), 1_800);
|
|
w.clear();
|
|
w.timer_stop(1);
|
|
hal.submit(&w.buf());
|
|
hal.clock_advance(10_000);
|
|
assert!(pump(&hal, &mut store, 0).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn inject_delivers_with_its_time() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut store = EventStore::new(s, 16, 256);
|
|
pump(&hal, &mut store, 0);
|
|
hal.clock_set(555);
|
|
let mut e = RawEvent::key_down(Id::new(1, 1), 65, false, 0);
|
|
e.time_ns = 999;
|
|
hal.inject(s, e);
|
|
hal.inject(s, RawEvent::key_up(Id::new(1, 1), 65, 0));
|
|
let got = pump(&hal, &mut store, 0);
|
|
assert_eq!(got.len(), 2);
|
|
assert_eq!(got[0].0, 999);
|
|
assert!(got[0].1.starts_with("KeyDown"));
|
|
}
|
|
|
|
#[test]
|
|
fn wake_from_another_thread_unblocks_pump() {
|
|
let hal = Hal::new();
|
|
let s = open(&hal, 0);
|
|
let mut store = EventStore::new(s, 16, 256);
|
|
pump(&hal, &mut store, 0);
|
|
let h = hal.clone();
|
|
let t = std::thread::spawn(move || {
|
|
std::thread::sleep(Duration::from_millis(100));
|
|
h.wake();
|
|
});
|
|
let t0 = Instant::now();
|
|
let got = pump(&hal, &mut store, 20_000_000_000);
|
|
let took = t0.elapsed();
|
|
t.join().unwrap();
|
|
assert!(took >= Duration::from_millis(80) && took < Duration::from_secs(10), "{took:?}");
|
|
assert_eq!(got.len(), 1);
|
|
assert!(got[0].1.starts_with("Wake"));
|
|
// a wake that arrives before the pump is not lost
|
|
hal.wake();
|
|
let t0 = Instant::now();
|
|
assert_eq!(pump(&hal, &mut store, 20_000_000_000).len(), 1);
|
|
assert!(t0.elapsed() < Duration::from_secs(5));
|
|
// plain timeout with nothing to deliver
|
|
let t0 = Instant::now();
|
|
assert!(pump(&hal, &mut store, 50_000_000).is_empty());
|
|
assert!(t0.elapsed() >= Duration::from_millis(40));
|
|
}
|
|
|
|
#[test]
|
|
fn the_four_ops_work_through_the_global_hal() {
|
|
let info = hal_open(&OpenReq { backend: backend_id::HEADLESS, app_key: 0xfeed, flags: 0, dpi_hint: 0.0, parent: 0 });
|
|
assert_ne!(info.session, 0);
|
|
assert_eq!(hal_open(&OpenReq { backend: 77, ..Default::default() }).session, 0);
|
|
let mut w = CmdWriter::new(info.session);
|
|
w.window_create(Id::new(0, 1), window_kind::NORMAL, Id::NONE, [0.0, 0.0, 10.0, 10.0], false);
|
|
w.frame_begin(1, FrameMeta::default());
|
|
w.frame_end(Id::new(0, 1), target::WINDOW);
|
|
hal_submit(&w.buf());
|
|
hal_wake();
|
|
let mut store = EventStore::new(info.session, 16, 256);
|
|
let mut buf = store.buf();
|
|
let n = hal_pump(0, &mut buf);
|
|
let evs: Vec<String> = store.reader(n, buf.arena_len).map(|e| format!("{:?}", e.1)).collect();
|
|
assert!(evs.iter().any(|e| e.starts_with("FrameDone")) && evs.iter().any(|e| e.starts_with("Wake")), "{evs:?}");
|
|
// a corrupt batch (misaligned element slice) is rejected whole and counted
|
|
let bytes = [0u8; 13];
|
|
let bad = CmdBuf { session: info.session, cmds: Slice::from_bytes(&bytes), ..Default::default() };
|
|
hal_submit(&bad);
|
|
assert_eq!(Hal::global().with_world(|w| w.session(info.session).unwrap().stats.bad_bufs), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn clock_is_monotonic_and_absolute() {
|
|
let a = clock_ns();
|
|
let b = clock_ns();
|
|
assert!(a > 0 && b >= a);
|
|
}
|
|
|
|
#[test]
|
|
fn relaunch_keeps_the_backend_and_frees_only_the_old_session() {
|
|
let hal = Hal::new();
|
|
let host = hal.with_world(|w| w.host_window([0.0, 0.0, 800.0, 600.0]));
|
|
let a = open(&hal, 7);
|
|
assert_eq!(hal.backend_inits(), 1);
|
|
let mut w = CmdWriter::new(a);
|
|
w.window_create(Id::new(0, 1), window_kind::NORMAL, Id::NONE, [0.0; 4], true);
|
|
w.texture_create(Id::new(3, 1), usage::RENDER_TARGET, format::RGBA8, 32, 32, [1, 1], Attr::default());
|
|
hal.submit(&w.buf());
|
|
assert_eq!(hal.with_world(|w| w.lent_to(host)), Some(a));
|
|
assert_eq!(hal.report(a, 30).unwrap().total_bytes, 32 * 32 * 4);
|
|
// another app is not touched by A's relaunch
|
|
let b = open(&hal, 8);
|
|
let mut wb = CmdWriter::new(b);
|
|
wb.texture_create(Id::new(0, 1), usage::SAMPLED, format::RGBA8, 4, 4, [1, 1], Attr::default());
|
|
hal.submit(&wb.buf());
|
|
// relaunch A
|
|
let a2 = open(&hal, 7);
|
|
assert_ne!(a, a2);
|
|
assert_eq!(hal.backend_inits(), 1, "same backend instance");
|
|
assert!(hal.report(a, 30).is_none(), "old session gone");
|
|
assert_eq!(hal.report_all(30).total_bytes, 4 * 4 * 4, "A's texture freed, B's kept");
|
|
assert_eq!(hal.with_world(|w| w.lent_to(host)), None);
|
|
assert!(hal.with_world(|w| w.host_window_alive(host)));
|
|
// a different backend id on a Hal that has one is refused
|
|
assert_eq!(hal.open(&OpenReq { backend: 5, ..Default::default() }).session, 0);
|
|
assert_eq!(hal.backend_inits(), 1);
|
|
// explicit close frees too
|
|
assert!(hal.close(b));
|
|
assert_eq!(hal.report_all(30).total_bytes, 0);
|
|
}
|
|
|
|
/// A draw names any number of sampled textures (no fixed slot count): the ids round-trip
|
|
/// through the arena, and a slot past the range reads as none.
|
|
#[test]
|
|
fn draw_texture_ids_have_no_slot_limit() {
|
|
use makepad_hal::buffers::{id_at, read_ids};
|
|
let mut w = CmdWriter::new(1);
|
|
let ids: Vec<Id> = (0..13).map(|i| Id::new(i, i + 1)).collect();
|
|
w.put_strs(&["odd"]); // leaves the arena unaligned
|
|
let r = w.put_ids(&ids);
|
|
assert_eq!(r.len, 13);
|
|
assert_eq!(read_ids(w.arena(), r).collect::<Vec<_>>(), ids);
|
|
assert_eq!(id_at(w.arena(), r, 12), Id::new(12, 13));
|
|
assert_eq!(id_at(w.arena(), r, 13), Id::NONE);
|
|
}
|