All outline text draws with Slug curves on every platform (SDF/MSDF stays selectable per size); a frame never presents a blank glyph and retained draw lists keep theirs. One coverage model for vector feathering, widget families split out of the core crate, fewer steady-state allocations, video keeps its aspect (Contain/Cover), render and SSAO pass unification. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
182 lines
9.3 KiB
Rust
182 lines
9.3 KiB
Rust
//! The kernel output lifecycle under the fence-delay test flag, and a hot
|
|
//! reload under load (KERNELS.md P5 gate).
|
|
//!
|
|
//! A simulated renderer draws one frame per iteration and its fences
|
|
//! complete two frames late (the GPU running behind), while kernel jobs on
|
|
//! the engine's pool keep writing the next output. Halfway, the draw
|
|
//! shader changes (a field is added: a new layout id and stride); the new
|
|
//! kernel compiles on another thread while the old one keeps producing.
|
|
//!
|
|
//! Checked every frame: a writer never gets a slot a frame in flight reads;
|
|
//! every drawn buffer is whole records of one layout (never mixed); after
|
|
//! the first publish in the new layout the old one never draws again; old
|
|
//! slots retire only after their fences; a job begun under the old layout
|
|
//! cannot publish.
|
|
|
|
use makepad_render_kernels::compute::boundary::{LayoutFieldView, LayoutView};
|
|
use makepad_render_kernels::compute::kernel::FieldTy;
|
|
use makepad_render_kernels::dispatch::sched::{Job, JobHandle, Priority};
|
|
use makepad_render_kernels::dispatch::admission::{JobBudget, Origin};
|
|
use makepad_render_kernels::{engine, FrameFences, ManualFences, OutputRing, PublishError, SlotState, Topology, WriteLease};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
fn inst_a() -> LayoutView<'static> {
|
|
LayoutView { name: "Inst", stride: 4, fields: &[LayoutFieldView { name: "pos", ty: FieldTy::Vec3, offset: 0 }, LayoutFieldView { name: "tag", ty: FieldTy::I32, offset: 3 }] }
|
|
}
|
|
|
|
fn inst_b() -> LayoutView<'static> {
|
|
const FIELDS: &[LayoutFieldView<'static>] = &[
|
|
LayoutFieldView { name: "pos", ty: FieldTy::Vec3, offset: 0 },
|
|
LayoutFieldView { name: "tag", ty: FieldTy::I32, offset: 3 },
|
|
LayoutFieldView { name: "birth", ty: FieldTy::Vec2, offset: 4 },
|
|
];
|
|
LayoutView { name: "Inst", stride: 6, fields: FIELDS }
|
|
}
|
|
|
|
const SRC_A: &str = "let out = output(Inst)\nlet frame = param(0)\nfn instance(i) { out[i].pos = vec3(float(i), frame, 0.5)\n out[i].tag = 1 }";
|
|
const SRC_B: &str = "let out = output(Inst)\nlet frame = param(0)\nfn instance(i) { out[i].pos = vec3(float(i), frame, 0.5)\n out[i].tag = 2\n out[i].birth = vec2(frame, 1.0) }";
|
|
|
|
fn count_of(frame: usize) -> usize {
|
|
3000 + (frame * 977) % 9000
|
|
}
|
|
|
|
#[test]
|
|
fn slots_under_delayed_fences_and_a_hot_reload_under_load() {
|
|
let fences = ManualFences::with_delay(2);
|
|
let (layout_a, layout_b) = (0xA, 0xB);
|
|
let mut ring = OutputRing::new(2, Topology::Instances, layout_a, 4);
|
|
let kernel_a = makepad_render_kernels::compile(SRC_A, &[inst_a()], &[]).unwrap();
|
|
let mut kernel = kernel_a.clone();
|
|
let mut layout = layout_a;
|
|
// The new shader compiles on its own thread while frames go on.
|
|
let mut compiling: Option<std::thread::JoinHandle<_>> = None;
|
|
let mut running: Option<(JobHandle, WriteLease, usize, u64)> = None;
|
|
// Frame serial -> the buffer it reads (its allocation), until its fence.
|
|
let mut reading: HashMap<u64, usize> = HashMap::new();
|
|
let mut new_layout_seen = false;
|
|
let mut stale_refused = 0;
|
|
let mut drawn = 0;
|
|
for frame in 0..400usize {
|
|
if frame == 150 {
|
|
compiling = Some(std::thread::spawn(|| makepad_render_kernels::compile(SRC_B, &[inst_b()], &[]).unwrap()));
|
|
}
|
|
// A finished job publishes.
|
|
if let Some((h, _, _, _)) = &running {
|
|
if h.is_done() {
|
|
let (h, mut lease, count, _) = running.take().unwrap();
|
|
let mut job = h.try_take().unwrap().unwrap_or_else(|(_, e)| panic!("{}", e));
|
|
lease.set_data(job.take_output_u32("out").unwrap());
|
|
match ring.publish(lease, count as u32, &fences) {
|
|
Ok(_) => {}
|
|
Err(PublishError::StaleLayout { .. }) => stale_refused += 1,
|
|
Err(e) => panic!("{}", e),
|
|
}
|
|
}
|
|
}
|
|
// Start the next output when a slot is free.
|
|
if running.is_none() {
|
|
if let Ok(mut lease) = ring.begin_write(&fences) {
|
|
// Never a buffer a frame in flight still reads.
|
|
let done = fences.completed();
|
|
let ptr = lease.data_mut().as_ptr() as usize;
|
|
if lease.data_mut().capacity() > 0 {
|
|
for (s, p) in &reading {
|
|
assert!(!(*s > done && *p == ptr), "frame {} (in flight, completed {}) reads the buffer handed to a writer", s, done);
|
|
}
|
|
}
|
|
let count = count_of(frame);
|
|
let stride = lease.stride() as usize;
|
|
assert_eq!(stride, if lease.layout() == layout_a { 4 } else { 6 });
|
|
let mut data = lease.take_data();
|
|
data.clear();
|
|
data.resize(count * stride, 0);
|
|
let mut job = Job::new(if lease.layout() == layout { kernel.clone() } else { kernel_a.clone() }, count);
|
|
job.set_param("frame", frame as f32);
|
|
job.output_u32("out", data).unwrap();
|
|
let h = engine().scheduler().submit(job, Priority::Near, Origin::Host, JobBudget::UNLIMITED).unwrap_or_else(|(_, e)| panic!("{}", e));
|
|
running = Some((h, lease, count, layout));
|
|
}
|
|
}
|
|
// The reload lands while an old-layout job runs: that job's output
|
|
// must be refused.
|
|
if running.is_some() && compiling.as_ref().is_some_and(|h| h.is_finished()) {
|
|
kernel = compiling.take().unwrap().join().unwrap();
|
|
layout = layout_b;
|
|
ring.set_layout(layout_b, 6);
|
|
}
|
|
// Draw this frame, then submit it.
|
|
let serial = fences.submitted() + 1;
|
|
if let Some(v) = ring.draw(&fences) {
|
|
drawn += 1;
|
|
let stride = v.stride as usize;
|
|
assert_eq!(v.data.len(), v.count as usize * stride, "whole records");
|
|
let tag = if v.layout == layout_a { 1 } else { 2 };
|
|
assert_eq!(stride, if tag == 1 { 4 } else { 6 });
|
|
for r in v.data.chunks_exact(stride) {
|
|
assert_eq!(r[3], tag, "one layout per draw");
|
|
}
|
|
if v.layout == layout_b {
|
|
new_layout_seen = true;
|
|
} else {
|
|
assert!(!new_layout_seen, "the old layout drew again after the new one published");
|
|
}
|
|
reading.insert(serial, v.data.as_ptr() as usize);
|
|
}
|
|
assert_eq!(fences.submit(), serial);
|
|
fences.complete(serial);
|
|
let done = fences.completed();
|
|
reading.retain(|s, _| *s > done);
|
|
std::thread::sleep(Duration::from_micros(300));
|
|
}
|
|
if let Some((h, lease, _, _)) = running.take() {
|
|
let mut job = h.wait().unwrap_or_else(|(_, e)| panic!("{}", e));
|
|
let _ = job.take_output_u32("out");
|
|
ring.abandon(lease);
|
|
}
|
|
assert!(new_layout_seen, "the new layout published");
|
|
let stats = ring.stats();
|
|
assert!(stats.published > 20, "{:?}", stats);
|
|
assert!(stats.busy > 0, "with fences two frames late some frames find no free slot: {:?}", stats);
|
|
assert_eq!(stats.layout_swaps, 1);
|
|
assert_eq!(stale_refused, 1, "the job begun under the old layout could not publish");
|
|
assert!(stats.max_in_flight >= 1);
|
|
assert!(drawn > 300);
|
|
// Old slots retire once their fences signal.
|
|
fences.complete(u64::MAX);
|
|
for _ in 0..3 {
|
|
fences.submit();
|
|
}
|
|
ring.collect(&fences);
|
|
assert!(ring.slot_states().iter().all(|s| matches!(s, SlotState::Free | SlotState::Published)), "{:?}", ring.slot_states());
|
|
println!("stats {:?}, stale refused {}", stats, stale_refused);
|
|
}
|
|
|
|
#[test]
|
|
fn an_emitted_index_buffer_is_validated_before_publish() {
|
|
// A kernel emits triangles whose indices point at its own vertices;
|
|
// a corrupt one (an index past the count) keeps the previous output.
|
|
let fences = ManualFences::new();
|
|
let mut ring = OutputRing::new(2, Topology::Triangles { indexed: true }, 1, 3);
|
|
let k = makepad_render_kernels::compile("let v = output(vec3)\nlet ix = output(i32, 3)\nlet bad = param(0)\nfn element(i) { v[i] = vec3(float(i), 0.0, 0.0)\n let o = if bad > 0.5 && i == 2 { 100 } else { 0 }\n ix[i] = [i, (i + 1) % count, (i + 2) % count + o] }", &[], &[]);
|
|
let k = match k {
|
|
Ok(k) => k,
|
|
// Arrays as outputs are optional syntax; the scalar form below.
|
|
Err(_) => makepad_render_kernels::compile("let v = output(vec3)\nlet a = output(i32, 3, 0, ix)\nlet b = output(i32, 3, 1, ix)\nlet c = output(i32, 3, 2, ix)\nlet bad = param(0)\nfn element(i) { v[i] = vec3(float(i), 0.0, 0.0)\n a[i] = i\n b[i] = (i + 1) % count\n c[i] = if bad > 0.5 && i == 2 { 100 } else { (i + 2) % count } }", &[], &[]).unwrap(),
|
|
};
|
|
for (bad, ok) in [(0.0, true), (1.0, false)] {
|
|
let mut lease = ring.begin_write(&fences).unwrap();
|
|
let mut job = Job::new(k.clone(), 5);
|
|
job.set_param("bad", bad);
|
|
job.output_u32("v", vec![0; 15]).unwrap();
|
|
job.output_u32("ix", vec![0; 15]).unwrap();
|
|
let mut job = engine().run(job, Priority::MustComplete).unwrap_or_else(|(_, e)| panic!("{}", e));
|
|
lease.set_data(job.take_output_u32("v").unwrap());
|
|
*lease.indices_mut() = job.take_output_u32("ix").unwrap();
|
|
assert_eq!(ring.publish(lease, 5, &fences).is_ok(), ok);
|
|
}
|
|
assert_eq!(ring.generation(), 1, "the corrupt output did not replace the good one");
|
|
assert_eq!(ring.draw(&fences).unwrap().indices[6..9], [2, 3, 4]);
|
|
let _ = Arc::new(());
|
|
}
|