makepad/libs/frametween/examples/tween_gate.rs
Admin 517308e676 vj: deck sync as a held lock, archive.org as a content source, frametween becomes a library, runs execute in the app
Squashed from work:
- vj decks: sync is a held lock — pinned master, per-pump rate servo
- vj: the deck explorer opens on music
- frametween: the VJ's in-betweener becomes a library — the whole mode set, one definition
- frametween: the gate says which tier moved the picture, from pixels
- frametween: BGRA words go in as they are, and a host may own the clock
- vj: archive.org as a content source
- platform: native file and save dialogs, in-house on all three desktops
- vj: rounder slider caps, and the cap body reads as a blob
- ai-hub: makepad-asset-ai becomes makepad-ai-hub at libs/ai/hub, the chat pane becomes makepad-chat-ui, the service bin
- vj: DREAM runs execute in the app — pipelines.rs becomes the run it used to watch (aicore §9 / F1)
- vj: plain generations execute in the app too — the store's job queue loses its last vj client (aicore §9 / F2)
- asset-creator: the runner — generate one thing and put it in the catalog, one implementation for every surface (aicore
- importer + asset-server host: the coordination era ends (aicore P7)
- client + chat dispatcher: the dead wire comes out (aicore P7/P8)
- zero-warning sweep, round five — vj and chat-ui
- zero-warning sweep, round six — three cascades
- zero-warning sweep, round seven — the last two
2026-09-01 16:46:34 +02:00

401 lines
14 KiB
Rust

//! THE GATE — every mode, on the real GPU, judged from pixels.
//!
//! Feeds one synthetic pair (a marker block riding a textured field, 96 px
//! of motion between the two endpoints) through each tier at t = 0.5, reads
//! the warp target back, and says where the marker landed:
//!
//! ```text
//! None marker at frame B, full strength (a hard swap)
//! Crossfade marker in BOTH places, half strength (a blend, nothing moved)
//! Flow ONE marker, full strength, halfway (features moved)
//! AI1/2/3 the same, from the neural producer
//! ```
//!
//! Run it through the GPU guard, never bare:
//!
//! ```text
//! local/tools/gpu-guard -t 300 -n tweengate -- \
//! cargo run -p makepad-frametween --example tween_gate --release
//! ```
//!
//! Exit status is the verdict: 0 = every tier behaved, 1 = one did not.
//! `FRAMETWEEN_GATE_PNG=<dir>` also dumps what each tier drew.
pub use makepad_widgets;
use makepad_frametween::selftest::{
gate_pair, read_block_bgra, BlockReading, BLOCK_B_X, BLOCK_MID_X, GATE_H, GATE_W,
};
use makepad_frametween::{
ai2_frame_plan, default_model_path, rife_proxy_dims, Ai2Pair, FlowTweenView, Mode, RifeJob,
RifeProduct, RifeProductKind, RifeService, RifeSource,
};
use makepad_widgets::*;
use std::sync::Arc;
app_main!(App);
script_mod! {
use mod.prelude.widgets.*
use mod.widgets.*
startup() do #(App::script_component(vm)){
ui: Root{
main_window := Window{
window.inner_size: vec2(320, 200)
body +: {
tween := FlowTweenView{}
}
}
}
}
}
/// How long a stage may wait before the gate calls it failed.
const SETTLE_FRAMES: u32 = 10;
const NEURAL_FRAMES: u32 = 900;
#[derive(Clone, Copy, PartialEq)]
enum Stage {
/// Upload the pair and let the flow stack derive.
Feed,
/// Ask the neural producer for this tier's product and wait for it.
Neural(u32),
/// Let the warp draw with everything in place.
Settle(u32),
/// Read the target back and judge.
Read,
}
#[derive(Script, ScriptHook)]
pub struct App {
#[live]
ui: WidgetRef,
#[rust]
pump: NextFrame,
#[rust(0usize)]
which: usize,
#[rust(Stage::Feed)]
stage: Stage,
#[rust]
rife: Option<RifeService>,
#[rust]
rife_broken: bool,
#[rust]
offered: bool,
#[rust]
failures: usize,
#[rust]
reported: usize,
}
impl App {
fn view<R>(
&self,
cx: &mut Cx,
f: impl FnOnce(&mut Cx, &mut FlowTweenView) -> R,
) -> Option<R> {
let widget = self.ui.widget(cx, ids!(tween));
let mut view = widget.borrow_mut::<FlowTweenView>()?;
Some(f(cx, &mut view))
}
/// The tiers to run: all of them, minus the neural ones when there is
/// no checkpoint to run them with (said out loud, never skipped
/// silently).
fn plan(&self) -> Vec<Mode> {
let neural = default_model_path().exists() && !self.rife_broken;
Mode::ALL
.iter()
.copied()
.filter(|m| neural || !m.uses_ai())
.collect()
}
/// Where this tier is supposed to put the marker, and what shape the
/// reading should have.
fn judge(mode: Mode, r: BlockReading) -> Result<String, String> {
match mode {
Mode::None => {
let off = r.offset_from(BLOCK_B_X);
if r.moved() && off.abs() <= 6 {
Ok(format!("hard swap: marker at frame B, offset {off:+}px"))
} else {
Err(format!("expected frame B untouched, got {r:?}"))
}
}
Mode::Crossfade => {
if r.ghosted() {
Ok(format!(
"blend: no full-strength marker anywhere, {} px tinted across both places",
r.tinted_width
))
} else {
Err(format!("expected a dissolve (two half ghosts), got {r:?}"))
}
}
_ => {
let off = r.offset_from(BLOCK_MID_X);
if r.moved() && off.abs() <= 14 {
Ok(format!(
"MOVED: one full-strength marker {} px wide, {off:+}px off the midpoint",
r.full_width
))
} else if r.ghosted() {
Err(format!("ghosted like a crossfade — features did not move: {r:?}"))
} else {
Err(format!("marker is {off:+}px off the midpoint: {r:?}"))
}
}
}
}
fn feed(&mut self, cx: &mut Cx, mode: Mode) {
let (a, b) = gate_pair();
let (w, h) = (GATE_W as u32, GATE_H as u32);
self.view(cx, |cx, view| {
view.clear(cx);
view.set_pair_rgb8(cx, &a, &b, w, h);
view.set_fade(cx, matches!(mode, Mode::None | Mode::Crossfade));
// NONE holds the newest picture: t = 1 samples frame B exactly
// through every producer's math.
view.set_t(cx, if mode == Mode::None { 1.0 } else { 0.5 });
view.redraw(cx);
});
}
/// Offer this tier's neural job and adopt the product when it lands.
fn neural(&mut self, cx: &mut Cx, mode: Mode) -> bool {
if self.rife.is_none() {
match RifeService::start(&default_model_path()) {
Ok(service) => self.rife = Some(service),
Err(error) => {
log!("gate: neural producer unavailable: {error}");
self.rife_broken = true;
return true;
}
}
}
let (a, b) = gate_pair();
let (pw, ph) = rife_proxy_dims(GATE_W as u32, GATE_H as u32);
let rife = self.rife.as_ref().unwrap();
if !self.offered {
self.offered = rife.offer_next(RifeJob {
generation: 1,
a: 0,
b: 1,
kind: match mode {
Mode::Ai2 => RifeProductKind::Midpoint,
Mode::Ai3 => RifeProductKind::Subdivision { depth: 1 },
_ => RifeProductKind::Field,
},
frames: RifeSource::Rgb8 {
a: Arc::new(a),
b: Arc::new(b),
width: GATE_W,
height: GATE_H,
},
width: pw,
height: ph,
deadline: std::time::Instant::now() + std::time::Duration::from_secs(60),
});
return false;
}
let Some(product) = rife.take() else { return false };
self.view(cx, |cx, view| match &product {
RifeProduct::Field(field) => {
view.set_rife_field(cx, 0, field.width, field.height, &field.flow, &field.mask);
}
RifeProduct::Midpoint(midpoint) => {
view.set_ai2_midpoint(cx, midpoint);
// At t = 0.5 a fresh midpoint hands the beat to the second
// half-pair at its own t = 0 — which IS the neural picture.
let plan = ai2_frame_plan(true, 0.5);
view.select_ai2_pair(cx, plan.pair);
view.set_t(cx, plan.t);
debug_assert_eq!(plan.pair, Ai2Pair::SecondHalf);
}
RifeProduct::Subdivision(subdivision) => {
view.set_ai3_subdivision(cx, subdivision, 1);
view.select_ai3_pair(cx, 1);
view.set_t(cx, 0.0);
}
});
true
}
fn read(&mut self, cx: &mut Cx, mode: Mode) {
let tex = self.view(cx, |_cx, view| view.output_texture()).flatten();
let Some(tex) = tex else {
log!("FAIL {:<10} the warp never rendered", mode.label());
self.failures += 1;
return;
};
let Some((w, h, bgra)) = cx.debug_read_render_texture(&tex) else {
log!("FAIL {:<10} render target readback failed", mode.label());
self.failures += 1;
return;
};
if let Some(dir) = std::env::var_os("FRAMETWEEN_GATE_PNG") {
let mut rgba = vec![255u8; bgra.len()];
for (o, px) in rgba.chunks_exact_mut(4).zip(bgra.chunks_exact(4)) {
(o[0], o[1], o[2]) = (px[2], px[1], px[0]);
}
let path = std::path::Path::new(&dir)
.join(format!("tween_gate_{}.png", mode.short().to_lowercase()));
if let Ok(png) = encode_png_rgba(&rgba, w as u32, h as u32) {
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(&path, png);
}
}
let reading = read_block_bgra(&bgra, w, h);
match Self::judge(mode, reading) {
Ok(note) => log!("PASS {:<10} {note}", mode.label()),
Err(note) => {
log!("FAIL {:<10} {note}", mode.label());
self.failures += 1;
}
}
self.reported += 1;
}
fn step(&mut self, cx: &mut Cx) {
let plan = self.plan();
let Some(&mode) = plan.get(self.which) else {
let skipped = Mode::ALL.len() - self.reported;
if skipped > 0 {
log!("gate: {skipped} neural tier(s) skipped — no RIFE checkpoint at {}",
default_model_path().display());
}
log!(
"gate: {} of {} tiers behaved",
self.reported - self.failures,
self.reported
);
if self.failures == 0 {
log!("gate: PASS");
} else {
log!("gate: FAIL — {} tier(s) did not behave", self.failures);
}
cx.quit();
return;
};
match self.stage {
Stage::Feed => {
self.feed(cx, mode);
self.stage = if mode.uses_ai() {
Stage::Neural(0)
} else {
Stage::Settle(0)
};
}
Stage::Neural(waited) => {
if self.neural(cx, mode) {
self.stage = Stage::Settle(0);
} else if waited >= NEURAL_FRAMES {
log!("FAIL {:<10} the neural producer never answered", mode.label());
self.failures += 1;
self.reported += 1;
self.which += 1;
self.offered = false;
self.stage = Stage::Feed;
} else {
self.stage = Stage::Neural(waited + 1);
}
}
Stage::Settle(waited) => {
self.view(cx, |cx, view| view.redraw(cx));
self.stage = if waited >= SETTLE_FRAMES {
Stage::Read
} else {
Stage::Settle(waited + 1)
};
}
Stage::Read => {
self.read(cx, mode);
self.which += 1;
self.offered = false;
self.stage = Stage::Feed;
}
}
self.pump = cx.new_next_frame();
}
}
impl MatchEvent for App {
fn handle_startup(&mut self, cx: &mut Cx) {
log!("gate: {} x {}, marker moves {} px between the endpoints", GATE_W, GATE_H,
BLOCK_B_X - makepad_frametween::selftest::BLOCK_A_X);
self.pump = cx.new_next_frame();
}
}
impl AppMain for App {
fn script_mod(vm: &mut ScriptVm) -> ScriptValue {
crate::makepad_widgets::script_mod(vm);
makepad_frametween::script_mod(vm);
self::script_mod(vm)
}
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
if self.pump.is_event(event).is_some() {
self.step(cx);
}
}
}
/// A minimal PNG writer — the gate's dumps are a debugging convenience and
/// do not deserve a dependency.
fn encode_png_rgba(rgba: &[u8], w: u32, h: u32) -> Result<Vec<u8>, ()> {
fn crc32(bytes: &[u8]) -> u32 {
let mut c = 0xffff_ffffu32;
for &b in bytes {
c ^= b as u32;
for _ in 0..8 {
c = if c & 1 != 0 { 0xedb8_8320 ^ (c >> 1) } else { c >> 1 };
}
}
!c
}
fn chunk(out: &mut Vec<u8>, tag: &[u8; 4], body: &[u8]) {
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
let mut with_tag = tag.to_vec();
with_tag.extend_from_slice(body);
out.extend_from_slice(&with_tag);
out.extend_from_slice(&crc32(&with_tag).to_be_bytes());
}
if rgba.len() < (w * h * 4) as usize {
return Err(());
}
// Stored (uncompressed) deflate blocks + the adler32 the format wants.
let mut raw = Vec::with_capacity((w * h * 4 + h) as usize);
for y in 0..h {
raw.push(0u8);
let at = (y * w * 4) as usize;
raw.extend_from_slice(&rgba[at..at + (w * 4) as usize]);
}
let (mut a, mut b) = (1u32, 0u32);
for &byte in &raw {
a = (a + byte as u32) % 65521;
b = (b + a) % 65521;
}
let mut z = vec![0x78, 0x01];
for (i, block) in raw.chunks(65535).enumerate() {
let last = if (i + 1) * 65535 >= raw.len() { 1u8 } else { 0 };
z.push(last);
z.extend_from_slice(&(block.len() as u16).to_le_bytes());
z.extend_from_slice(&(!(block.len() as u16)).to_le_bytes());
z.extend_from_slice(block);
}
z.extend_from_slice(&((b << 16) | a).to_be_bytes());
let mut out = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&w.to_be_bytes());
ihdr.extend_from_slice(&h.to_be_bytes());
ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);
chunk(&mut out, b"IHDR", &ihdr);
chunk(&mut out, b"IDAT", &z);
chunk(&mut out, b"IEND", &[]);
Ok(out)
}