makepad/platform/hal/tests/framecap_vulkan.rs

149 lines
7 KiB
Rust

//! Frame capture on Vulkan (Linux, offscreen windows): a recorded frame replays in a private HAL
//! to the same pixels, a cut after draw k shows the first k draws, the texture the frame names is
//! captured with its contents.
#![cfg(target_os = "linux")]
use makepad_hal::framecap::replay::{Cut, Opts, Replayer};
use makepad_hal::framecap::{self, file};
use makepad_hal::spirv::SpirvPipeline;
use makepad_hal::*;
use makepad_shader::shader_ir::*;
use makepad_shader::shader_ir_spirv::ir_to_spirv;
use std::time::{Duration, Instant};
const WIN: Id = Id::new(1, 1);
const TEX: Id = Id::new(1, 1);
const VERTEX: Id = Id::new(1, 1);
const INDEX: Id = Id::new(2, 1);
const PIPE: Id = Id::new(1, 1);
const W: u32 = 60;
const H: u32 = 30;
/// A full-target triangle in white (the geometry is clip space).
fn shader() -> Vec<u8> {
let mut ir = IrModule::new();
let vec4 = ir.vec_ty(4, IrScalar::F32);
let mut vertex = IrFunction::new("vertex_main".into(), TY_VOID);
vertex.entry = Some(IrEntry { stage: IrStage::Vertex,
inputs: vec![IrEntryIo { name: "geometry".into(), ty: vec4, binding: IrIoBinding::Location(0) }],
outputs: vec![IrEntryIo { name: "position".into(), ty: vec4, binding: IrIoBinding::Builtin(IrBuiltinIo::Position) }] });
let input = vertex.expr(ExprKind::EntryIn(0), vec4);
let output = vertex.expr(ExprKind::EntryOut(0), vec4);
vertex.body.extend([Stmt::Assign(output, input), Stmt::Return(NO_EXPR)]);
let mut fragment = IrFunction::new("fragment_main".into(), TY_VOID);
fragment.entry = Some(IrEntry { stage: IrStage::Fragment, inputs: Vec::new(),
outputs: vec![IrEntryIo { name: "color".into(), ty: vec4, binding: IrIoBinding::Location(0) }] });
let one = fragment.expr(ExprKind::Lit(IrLit::F32(1.0)), TY_F32);
let color = fragment.expr(ExprKind::Construct(vec![one; 4]), vec4);
let output = fragment.expr(ExprKind::EntryOut(0), vec4);
fragment.body.extend([Stmt::Assign(output, color), Stmt::Return(NO_EXPR)]);
ir.functions.extend([vertex, fragment]);
SpirvPipeline { vertex: ir_to_spirv(&ir, 0, false).unwrap(), fragment: ir_to_spirv(&ir, 1, false).unwrap(),
texture_base: 0, sampler_base: 0, depth_binding: u32::MAX, instance_binding: u32::MAX,
pick_binding: u32::MAX, pick_texture_base: u32::MAX, textures: Vec::new(), samplers: Vec::new() }.encode()
}
fn until(hal: &Hal, 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;
}
}
}
}
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 ids = w.put_ids(&[TEX]);
for i in 0..3 {
w.draw(Draw { pipeline: PIPE, vb: VERTEX, ib: INDEX, index: [0, 3], inst: Inst::inline(Slice::EMPTY, 1), tex: ids,
scissor: [i as f32 * 20.0, 0.0, 10.0, H as f32], bounds: [f32::NAN; 4], ..Default::default() });
}
w.frame_end(WIN, target::WINDOW);
}
#[test]
fn a_captured_vulkan_frame_replays_to_the_same_pixels() {
let hal = Hal::new();
let info = hal.open(&OpenReq { backend: backend_id::VULKAN, flags: open_flags::OFFSCREEN_WINDOWS, ..Default::default() });
if info.backend != backend_id::VULKAN {
eprintln!("SKIP: no Vulkan 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, 7, 9, 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 vertices: Vec<u8> = [-1.0f32, -1.0, 0.0, 1.0, 3.0, -1.0, 0.0, 1.0, -1.0, 3.0, 0.0, 1.0].into_iter().flat_map(f32::to_le_bytes).collect();
w.buffer_create(VERTEX, buffer_kind::VERTEX, vertices.len() as u64, Attr::default());
w.buffer_update(VERTEX, 0, &vertices);
w.buffer_create(INDEX, buffer_kind::INDEX, 6, Attr::default());
w.buffer_update(INDEX, 0, &[0, 0, 1, 0, 2, 0]);
let blob = CmdWriter::pipeline_blob(PipelineDesc { outputs: 1, ..Default::default() }, &shader(), &[u32::MAX, u32::MAX, u32::MAX, u32::MAX, u32::MAX, 0], &[16]);
w.pipeline_create(PIPE, dialect::SPIRV, Attr::default(), &blob);
hal.submit(&w.buf());
until(&hal, &mut store, |e| matches!(e, HalEvent::PipelineReady { .. }));
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, &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");
}
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, &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, 1, 1, 2));
assert_eq!(cap.textures[0].contents.as_deref(), Some(&texels[..]));
let cap = file::decode(&file::encode(&cap)).unwrap();
let mut r = Replayer::new(&cap).unwrap();
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.rgba, live, "the replay draws what the app drew");
assert_eq!(img.px(45, 10), [255; 4]);
r.run(&cap, Cut::after(&cap, 1), Opts::default()).unwrap();
let two = r.read(&cap, rt, None).unwrap();
assert_eq!((two.px(5, 10), two.px(25, 10), two.px(45, 10)), ([255; 4], [255; 4], [0, 0, 0, 255]));
}