render: anchor resolution_in to the sequence resolution
Preview renders at proxy size while a paused frame renders full-res, so anchoring resolution_in to the render target made every sequence-pixel effect (shape size/pos, transform offsets, corner pin points, drop shadow distance) change apparent size whenever the transport stopped. Pre-fill resolution_in from the sequence's video params (C++ inserts the NodeGlobals square resolution at job-build time), covering the nested generator job inside a merge as well; a node that inserted its own resolution_in keeps it.
This commit is contained in:
@@ -164,6 +164,14 @@ pub struct RenderEvalHooks {
|
||||
/// texture at the output resolution). The graph-sequence driver sets
|
||||
/// this to the sequence frame size.
|
||||
pub frame_size: Option<(i32, i32)>,
|
||||
/// The sequence's square-pixel resolution (C++ the `NodeGlobals`
|
||||
/// square resolution the shape/polygon generators insert as
|
||||
/// `resolution_in` at job-build time). Generator jobs anchor their
|
||||
/// pixel-size params to this — NOT to the render-target size — so a
|
||||
/// proxy-resolution playback render draws them at the same relative
|
||||
/// size as a paused full-res frame. `None` (non-sequence renders)
|
||||
/// falls back to the render-target size.
|
||||
pub sequence_size: Option<(i32, i32)>,
|
||||
|
||||
}
|
||||
|
||||
@@ -274,6 +282,7 @@ impl RenderEvalHooks {
|
||||
use_cache: false,
|
||||
ticket: None,
|
||||
frame_size: None,
|
||||
sequence_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,6 +803,36 @@ impl RenderEvalHooks {
|
||||
// nobody knows better.
|
||||
let size = size.or(self.frame_size).unwrap_or((1, 1));
|
||||
|
||||
// `resolution_in` anchors to the sequence square resolution, not
|
||||
// the render target (C++ inserts the NodeGlobals square
|
||||
// resolution into the job at build time): the node params it
|
||||
// denormalizes (shape size/pos, transform offsets, corner pin
|
||||
// points, drop shadow distance) are all sequence-pixel values,
|
||||
// so a proxy-size playback render must resolve them against the
|
||||
// same resolution as a paused full-res frame, or the effect
|
||||
// visibly changes size whenever the transport stops. Pre-filling
|
||||
// the row wins over `run_effect`'s frame-size auto-fill; a node
|
||||
// that inserted its own `resolution_in` keeps it.
|
||||
let mut sequence_row;
|
||||
let params = if self.sequence_size.is_some()
|
||||
&& !payload.params.contains_key("resolution_in")
|
||||
&& compiled
|
||||
.translated
|
||||
.uniforms
|
||||
.iter()
|
||||
.any(|u| u.name == "resolution_in")
|
||||
{
|
||||
let (w, h) = self.sequence_size.unwrap();
|
||||
sequence_row = payload.params.clone();
|
||||
sequence_row.insert(
|
||||
"resolution_in".to_string(),
|
||||
NodeValue::Vec2([w as f64, h as f64]),
|
||||
);
|
||||
&sequence_row
|
||||
} else {
|
||||
&payload.params
|
||||
};
|
||||
|
||||
let dst = match ctx.create_texture(size.0.max(1), size.1.max(1)) {
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
@@ -808,7 +847,7 @@ impl RenderEvalHooks {
|
||||
let result = run_effect(
|
||||
&ctx,
|
||||
&compiled,
|
||||
&payload.params,
|
||||
params,
|
||||
&inputs,
|
||||
dst,
|
||||
size,
|
||||
@@ -1441,6 +1480,13 @@ pub fn render_graph_frame(
|
||||
let mut traverser = oak_node::traverser::Traverser::new();
|
||||
let mut hooks = RenderEvalHooks::new();
|
||||
hooks.frame_size = Some(size);
|
||||
// The generators' `resolution_in` anchor (C++ NodeGlobals square
|
||||
// resolution): the sequence's native size, so proxy-size playback and
|
||||
// full-res paused frames draw generated layers identically.
|
||||
hooks.sequence_size = sequence
|
||||
.video_params
|
||||
.first()
|
||||
.map(|p| (p.width.max(1), p.height.max(1)));
|
||||
let perf = std::env::var_os("OAK_PERF").is_some();
|
||||
let mut perf_collect = perf.then(std::time::Instant::now);
|
||||
let mut perf_collect_ms = 0.0f64;
|
||||
|
||||
@@ -639,3 +639,88 @@ fn chromakey_job_keys_green_with_ociobased_stub() {
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
/// Generator sizing is anchored to the SEQUENCE resolution (C++ inserts
|
||||
/// the NodeGlobals square resolution as `resolution_in` at job-build
|
||||
/// time), not the render target: a proxy-size playback render and a
|
||||
/// full-res paused frame must draw the shape at the same relative size,
|
||||
/// or the generated layer visibly changes size whenever the transport
|
||||
/// stops. A 96px rectangle on the default 1920x1080 sequence spans 5%
|
||||
/// of the width at BOTH 192x108 and 768x432 (the pre-fix
|
||||
/// target-anchored behavior drew 50% vs 12.5%).
|
||||
#[test]
|
||||
fn shape_generator_size_is_sequence_relative() {
|
||||
if oak_core::backend::GpuContext::shared().is_none() {
|
||||
eprintln!("skipping shape_generator_size_is_sequence_relative: no GPU adapter");
|
||||
return;
|
||||
}
|
||||
let path = clip_path("shape_seqrel");
|
||||
oak_codec::testmedia::write_test_clip_solid(&path, 64, 64, 10, 10, [0.0, 0.0, 1.0, 1.0])
|
||||
.expect("blue clip generation");
|
||||
|
||||
let (project, seq) = build_effect_project(
|
||||
(&path.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1)),
|
||||
|p, footage, clip| {
|
||||
let (ecore, ebehavior) = oak_node::factory::Factory::global()
|
||||
.create_any("org.olivevideoeditor.Olive.shape")
|
||||
.expect("shape factory entry");
|
||||
let effect = p.graph.add_node(ecore, ebehavior);
|
||||
p.graph.disconnect(footage, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1);
|
||||
// The shape module is crate-private; the input ids are the C++
|
||||
// kBaseInput/kSizeInput strings.
|
||||
p.graph
|
||||
.connect(footage, effect, "base_in", -1)
|
||||
.expect("connect footage to shape base");
|
||||
p.graph
|
||||
.connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1)
|
||||
.expect("connect shape to clip");
|
||||
p.graph.get_mut(effect).unwrap().core.set_standard_value(
|
||||
"size_in",
|
||||
-1,
|
||||
oak_node::value::NodeValue::Vec2([96.0, 96.0]),
|
||||
);
|
||||
effect
|
||||
},
|
||||
);
|
||||
|
||||
// The fraction of the middle row covered by the opaque red square.
|
||||
let red_fraction = |width: i32, height: i32| -> f32 {
|
||||
let tex = oak_render::eval::render_graph_frame(
|
||||
&project,
|
||||
seq,
|
||||
Rational::new(0, 1),
|
||||
(width, height),
|
||||
PixelFormat::F32,
|
||||
)
|
||||
.expect("shape render");
|
||||
let Texture::Cpu(frame) = &tex else {
|
||||
panic!("graph render produced a non-CPU texture");
|
||||
};
|
||||
let stride = frame.linesize_bytes() as usize;
|
||||
let y = (height / 2) as usize;
|
||||
let mut red = 0usize;
|
||||
for x in 0..width as usize {
|
||||
let off = y * stride + x * 16;
|
||||
let r = f32::from_le_bytes(frame.data[off..off + 4].try_into().unwrap());
|
||||
let g = f32::from_le_bytes(frame.data[off + 4..off + 8].try_into().unwrap());
|
||||
let a = f32::from_le_bytes(frame.data[off + 12..off + 16].try_into().unwrap());
|
||||
if r > 0.9 && g < 0.1 && a > 0.9 {
|
||||
red += 1;
|
||||
}
|
||||
}
|
||||
red as f32 / width as f32
|
||||
};
|
||||
|
||||
let small = red_fraction(192, 108);
|
||||
let large = red_fraction(768, 432);
|
||||
assert!(
|
||||
(small - large).abs() < 0.02,
|
||||
"the shape's relative size must not depend on the render target: {small} at 192px vs {large} at 768px"
|
||||
);
|
||||
assert!(
|
||||
(small - 0.05).abs() < 0.02,
|
||||
"96px on the 1920px sequence is 5%%, got {small}"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user