mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 07:38:24 +00:00
#1830: require exporter support before skipping duplicate frames
This commit is contained in:
1 parent
37561799f6
commit
c5671f6569
7 files changed
+39
-4
No files matched your search
@@ -494,7 +494,8 @@ impl Session {
|
||||
} else {
|
||||
Target::file(Path::new(path), by_identity)
|
||||
};
|
||||
Write { item: m.id, path: path.to_string(), target, skips: m.settings.skip_existing }
|
||||
let skips = m.settings.skip_existing && self.exporter.as_ref().is_some_and(|exporter| exporter.supports_skip_existing());
|
||||
Write { item: m.id, path: path.to_string(), target, skips }
|
||||
}
|
||||
|
||||
/// The report of `later` writing over `clash`.
|
||||
|
||||
@@ -57,6 +57,11 @@ pub struct ExportResult {
|
||||
pub trait Exporter: Send + Sync {
|
||||
/// Formats this build can write.
|
||||
fn formats(&self) -> Vec<OutputFormat>;
|
||||
/// Whether Skip Existing Files preserves sequence frames written by earlier items. Exporters
|
||||
/// without an existence check (such as memory sinks) must keep the conservative default.
|
||||
fn supports_skip_existing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Render and write one item (blocking). `progress(done, total)` returns `false` to cancel;
|
||||
/// a cancelled export removes the files it opened for that output and returns `Err("cancelled")`.
|
||||
/// Completed earlier queue items and unrelated files must be preserved.
|
||||
|
||||
@@ -28,6 +28,9 @@ impl Exporter for Writer {
|
||||
fn formats(&self) -> Vec<OutputFormat> {
|
||||
OutputFormat::ALL.to_vec()
|
||||
}
|
||||
fn supports_skip_existing(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn export(&self, job: &ExportJob, _: &mut dyn FnMut(u64, u64) -> bool) -> Result<ExportResult, String> {
|
||||
self.jobs.lock().unwrap().push(job.path.to_string());
|
||||
let comp = job.project.comp(job.item.comp).ok_or("no comp")?;
|
||||
|
||||
@@ -33,6 +33,9 @@ impl Exporter for FileExporter {
|
||||
fn formats(&self) -> Vec<OutputFormat> {
|
||||
effectcraft_export::available_formats()
|
||||
}
|
||||
fn supports_skip_existing(&self) -> bool {
|
||||
self.sink.is_none() && !cfg!(target_arch = "wasm32")
|
||||
}
|
||||
fn export(&self, job: &ExportJob, progress: &mut dyn FnMut(u64, u64) -> bool) -> Result<ExportResult, String> {
|
||||
effectcraft_engine::render::passes::block_on(self.export_async(job, progress))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn memory_sink_cannot_overwrite_a_previous_output_even_with_skip_existing() {
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
let writes = Arc::new(AtomicUsize::new(0));
|
||||
let count = writes.clone();
|
||||
let sink: Arc<effectcraft_export::Sink> = Arc::new(move |_, _| {
|
||||
count.fetch_add(1, Ordering::Relaxed);
|
||||
});
|
||||
let mut s = effectcraft_engine::Session { exporter: Some(Arc::new(super::FileExporter { sink: Some(sink) })), ..Default::default() };
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 16, "height": 16, "frameRate": 10, "duration": 0.2})).unwrap();
|
||||
for _ in 0..2 {
|
||||
s.execute("renderQueue.add", json!({"format": "png", "output": "/virtual/frames_[#####].png", "skipExisting": true})).unwrap();
|
||||
}
|
||||
let result = s.execute("renderQueue.render", json!({}));
|
||||
assert!(result.is_err(), "a sink cannot skip previous paths: {result:?}");
|
||||
assert_eq!(writes.load(Ordering::Relaxed), 0, "refused before the sink receives bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_runs_tasks_scheduled_by_panel_handlers() {
|
||||
let mut s = super::session();
|
||||
|
||||
+2
-1
@@ -322,7 +322,8 @@ build real projects through these interfaces; they are worked examples of everyt
|
||||
**A render never writes one file twice either:** when two outputs of the job (two items, a
|
||||
`renderQueue.duplicate` copy, output modules, segments) name the same file, the whole render is
|
||||
refused before anything is written, naming both items. Sequences clash only on frames both
|
||||
write, and a later item with Skip Existing Files leaves the earlier one's frames alone. Default
|
||||
write. A later item with Skip Existing Files leaves the earlier one's frames alone only when
|
||||
the exporter can check existing files; browser and other memory sinks still report collisions. Default
|
||||
names are numbered like footage collisions when another queue item writes them
|
||||
(`renderQueue.add`, `render.preRender`, `comp.saveFrameAs {queue}`, `file.createProxy`: a second
|
||||
`Main` renders to `Main_1.mp4`); a name you give is kept, and `renderQueue.duplicate` copies the
|
||||
|
||||
+3
-2
@@ -58,8 +58,9 @@ receive `resultType: complete` and list/read cache hints; document reads are nev
|
||||
Two outputs of one job that write the same file (two items with one Output To, a duplicated item,
|
||||
output modules or segments that meet) refuse the whole `renderQueue.render` before anything is
|
||||
written: the reply is `isError: true`, naming both items, and the queue is left as it was.
|
||||
Sequences clash only on the frames both write, and an item with Skip Existing Files leaves the
|
||||
earlier one's frames alone. A new item's default output name is numbered (`Main.mp4`,
|
||||
Sequences clash only on the frames both write. Skip Existing Files leaves earlier frames alone
|
||||
only when the exporter can check existing files; browser and other memory sinks still report
|
||||
collisions. A new item's default output name is numbered (`Main.mp4`,
|
||||
`Main_1.mp4`) when another item of the queue writes it.
|
||||
|
||||
## Progress and cancellation
|
||||
|
||||
Reference in new issue
Block a user