optimize: video and audio render
This commit is contained in:
@@ -1198,7 +1198,18 @@ fn audio_layout(params: &crate::ticket::AudioTicketParams) -> Result<(i32, u64,
|
||||
if seconds <= 0.0 || seconds > 3600.0 {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let total_frames = (seconds * rate as f64).round() as usize;
|
||||
|
||||
// Anchor each chunk to the absolute sample grid (`round(out·rate) -
|
||||
// round(in·rate)`) instead of rounding the duration: at fractional
|
||||
// frame rates (29.97 fps → 1601.6 samples/frame) a duration-round
|
||||
// would emit 1602 samples for every chunk and accumulate ~12 extra
|
||||
// samples per second, slowly desyncing audio from video. Per-chunk
|
||||
// anchoring keeps the total exact and matches the decode side
|
||||
// (`FFmpegDecoder::retrieve_audio_to` fills `round(out·rate) -
|
||||
// round(in·rate)` samples).
|
||||
let total_frames = ((params.range.out().to_f64() * rate as f64).round()
|
||||
- (params.range.in_().to_f64() * rate as f64).round())
|
||||
.max(0.0) as usize;
|
||||
Ok((rate, params.channel_layout, channels, total_frames))
|
||||
}
|
||||
|
||||
@@ -1230,8 +1241,11 @@ fn mix_audio_montage(
|
||||
if out_time <= in_time {
|
||||
continue;
|
||||
}
|
||||
let start_frame = ((in_time - params.range.in_()).to_f64() * rate as f64) as usize;
|
||||
let end_frame = ((out_time - params.range.in_()).to_f64() * rate as f64) as usize;
|
||||
// Round to the absolute sample grid like the decode side (which
|
||||
// anchors at `round(media_start·rate)`): truncation would shift a
|
||||
// clip's mix by up to one sample per chunk.
|
||||
let start_frame = ((in_time - params.range.in_()).to_f64() * rate as f64).round() as usize;
|
||||
let end_frame = ((out_time - params.range.in_()).to_f64() * rate as f64).round() as usize;
|
||||
if start_frame >= total_frames {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -232,6 +232,33 @@ impl RenderManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// M16 S1 graph mode: force the workers to re-load the current project
|
||||
/// snapshot even when the undo-stack revision is unchanged. The
|
||||
/// color-settings dialog writes project settings directly (no undo
|
||||
/// command), so the revision-based dedup in [`set_graph_snapshot`] would
|
||||
/// never re-send the snapshot — workers would keep rendering under the
|
||||
/// colors they loaded at project-load time. The snapshot file is
|
||||
/// rewritten (see [`GraphSnapshotStore::acquire_rewrite`]) and
|
||||
/// `load_graph` re-broadcast to every live worker (the dispatcher's
|
||||
/// re-send has no dedup).
|
||||
pub fn resync_graph_snapshot(
|
||||
&self,
|
||||
project: &std::sync::Mutex<oak_node::project::Project>,
|
||||
revision: u64,
|
||||
) -> Result<()> {
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Ok(()); // teardown: no re-arm after the drain
|
||||
}
|
||||
let path = self.snapshots.acquire_rewrite(project, revision)?;
|
||||
if let Some(old) = lock(&self.current_snapshot).replace(path.clone()) {
|
||||
self.snapshots.release(&old);
|
||||
}
|
||||
self.dispatch.set_graph_snapshot(Some(path));
|
||||
let uuid = lock(project).uuid.clone();
|
||||
*lock(&self.current_key) = Some((uuid, revision));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// M16 S1 graph mode: drop the current snapshot (project closed). The
|
||||
/// protocol has no clear message, so alive workers keep their loaded
|
||||
/// graph; new/restarted workers no longer load it and the snapshot file
|
||||
|
||||
@@ -352,6 +352,51 @@ impl GraphSnapshotStore {
|
||||
Ok(path_str)
|
||||
}
|
||||
|
||||
/// Like [`acquire`], but always rewrites the snapshot file even when the
|
||||
/// (uuid, revision) key is already present. Used to force the workers to
|
||||
/// re-deserialize a project whose settings changed without an undo-stack
|
||||
/// revision bump (color-settings resync): the dedup in [`acquire`] would
|
||||
/// otherwise hand the workers the stale file and `load_graph` would never
|
||||
/// re-broadcast. Atomic staging and reference counting are identical to
|
||||
/// [`acquire`].
|
||||
pub fn acquire_rewrite(
|
||||
&self,
|
||||
project: &std::sync::Mutex<oak_node::project::Project>,
|
||||
revision: u64,
|
||||
) -> Result<String> {
|
||||
let (uuid, xml) = {
|
||||
let g = lock(project);
|
||||
let xml = oak_node::serializer::save(&g)
|
||||
.map_err(|e| Error::Failed(format!("save graph snapshot: {e}")))?;
|
||||
(g.uuid.clone(), xml)
|
||||
};
|
||||
let path = self.dir.join(format!("graph-{uuid}-{revision}.xml"));
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
// Atomic staging: temp file + rename (see [`acquire`]).
|
||||
let tmp = self
|
||||
.dir
|
||||
.join(format!("graph-{uuid}-{revision}.{}.tmp", std::process::id()));
|
||||
std::fs::write(&tmp, &xml)
|
||||
.map_err(|e| Error::Failed(format!("write snapshot temp: {e}")))?;
|
||||
if let Err(e) = std::fs::rename(&tmp, &path) {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
return Err(Error::Failed(format!("rename snapshot: {e}")));
|
||||
}
|
||||
let mut entries = lock(&self.entries);
|
||||
if let Some(entry) = entries.get_mut(&path_str) {
|
||||
entry.refs += 1;
|
||||
} else {
|
||||
entries.insert(
|
||||
path_str.clone(),
|
||||
SnapshotEntry {
|
||||
refs: 1,
|
||||
cached: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(path_str)
|
||||
}
|
||||
|
||||
/// Drop one reference. The FILE IS NOT UNLINKED: a worker may still
|
||||
/// hold the path for a late `load_graph` (M16 S1), and per-project
|
||||
/// filenames mean a stale snapshot is never confused with a live one.
|
||||
@@ -608,4 +653,42 @@ mod tests {
|
||||
"cleanup removes the snapshot directory"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquire_rewrite_forces_file_rewrite_on_same_key() {
|
||||
let store = GraphSnapshotStore::new();
|
||||
let project = oak_node::project::Project::new();
|
||||
// First snapshot: the default project (working space ACEScg).
|
||||
let p1 = store.acquire(&project, 1).unwrap();
|
||||
let before = std::fs::read_to_string(&p1).unwrap();
|
||||
assert!(
|
||||
std::path::Path::new(&p1).exists(),
|
||||
"snapshot file written"
|
||||
);
|
||||
assert_eq!(store.refs(&p1), 1);
|
||||
|
||||
// A settings change with no revision bump: plain acquire must NOT
|
||||
// rewrite the file (the dedup the resync exists to bypass)...
|
||||
{
|
||||
let mut guard = project.lock().unwrap_or_else(|e| e.into_inner());
|
||||
guard.set_working_color_space(oak_common::colormath::WorkingColorSpace::SrgbLegacy);
|
||||
}
|
||||
let p2 = store.acquire(&project, 1).unwrap();
|
||||
assert_eq!(p1, p2, "same (uuid, revision) key reuses the file");
|
||||
assert_eq!(std::fs::read_to_string(&p1).unwrap(), before, "acquire never rewrites");
|
||||
store.release(&p2);
|
||||
|
||||
// ...while acquire_rewrite rewrites it in place at the same key.
|
||||
let p3 = store.acquire_rewrite(&project, 1).unwrap();
|
||||
assert_eq!(p1, p3, "rewrite keeps the same path token");
|
||||
let after = std::fs::read_to_string(&p1).unwrap();
|
||||
assert_ne!(after, before, "rewrite replaces the snapshot content");
|
||||
assert!(
|
||||
after.contains("srgb_legacy"),
|
||||
"rewritten snapshot carries the new setting: {after}"
|
||||
);
|
||||
store.release(&p3);
|
||||
store.release(&p1);
|
||||
store.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user