261 lines
12 KiB
Rust
261 lines
12 KiB
Rust
//! Frame capture on a real device: a frame recorded from one HAL replays in a private HAL to the
|
|
//! same pixels, and the per-draw cut gives each draw's own pixels. Metal (offscreen windows: no
|
|
//! window shows).
|
|
#![cfg(target_vendor = "apple")]
|
|
|
|
use makepad_hal::framecap::replay::{Cut, Opts, Replayer};
|
|
use makepad_hal::framecap::{self, file};
|
|
use makepad_hal::*;
|
|
use std::time::{Duration, Instant};
|
|
|
|
const WIN: Id = Id::new(1, 1);
|
|
const TEX: Id = Id::new(1, 1);
|
|
const QUAD: Id = Id::new(1, 1);
|
|
const INDEX: Id = Id::new(2, 1);
|
|
const SOLID: Id = Id::new(1, 1);
|
|
const SAMPLED: Id = Id::new(2, 1);
|
|
const W: u32 = 64;
|
|
const H: u32 = 48;
|
|
|
|
const MSL_SOLID: &str = r#"
|
|
#include <metal_stdlib>
|
|
using namespace metal;
|
|
struct V { float4 pos [[position]]; float4 color; };
|
|
vertex V vertex_main(uint vid [[vertex_id]], uint iid [[instance_id]], constant float2 *geom [[buffer(0)]], constant float4 *inst [[buffer(1)]]) {
|
|
float4 r = inst[iid * 2];
|
|
V o;
|
|
float2 p = r.xy + geom[vid] * r.zw;
|
|
o.pos = float4(p.x * 2.0 - 1.0, 1.0 - p.y * 2.0, 0.0, 1.0);
|
|
o.color = inst[iid * 2 + 1];
|
|
return o;
|
|
}
|
|
fragment float4 fragment_main(V i [[stage_in]]) { return i.color; }
|
|
"#;
|
|
|
|
const MSL_SAMPLED: &str = r#"
|
|
#include <metal_stdlib>
|
|
using namespace metal;
|
|
struct V { float4 pos [[position]]; float2 uv; };
|
|
vertex V vertex_main(uint vid [[vertex_id]], uint iid [[instance_id]], constant float2 *geom [[buffer(0)]], constant float4 *inst [[buffer(1)]]) {
|
|
float4 r = inst[iid * 2];
|
|
V o;
|
|
float2 p = r.xy + geom[vid] * r.zw;
|
|
o.pos = float4(p.x * 2.0 - 1.0, 1.0 - p.y * 2.0, 0.0, 1.0);
|
|
o.uv = geom[vid];
|
|
return o;
|
|
}
|
|
fragment float4 fragment_main(V i [[stage_in]], texture2d<float> t [[texture(0)]]) {
|
|
constexpr sampler s(filter::nearest);
|
|
return t.sample(s, i.uv);
|
|
}
|
|
"#;
|
|
|
|
fn until(hal: &Hal, _sid: u32, store: &mut EventStore, mut accept: impl FnMut(HalEvent<'_>) -> bool) {
|
|
let deadline = Instant::now() + Duration::from_secs(30);
|
|
loop {
|
|
assert!(Instant::now() < deadline, "event deadline");
|
|
let mut buf = store.buf();
|
|
let n = hal.pump(10_000_000, &mut buf);
|
|
let len = buf.arena_len;
|
|
for (_, e) in store.reader(n, len) {
|
|
if let HalEvent::PipelineFailed { log, .. } = e {
|
|
panic!("pipeline: {}", String::from_utf8_lossy(log));
|
|
}
|
|
if accept(e) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The instance data of the three draws (red quarter, textured right half, green middle): it is
|
|
/// the writer's borrowed memory until the submit, so it lives in a static.
|
|
static INSTANCES: [[f32; 8]; 3] = [
|
|
[0.0, 0.0, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0],
|
|
[0.5, 0.0, 0.5, 1.0, 0.0, 0.0, 0.0, 0.0],
|
|
[0.25, 0.25, 0.5, 0.5, 0.0, 1.0, 0.0, 1.0],
|
|
];
|
|
|
|
fn frame(w: &mut CmdWriter, n: u64) {
|
|
w.clear();
|
|
w.frame_begin(n, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, load: [pass_load::CLEAR; 5], clear: [[0.0, 0.0, 0.0, 1.0]; 4], viewport: [0.0, 0.0, W as f32, H as f32], dpi: 1.0, ..Default::default() });
|
|
let base = Draw { vb: QUAD, ib: INDEX, index: [0, 6], scissor: [f32::NAN; 4], bounds: [f32::NAN; 4], ..Default::default() };
|
|
let ids = w.put_ids(&[TEX]);
|
|
for (i, (pipe, tex)) in [(SOLID, Range::default()), (SAMPLED, ids), (SOLID, Range::default())].into_iter().enumerate() {
|
|
w.draw(Draw { pipeline: pipe, inst: Inst::inline(Slice::of(&INSTANCES[i][..]), 1), tex, ..base });
|
|
}
|
|
w.frame_end(WIN, target::WINDOW);
|
|
}
|
|
|
|
#[test]
|
|
fn a_captured_frame_replays_to_the_same_pixels() {
|
|
let hal = Hal::new();
|
|
let info = hal.open(&OpenReq { backend: backend_id::METAL, flags: open_flags::OFFSCREEN_WINDOWS, ..Default::default() });
|
|
if info.backend != backend_id::METAL {
|
|
eprintln!("SKIP: no Metal device");
|
|
return;
|
|
}
|
|
let sid = info.session;
|
|
let mut store = EventStore::new(sid, 128, 1 << 22);
|
|
let mut w = CmdWriter::new(sid);
|
|
w.window_create(WIN, window_kind::NORMAL, Id::NONE, [0.0, 0.0, W as f32, H as f32], false);
|
|
let texels: Vec<u8> = (0..16u32).flat_map(|i| [(i * 16) as u8, 0, 255 - (i * 16) as u8, 255]).collect();
|
|
w.texture_create(TEX, usage::SAMPLED, format::RGBA8, 4, 4, [1, 1], Attr::default());
|
|
w.texture_update(TEX, 0, [0, 0, 4, 4], 0, &texels);
|
|
let quad: Vec<u8> = [0.0f32, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0].iter().flat_map(|f| f.to_le_bytes()).collect();
|
|
w.buffer_create(QUAD, buffer_kind::VERTEX, quad.len() as u64, Attr::default());
|
|
w.buffer_update(QUAD, 0, &quad);
|
|
let idx: Vec<u8> = [0u16, 1, 2, 0, 2, 3].iter().flat_map(|i| i.to_le_bytes()).collect();
|
|
w.buffer_create(INDEX, buffer_kind::INDEX, idx.len() as u64, Attr::default());
|
|
w.buffer_update(INDEX, 0, &idx);
|
|
let none = pipeline_binding::NONE;
|
|
for (id, code, ntex) in [(SOLID, MSL_SOLID, 0u32), (SAMPLED, MSL_SAMPLED, 1)] {
|
|
let blob = CmdWriter::pipeline_blob(PipelineDesc { outputs: 1, instance_stride: 8, color_formats: [format::BGRA8, 0, 0, 0], ..Default::default() }, code.as_bytes(), &[none, none, none, none, none, ntex], &[8]);
|
|
w.pipeline_create(id, dialect::MSL, Attr::default(), &blob);
|
|
}
|
|
hal.submit(&w.buf());
|
|
let mut ready = 0;
|
|
until(&hal, sid, &mut store, |e| {
|
|
ready += matches!(e, HalEvent::PipelineReady { .. }) as u32;
|
|
ready == 2
|
|
});
|
|
// frames until every draw is drawn (pipeline variants for the window's layout are built lazily)
|
|
let mut n = 1;
|
|
loop {
|
|
let before = hal.with_world(|wd| wd.session(sid).unwrap().stats.skipped_draws);
|
|
frame(&mut w, n);
|
|
hal.submit(&w.buf());
|
|
let mut partial = false;
|
|
until(&hal, sid, &mut store, |e| match e {
|
|
HalEvent::FramePartial { frame, .. } if frame == n => {
|
|
partial = true;
|
|
false
|
|
}
|
|
HalEvent::FrameDone { frame, .. } => frame == n,
|
|
_ => false,
|
|
});
|
|
n += 1;
|
|
if !partial && hal.with_world(|wd| wd.session(sid).unwrap().stats.skipped_draws) == before {
|
|
break;
|
|
}
|
|
assert!(n < 200, "draws stayed skipped");
|
|
}
|
|
// record the next frame
|
|
let seq = framecap::arm(Some(sid), None);
|
|
frame(&mut w, n);
|
|
w.snapshot(WIN, 99);
|
|
hal.submit(&w.buf());
|
|
let mut live = Vec::new();
|
|
until(&hal, sid, &mut store, |e| match e {
|
|
HalEvent::Readback { req: 99, status, pixels, .. } => {
|
|
assert_eq!(status, readback::OK);
|
|
live = pixels.to_vec();
|
|
true
|
|
}
|
|
_ => false,
|
|
});
|
|
let cap = framecap::wait(seq, Duration::from_secs(10)).expect("the capture finished");
|
|
let mut cap = (*cap).clone();
|
|
cap.backend = info.backend;
|
|
cap.dialect = info.dialect;
|
|
assert_eq!((cap.passes.len(), cap.draws.len(), cap.pipelines.len(), cap.textures.len(), cap.buffers.len()), (1, 3, 2, 1, 2));
|
|
assert_eq!(cap.textures[0].contents.as_deref(), Some(&texels[..]), "texture contents as uploaded");
|
|
assert_eq!(cap.size, [W, H]);
|
|
|
|
// the file round trip, then the replay in a private HAL
|
|
let cap = file::decode(&file::encode(&cap)).unwrap();
|
|
let mut r = Replayer::new(&cap).unwrap();
|
|
assert!(r.failed.is_empty(), "{:?}", r.failed);
|
|
r.run(&cap, Cut::all(&cap), Opts::default()).unwrap();
|
|
let rt = r.target_of(&cap, 0);
|
|
let img = r.read(&cap, rt, None).unwrap();
|
|
assert_eq!((img.w, img.h), (W, H));
|
|
assert_eq!(img.rgba, live, "the replay draws what the app drew");
|
|
|
|
// cut after the first draw: only the red quarter
|
|
r.run(&cap, Cut::after(&cap, 0), Opts::default()).unwrap();
|
|
let first = r.read(&cap, rt, None).unwrap();
|
|
assert_eq!(first.px(5, 5), [255, 0, 0, 255]);
|
|
assert_eq!(first.px(40, 5), [0, 0, 0, 255], "the textured draw is not drawn yet");
|
|
assert_eq!(first.px(20, 20), [255, 0, 0, 255], "the green draw is not drawn yet");
|
|
// the whole frame: green over red in the middle, the texture on the right
|
|
assert_eq!(img.px(20, 20), [0, 255, 0, 255]);
|
|
assert_ne!(img.px(60, 40), [0, 0, 0, 255]);
|
|
// the answers an inspector gets, from the file
|
|
let path = std::env::temp_dir().join(format!("framecap-test-{}.mkcap", std::process::id()));
|
|
std::fs::write(&path, file::encode(&cap)).unwrap();
|
|
let ask = |q: &str| {
|
|
fn d(req: &makepad_remote::Req, out: &mut makepad_remote::Out) {
|
|
framecap::dispatch(req, out);
|
|
}
|
|
let (bytes, _) = makepad_remote::cmd::answer(d, &format!("{q} file={}", path.display()));
|
|
String::from_utf8(bytes).unwrap()
|
|
};
|
|
let frame = ask("cap frame");
|
|
assert!(frame.contains("1 passes, 3 draws"), "{frame}");
|
|
for (draw, px) in [(0, 768), (1, 1536), (2, 768)] {
|
|
let a = ask(&format!("cap draw D{draw}"));
|
|
assert!(a.contains(&format!("changed {px} px")), "{a}");
|
|
}
|
|
let pixel = ask("cap pixel 20,20");
|
|
assert!(pixel.contains("2 draws changed it; the last is D2"), "{pixel}");
|
|
let od = ask("cap overdraw");
|
|
assert!(od.contains("peak 2"), "{od}");
|
|
let shown = ask("cap rt D1 wire=1");
|
|
assert!(shown.contains("png: "), "{shown}");
|
|
let _ = std::fs::remove_file(&path);
|
|
// a frame of two packets: a texture pass (an offscreen view) then the window that samples it
|
|
const RT: Id = Id::new(2, 1);
|
|
w.clear();
|
|
w.texture_create(RT, usage::RENDER_TARGET | usage::SAMPLED, format::RGBA8, 32, 32, [1, 1], Attr::default());
|
|
hal.submit(&w.buf());
|
|
let two = |w: &mut CmdWriter, n: u64| {
|
|
w.clear();
|
|
w.frame_begin(n, FrameMeta::default());
|
|
w.pass(Pass { color: [RT, Id::NONE, Id::NONE, Id::NONE], depth: Id::NONE, load: [pass_load::CLEAR; 5], clear: [[0.0, 0.0, 1.0, 1.0]; 4], viewport: [0.0, 0.0, 32.0, 32.0], dpi: 1.0, ..Default::default() });
|
|
let base = Draw { vb: QUAD, ib: INDEX, index: [0, 6], scissor: [f32::NAN; 4], bounds: [f32::NAN; 4], ..Default::default() };
|
|
w.draw(Draw { pipeline: SOLID, inst: Inst::inline(Slice::of(&INSTANCES[0][..]), 1), ..base });
|
|
w.frame_end(RT, target::TEXTURE);
|
|
w.frame_begin(n + 1, FrameMeta::default());
|
|
w.pass(Pass { color: [Id::NONE; 4], depth: Id::NONE, load: [pass_load::CLEAR; 5], clear: [[0.0, 0.0, 0.0, 1.0]; 4], viewport: [0.0, 0.0, W as f32, H as f32], dpi: 1.0, ..Default::default() });
|
|
let ids = w.put_ids(&[RT]);
|
|
w.draw(Draw { pipeline: SAMPLED, inst: Inst::inline(Slice::of(&INSTANCES[1][..]), 1), tex: ids, ..base });
|
|
w.frame_end(WIN, target::WINDOW);
|
|
};
|
|
let mut n = n + 10;
|
|
for _ in 0..30 {
|
|
two(&mut w, n);
|
|
hal.submit(&w.buf());
|
|
until(&hal, sid, &mut store, |e| matches!(e, HalEvent::FrameDone { frame, .. } if frame == n + 1));
|
|
n += 2;
|
|
}
|
|
let seq = framecap::arm(Some(sid), None);
|
|
two(&mut w, n);
|
|
w.snapshot(WIN, 98);
|
|
hal.submit(&w.buf());
|
|
let mut live2 = Vec::new();
|
|
until(&hal, sid, &mut store, |e| match e {
|
|
HalEvent::Readback { req: 98, pixels, .. } => {
|
|
live2 = pixels.to_vec();
|
|
true
|
|
}
|
|
_ => false,
|
|
});
|
|
let mut cap2 = (*framecap::wait(seq, Duration::from_secs(10)).expect("the two-packet capture")).clone();
|
|
cap2.backend = info.backend;
|
|
cap2.dialect = info.dialect;
|
|
assert_eq!((cap2.passes.len(), cap2.draws.len()), (2, 2), "both packets recorded");
|
|
assert!(cap2.texture(RT).unwrap().contents.is_none(), "the record clears the offscreen target itself");
|
|
let mut r2 = Replayer::new(&cap2).unwrap();
|
|
r2.run(&cap2, Cut::all(&cap2), Opts::default()).unwrap();
|
|
let rt2 = r2.target_of(&cap2, 1);
|
|
assert_eq!(r2.read(&cap2, rt2, None).unwrap().rgba, live2, "two packets replay as drawn");
|
|
r2.run(&cap2, Cut { pass: 0, draws: 0 }, Opts::default()).unwrap();
|
|
let off = r2.target_of(&cap2, 0);
|
|
assert_eq!(r2.read(&cap2, off, Some([30, 30, 1, 1])).unwrap().px(0, 0), [0, 0, 255, 255], "the offscreen pass's clear");
|
|
|
|
// skipping the last draw leaves the red
|
|
r.run(&cap, Cut::all(&cap), Opts { skip: Some(2), ..Default::default() }).unwrap();
|
|
assert_eq!(r.read(&cap, rt, Some([20, 20, 1, 1])).unwrap().px(0, 0), [255, 0, 0, 255]);
|
|
}
|