mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 04:21:25 +00:00
Keep native control renders responsive (#3306)
* Keep native control renders responsive * Refresh replies for immediately completed render jobs * #3289: Bind waiting replies to their native render
This commit is contained in:
1 parent
07be19d402
commit
86c08aa0ee
20 files changed
+950
-29
No files matched your search
@@ -79,6 +79,7 @@ pub fn install(s: &mut Session) {
|
||||
s.script = Some(runner);
|
||||
s.script_ui.dispatch = Some(dispatch_ui);
|
||||
s.script_ui.tasks = Some(run_tasks);
|
||||
s.script_ui.poll_render = Some(ui::poll_render_wait);
|
||||
}
|
||||
|
||||
/// Run `code` as a one-off script named `name`.
|
||||
|
||||
@@ -30,6 +30,8 @@ pub struct Outcome {
|
||||
/// The script is waiting in a modal ScriptUI dialog's `show()` (it goes on when the dialog
|
||||
/// closes; see [`crate::dispatch_ui`]).
|
||||
pub waiting: bool,
|
||||
/// The native frontend owns the session while this host waits for its render worker.
|
||||
pub render_host: Option<u32>,
|
||||
}
|
||||
|
||||
/// An uncaught script error.
|
||||
@@ -55,6 +57,9 @@ impl Outcome {
|
||||
if self.waiting {
|
||||
j["waiting"] = json!(true);
|
||||
}
|
||||
if let Some(host) = self.render_host {
|
||||
j["renderHost"] = json!(host);
|
||||
}
|
||||
j
|
||||
}
|
||||
}
|
||||
@@ -74,13 +79,16 @@ pub(crate) struct Active {
|
||||
/// Running on a script-host thread: modal dialogs hand the session back to the caller
|
||||
/// through this link while they wait.
|
||||
pub(crate) link: Option<crate::ui::Link>,
|
||||
/// Captured at invocation start: the frontend resets its opt-in when the caller returns.
|
||||
pub(crate) yield_renders: bool,
|
||||
/// The files `$.evalFile` is evaluating, outermost first.
|
||||
pub(crate) loading: Vec<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl Active {
|
||||
pub(crate) fn new(session: Session, name: &str, host: u32) -> Active {
|
||||
Active { session, output: vec![], groups: vec![], name: name.to_string(), host, link: None, loading: vec![] }
|
||||
let yield_renders = session.script_ui.yield_renders;
|
||||
Active { session, output: vec![], groups: vec![], name: name.to_string(), host, link: None, yield_renders, loading: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +138,13 @@ fn native_exec(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsV
|
||||
return Err(throw("scripts can't run scripts"));
|
||||
}
|
||||
let keys = params.as_object_mut().and_then(|o| o.remove("__keys"));
|
||||
let yield_render = id == "renderQueue.render"
|
||||
&& params.get("wait").and_then(J::as_bool).unwrap_or(true)
|
||||
&& with_active(|a| Ok(a.yield_renders && a.link.is_some())).unwrap_or(false);
|
||||
if yield_render {
|
||||
let object = params.as_object_mut().ok_or_else(|| throw("renderQueue.render needs an object of parameters"))?;
|
||||
object.insert("wait".into(), json!(false));
|
||||
}
|
||||
let r = with_active(|a| {
|
||||
Ok(match &keys {
|
||||
Some(k) => exec_on_keys(&mut a.session, &id, params, k),
|
||||
@@ -137,7 +152,10 @@ fn native_exec(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsV
|
||||
})
|
||||
});
|
||||
match r {
|
||||
Ok(Ok(v)) => Ok(js_str(&v.to_string())),
|
||||
Ok(Ok(v)) => {
|
||||
let v = if yield_render { crate::ui::wait_for_render(ctx, v).map_err(throw)? } else { v };
|
||||
Ok(js_str(&v.to_string()))
|
||||
}
|
||||
Ok(Err(e)) | Err(e) => Err(throw(e)),
|
||||
}
|
||||
}
|
||||
@@ -718,6 +736,10 @@ pub fn run(session: &mut Session, req: &ScriptRequest) -> Outcome {
|
||||
};
|
||||
let Some(x) = expanded else { return run_expanded(session, req) };
|
||||
let mut out = run_expanded(session, &ScriptRequest { code: &x.code, name: req.name, console: req.console });
|
||||
if let Some(host) = out.render_host {
|
||||
crate::ui::keep_render_source_map(host, x);
|
||||
return out;
|
||||
}
|
||||
if let Some(e) = out.error.as_mut() {
|
||||
x.locate(e);
|
||||
}
|
||||
|
||||
@@ -208,7 +208,9 @@ pub(crate) fn run_tasks_at(session: &mut Session, now: Instant) -> J {
|
||||
if !running {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
crate::ui::tick_threaded(session, now, &mut report);
|
||||
crate::ui::tick_inline(session, now, &mut report);
|
||||
if session.script_ui.render_host.is_none() {
|
||||
crate::ui::tick_inline(session, now, &mut report);
|
||||
}
|
||||
crate::ui::prune_task_hosts(session);
|
||||
}
|
||||
let errors: Vec<J> = report.errors.iter().map(|e| json!({"message": e.message, "line": e.line, "column": e.column, "file": e.file})).collect();
|
||||
|
||||
@@ -15,6 +15,8 @@ mod ease_presets;
|
||||
mod evalfile;
|
||||
mod include;
|
||||
mod panels;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod render_wait;
|
||||
#[cfg(not(windows))]
|
||||
mod services;
|
||||
mod startup;
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
//! #3289: a native frontend may service the event loop while a script waits for rendering.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use effectcraft_engine::project::render_queue::OutputFormat;
|
||||
use effectcraft_engine::{ExportJob, ExportResult, Exporter, Session};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// A slow exporter without expensive frames. A bounded wait makes the old blocking path
|
||||
/// fail the assertions instead of hanging the test process.
|
||||
struct Gate {
|
||||
entered: AtomicUsize,
|
||||
permits: AtomicUsize,
|
||||
failed: bool,
|
||||
}
|
||||
|
||||
impl Exporter for Gate {
|
||||
fn formats(&self) -> Vec<OutputFormat> {
|
||||
vec![OutputFormat::H264]
|
||||
}
|
||||
fn export(&self, job: &ExportJob, progress: &mut dyn FnMut(u64, u64) -> bool) -> Result<ExportResult, String> {
|
||||
let call = self.entered.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let end = Instant::now() + Duration::from_secs(2);
|
||||
while self.permits.load(Ordering::SeqCst) < call {
|
||||
if !progress(0, 1) {
|
||||
return Err(effectcraft_engine::render_queue::CANCELLED.into());
|
||||
}
|
||||
if Instant::now() > end {
|
||||
return Err("test exporter timed out waiting for the frontend".into());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
if self.failed { Err("test encoder failure".into()) } else { Ok(ExportResult { path: job.path.into(), frames: 1, ..Default::default() }) }
|
||||
}
|
||||
}
|
||||
|
||||
fn session(permits: usize, failed: bool) -> (Session, Arc<Gate>) {
|
||||
let gate = Arc::new(Gate { entered: AtomicUsize::new(0), permits: AtomicUsize::new(permits), failed });
|
||||
let mut s = super::session();
|
||||
s.exporter = Some(gate.clone());
|
||||
s.execute("comp.new", json!({"name": "Render", "width": 16, "height": 16, "duration": 1})).unwrap();
|
||||
let path = std::env::temp_dir().join(format!("ec-script-render-{}.mp4", std::process::id()));
|
||||
s.execute("renderQueue.add", json!({"output": path.to_string_lossy()})).unwrap();
|
||||
s.prefs.auto_save.enabled = false;
|
||||
(s, gate)
|
||||
}
|
||||
|
||||
fn finish(s: &mut Session) -> Value {
|
||||
let end = Instant::now() + Duration::from_secs(3);
|
||||
loop {
|
||||
if let Some(result) = crate::ui::poll_render_wait(s) {
|
||||
return result;
|
||||
}
|
||||
assert!(Instant::now() < end, "script did not resume");
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_yields_without_finishing_the_script_or_its_undo_group() {
|
||||
let (mut s, gate) = session(0, false);
|
||||
s.script_ui.yield_renders = true;
|
||||
let before = s.history.undo.len();
|
||||
let out = crate::run_code(
|
||||
&mut s,
|
||||
"app.beginUndoGroup('Render transaction'); app.project.activeItem.layers.addNull().name = 'Before'; $.writeln('before'); app.project.renderQueue.render(); app.project.activeItem.layers.addNull().name = 'After'; $.writeln('after'); app.endUndoGroup(); [42, app.project.renderQueue.rendering];",
|
||||
"render.jsx",
|
||||
);
|
||||
assert!(out.error.is_none(), "{out:?}");
|
||||
assert!(out.render_host.is_some(), "blocking render returned instead of yielding: {out:?}");
|
||||
assert!(!out.waiting, "a render suspension is not a modal dialog");
|
||||
assert!(out.output.is_empty(), "output belongs to the eventual reply");
|
||||
assert_eq!(s.active_comp().unwrap().layers.len(), 1, "the continuation has not run");
|
||||
assert!(crate::ui::poll_render_wait(&mut s).is_none());
|
||||
assert!(!s.undo(), "undo cannot discard the suspended script's live edits");
|
||||
assert!(s.execute("engine.batch", json!({"steps": [{"command": "layer.newNull"}]})).is_err());
|
||||
assert!(s.execute("jobs.list", json!({})).is_err(), "status polling must not apply unrelated jobs");
|
||||
// The frontend resets the invocation-only opt-in after script.run returns.
|
||||
s.script_ui.yield_renders = false;
|
||||
gate.permits.store(1, Ordering::SeqCst);
|
||||
let done = finish(&mut s);
|
||||
assert_eq!(done["ok"], true, "{done}");
|
||||
assert_eq!(done["result"], json!([42, false]));
|
||||
assert_eq!(done["output"], "before\nafter");
|
||||
assert_eq!(s.project.render_queue[0].status.label(), "Done");
|
||||
assert_eq!(s.history.undo.len(), before + 1, "one undo group spans the render");
|
||||
assert_eq!(s.history.undo.last().unwrap().0, "Render transaction");
|
||||
assert!(s.script_ui.render_host.is_none());
|
||||
s.undo();
|
||||
assert!(s.active_comp().unwrap().layers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_render_keeps_the_same_script_pending() {
|
||||
let (mut s, gate) = session(0, false);
|
||||
s.script_ui.yield_renders = true;
|
||||
let out = crate::run_code(
|
||||
&mut s,
|
||||
"app.project.renderQueue.render(); app.project.renderQueue.item(1).render = true; app.project.renderQueue.render(); 17;",
|
||||
"twice.jsx",
|
||||
);
|
||||
let host = out.render_host.unwrap();
|
||||
s.script_ui.yield_renders = false;
|
||||
gate.permits.store(1, Ordering::SeqCst);
|
||||
let end = Instant::now() + Duration::from_secs(3);
|
||||
while gate.entered.load(Ordering::SeqCst) < 2 {
|
||||
assert!(crate::ui::poll_render_wait(&mut s).is_none(), "no premature reply after the first render");
|
||||
assert!(Instant::now() < end);
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert_eq!(s.script_ui.render_host, Some(host));
|
||||
gate.permits.store(2, Ordering::SeqCst);
|
||||
assert_eq!(finish(&mut s)["result"], 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_and_encoder_failure_resume_with_terminal_statuses() {
|
||||
for failed in [false, true] {
|
||||
let (mut s, gate) = session(0, failed);
|
||||
s.script_ui.yield_renders = true;
|
||||
let out = crate::run_code(&mut s, "app.project.renderQueue.render(); [9, app.project.renderQueue.rendering];", "stopped.jsx");
|
||||
assert!(out.render_host.is_some());
|
||||
if failed {
|
||||
gate.permits.store(1, Ordering::SeqCst);
|
||||
} else {
|
||||
s.stop_render();
|
||||
}
|
||||
let done = finish(&mut s);
|
||||
assert_eq!(done["ok"], true, "the synchronous command reports status, not a thrown script error: {done}");
|
||||
assert_eq!(done["result"], json!([9, false]));
|
||||
assert_ne!(s.project.render_queue[0].status.label(), "Done");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_scripts_still_wait_and_start_errors_do_not_suspend() {
|
||||
let (mut s, _) = session(1, false);
|
||||
let out = crate::run_code(&mut s, "app.project.renderQueue.render(); [7, app.project.renderQueue.rendering];", "headless.jsx");
|
||||
assert!(out.error.is_none(), "{out:?}");
|
||||
assert_eq!(out.result, json!([7, false]));
|
||||
assert!(out.render_host.is_none());
|
||||
assert_eq!(s.project.render_queue[0].status.label(), "Done");
|
||||
s.script_ui.yield_renders = true;
|
||||
let out = crate::run_code(&mut s, "app.project.renderQueue.render();", "empty.jsx");
|
||||
assert!(out.error.is_some());
|
||||
assert!(out.render_host.is_none());
|
||||
assert!(s.script_ui.render_host.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_palette_callbacks_can_yield_after_the_original_control_invocation() {
|
||||
let (mut s, gate) = session(0, false);
|
||||
s.script_ui.yield_renders = true;
|
||||
let out = crate::run_code(
|
||||
&mut s,
|
||||
"var w = new Window('palette', 'Render'); var b = w.add('button', undefined, 'Go', {name:'go'}); b.onClick = function () { app.project.renderQueue.render(); $.writeln('resumed'); }; w.show();",
|
||||
"palette-render.jsx",
|
||||
);
|
||||
assert!(out.error.is_none(), "{out:?}");
|
||||
s.script_ui.yield_renders = false;
|
||||
let result = s.execute("scriptui.click", json!({"widget": "go"})).unwrap();
|
||||
assert!(result["renderHost"].is_number(), "{result}");
|
||||
gate.permits.store(1, Ordering::SeqCst);
|
||||
assert_eq!(finish(&mut s)["output"], "resumed");
|
||||
let window = s.script_ui.windows[0].id;
|
||||
s.execute("scriptui.close", json!({"window": window})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_render_actions_complete_before_the_script_resumes() {
|
||||
use effectcraft_engine::project::{Footage, FootageKind};
|
||||
struct Probe;
|
||||
impl effectcraft_engine::Importer for Probe {
|
||||
fn probe(&self, path: &str) -> Result<Footage, String> {
|
||||
Ok(Footage { path: path.into(), kind: FootageKind::Still, width: 16, height: 16, has_video: true, ..Default::default() })
|
||||
}
|
||||
}
|
||||
for action in ["import", "setProxy"] {
|
||||
let (mut s, gate) = session(0, false);
|
||||
s.importer = Some(Arc::new(Probe));
|
||||
let id = s.project.render_queue[0].id;
|
||||
s.execute("renderQueue.setOutputModule", json!({"item": id, "postRenderAction": action})).unwrap();
|
||||
let before = s.project.items.len();
|
||||
let comp = s.active_comp_id().unwrap();
|
||||
s.script_ui.yield_renders = true;
|
||||
let out = crate::run_code(&mut s, "app.project.renderQueue.render(); 29;", "post-render.jsx");
|
||||
assert!(out.render_host.is_some(), "{out:?}");
|
||||
gate.permits.store(1, Ordering::SeqCst);
|
||||
// A query must not consume updates while the edit guard is still active.
|
||||
let end = Instant::now() + Duration::from_secs(3);
|
||||
while s.is_rendering() {
|
||||
assert!(Instant::now() < end);
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
s.execute("renderQueue.list", json!({})).unwrap();
|
||||
assert_eq!(s.project.items.len(), before);
|
||||
assert_eq!(finish(&mut s)["result"], 29);
|
||||
if action == "import" {
|
||||
assert_eq!(s.project.items.len(), before + 1);
|
||||
} else {
|
||||
assert!(s.project.item(comp).unwrap().proxy.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_errors_keep_their_included_source_location() {
|
||||
let (mut s, gate) = session(0, false);
|
||||
let dir = std::env::temp_dir().join(format!("ec-render-include-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("render.jsx"), "app.project.renderQueue.render();\nmissingAfterRender();").unwrap();
|
||||
std::fs::write(dir.join("main.jsx"), "#include 'render.jsx'").unwrap();
|
||||
s.script_ui.yield_renders = true;
|
||||
let out = crate::run_code(&mut s, "#include 'render.jsx'", &dir.join("main.jsx").to_string_lossy());
|
||||
assert!(out.render_host.is_some(), "{out:?}");
|
||||
gate.permits.store(1, Ordering::SeqCst);
|
||||
let done = finish(&mut s);
|
||||
assert_eq!(done["ok"], false);
|
||||
assert_eq!(done["error"]["line"], 2);
|
||||
assert!(done["error"]["file"].as_str().unwrap().ends_with("render.jsx"), "{done}");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn included_error_mapping_survives_a_modal_yield_after_rendering() {
|
||||
let (mut s, gate) = session(0, false);
|
||||
let dir = std::env::temp_dir().join(format!("ec-render-modal-include-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("render.jsx"), "app.project.renderQueue.render();\nvar d = new Window('dialog', 'Continue'); d.add('button', undefined, 'OK', {name:'ok'}); d.show();\nmissingAfterModal();").unwrap();
|
||||
std::fs::write(dir.join("main.jsx"), "#include 'render.jsx'").unwrap();
|
||||
s.script_ui.yield_renders = true;
|
||||
let out = crate::run_code(&mut s, "#include 'render.jsx'", &dir.join("main.jsx").to_string_lossy());
|
||||
assert!(out.render_host.is_some(), "{out:?}");
|
||||
gate.permits.store(1, Ordering::SeqCst);
|
||||
assert_eq!(finish(&mut s)["waiting"], true);
|
||||
let done = s.execute("scriptui.click", json!({"widget": "ok"})).unwrap();
|
||||
assert_eq!(done["ok"], false);
|
||||
assert_eq!(done["error"]["line"], 3);
|
||||
assert!(done["error"]["file"].as_str().unwrap().ends_with("render.jsx"), "{done}");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
+91
-8
@@ -3,7 +3,8 @@
|
||||
//!
|
||||
//! * **Script files** (native builds) run on a *script-host thread*. The caller's session moves
|
||||
//! to that thread while JavaScript runs and comes back whenever the script stops: at the end,
|
||||
//! or while a modal dialog waits in `show()`. Each [`ScriptUiEvent`] moves the session there
|
||||
//! while a modal dialog waits in `show()`, or while an opted-in frontend waits for rendering.
|
||||
//! Each [`ScriptUiEvent`] moves the session there
|
||||
//! again, runs the handler, and brings it back with the windows' new state. The host ends
|
||||
//! when its last window closes. Only one side holds the session at a time, so nothing runs
|
||||
//! concurrently and every edit is still an ordinary undoable engine command.
|
||||
@@ -137,6 +138,8 @@ pub(crate) enum ToHost {
|
||||
/// script-host threads).
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
Tick(Session, Instant),
|
||||
/// Resume the suspended native render call, never a callback or scheduled task.
|
||||
ResumeRender(Session),
|
||||
}
|
||||
|
||||
impl ToHost {
|
||||
@@ -145,7 +148,7 @@ impl ToHost {
|
||||
match self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ToHost::Start(s) => s,
|
||||
ToHost::Event(s, _) | ToHost::Tick(s, _) => s,
|
||||
ToHost::Event(s, _) | ToHost::Tick(s, _) | ToHost::ResumeRender(s) => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +162,7 @@ pub(crate) struct FromHost {
|
||||
done: bool,
|
||||
/// When its next scheduled task is due.
|
||||
next_due: Option<Instant>,
|
||||
render_wait: bool,
|
||||
}
|
||||
|
||||
/// The host thread's end of the hand-off.
|
||||
@@ -177,6 +181,8 @@ struct HostEnd {
|
||||
/// [`effectcraft_engine::scriptui::ScriptUi::project_epoch`] when the host last ran.
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
epoch: u64,
|
||||
/// Retained across render yields so a later error still names its included source file.
|
||||
source_map: Option<crate::include::Expanded>,
|
||||
}
|
||||
|
||||
fn hosts() -> &'static std::sync::Mutex<BTreeMap<u32, HostEnd>> {
|
||||
@@ -195,6 +201,7 @@ enum Wake {
|
||||
Event(ScriptUiEvent),
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
Tick(Instant),
|
||||
Render,
|
||||
}
|
||||
|
||||
/// On the host thread: hand the session (with the windows' state and what the script printed)
|
||||
@@ -206,18 +213,20 @@ fn yield_wait(outcome: crate::Outcome, windows: Vec<ScriptWindow>, done: bool) -
|
||||
match yield_wake(out, windows.clone(), done, None)? {
|
||||
Wake::Event(ev) => return Some(ev),
|
||||
Wake::Tick(_) => out = crate::Outcome { waiting: true, ..Default::default() },
|
||||
Wake::Render => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`yield_wait`] that also wakes for the scheduled tasks due at `next_due`.
|
||||
fn yield_wake(outcome: crate::Outcome, windows: Vec<ScriptWindow>, done: bool, next_due: Option<Instant>) -> Option<Wake> {
|
||||
let render_wait = outcome.render_host.is_some();
|
||||
let (session, link, name) = ACTIVE.with(|a| {
|
||||
let mut a = a.borrow_mut();
|
||||
let a = a.as_mut()?;
|
||||
Some((std::mem::take(&mut a.session), a.link.take()?, a.name.clone()))
|
||||
})?;
|
||||
if link.tx.send(FromHost { session, outcome, windows, name, done, next_due }).is_err() {
|
||||
if link.tx.send(FromHost { session, outcome, windows, name, done, next_due, render_wait }).is_err() {
|
||||
return None;
|
||||
}
|
||||
if done {
|
||||
@@ -226,6 +235,7 @@ fn yield_wake(outcome: crate::Outcome, windows: Vec<ScriptWindow>, done: bool, n
|
||||
let (s, wake) = match link.rx.recv() {
|
||||
Ok(ToHost::Event(s, ev)) => (s, Wake::Event(ev)),
|
||||
Ok(ToHost::Tick(s, now)) => (s, Wake::Tick(now)),
|
||||
Ok(ToHost::ResumeRender(s)) => (s, Wake::Render),
|
||||
_ => return None,
|
||||
};
|
||||
ACTIVE.with(|a| {
|
||||
@@ -269,8 +279,9 @@ fn host_main(host: u32, name: String, code: String, link: Link) {
|
||||
}
|
||||
Some(Wake::Tick(now)) => {
|
||||
let (ran, error) = crate::tasks::run_due(&mut ctx, host, now);
|
||||
out = crate::Outcome { result: json!(ran), output: settle(), error, waiting: false };
|
||||
out = crate::Outcome { result: json!(ran), output: settle(), error, ..Default::default() };
|
||||
}
|
||||
Some(Wake::Render) => break,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
@@ -283,15 +294,30 @@ fn host_main(host: u32, name: String, code: String, link: Link) {
|
||||
{
|
||||
let message = if body.is_err() { "the script engine stopped unexpectedly" } else { "the script ended" };
|
||||
let out = crate::Outcome { error: Some(ScriptError { message: message.into(), file, ..Default::default() }), ..Default::default() };
|
||||
let _ = link.tx.send(FromHost { session: std::mem::take(&mut a.session), outcome: out, windows: vec![], name, done: true, next_due: None });
|
||||
let _ = link.tx.send(FromHost {
|
||||
session: std::mem::take(&mut a.session),
|
||||
outcome: out,
|
||||
windows: vec![],
|
||||
name,
|
||||
done: true,
|
||||
next_due: None,
|
||||
render_wait: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the host to hand the session back; keep its link while it has windows open.
|
||||
fn receive(session: &mut Session, host: u32, end: HostEnd) -> crate::Outcome {
|
||||
match end.rx.recv() {
|
||||
Ok(FromHost { session: s, outcome, windows, name, done, next_due }) => {
|
||||
Ok(FromHost { session: s, mut outcome, windows, name, done, next_due, render_wait }) => {
|
||||
*session = s;
|
||||
session.script_ui.render_host = render_wait.then_some(host);
|
||||
if !render_wait
|
||||
&& let Some(map) = &end.source_map
|
||||
&& let Some(error) = &mut outcome.error
|
||||
{
|
||||
map.locate(error);
|
||||
}
|
||||
publish(session, host, &name, windows);
|
||||
if done {
|
||||
session.script_ui.windows.retain(|w| w.host != host);
|
||||
@@ -324,7 +350,61 @@ pub(crate) fn run_threaded(session: &mut Session, req: &effectcraft_engine::Scri
|
||||
*session = msg.into_session();
|
||||
return crate::Outcome { error: Some(ScriptError { message: "the script host did not start".into(), ..Default::default() }), ..Default::default() };
|
||||
}
|
||||
receive(session, host, HostEnd { tx: to_tx, rx: from_rx, next_due: None, epoch: 0 })
|
||||
receive(session, host, HostEnd { tx: to_tx, rx: from_rx, next_due: None, epoch: 0, source_map: None })
|
||||
}
|
||||
|
||||
/// Suspend only the JavaScript stack, not the window's event loop (#3289). Keep the output
|
||||
/// and open undo groups in `Active`: settling here would split the script's undo transaction.
|
||||
pub(crate) fn wait_for_render(ctx: &mut Context, mut result: J) -> Result<J, String> {
|
||||
let host = with_active(|a| Ok(a.host))?;
|
||||
let windows = snapshot(ctx);
|
||||
let out = crate::Outcome { render_host: Some(host), ..Default::default() };
|
||||
if !matches!(yield_wake(out, windows, false, None), Some(Wake::Render)) {
|
||||
return Err("the frontend stopped waiting for the script's render".into());
|
||||
}
|
||||
with_active(|a| {
|
||||
let list = a.session.execute_for_script("renderQueue.list", json!({})).map_err(|e| e.to_string())?;
|
||||
let ids: Vec<J> = result["items"].as_array().into_iter().flatten().map(|i| i["id"].clone()).collect();
|
||||
result["items"] = json!(list["items"].as_array().into_iter().flatten().filter(|i| ids.contains(&i["id"])).collect::<Vec<_>>());
|
||||
result["rendering"] = list["rendering"].clone();
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn keep_render_source_map(host: u32, map: crate::include::Expanded) {
|
||||
if let Ok(mut h) = hosts().lock()
|
||||
&& let Some(end) = h.get_mut(&host)
|
||||
{
|
||||
end.source_map = Some(map);
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by the native frontend while its normal editing/callback tick is paused. Polling
|
||||
/// applies terminal statuses and post-render actions before the suspended JS call returns.
|
||||
pub(crate) fn poll_render_wait(session: &mut Session) -> Option<J> {
|
||||
let host = session.script_ui.render_host?;
|
||||
// Only this completion boundary may apply the render's own import/proxy undo steps.
|
||||
session.script_ui.render_host = None;
|
||||
session.poll_render();
|
||||
if session.is_rendering() {
|
||||
session.script_ui.render_host = Some(host);
|
||||
return None;
|
||||
}
|
||||
session.script_ui.render_host = None;
|
||||
let fail = |message: &str| crate::Outcome { error: Some(ScriptError { message: message.into(), ..Default::default() }), ..Default::default() }.to_json();
|
||||
let Some(end) = hosts().lock().ok().and_then(|mut h| h.remove(&host)) else {
|
||||
return Some(fail("the script host stopped while rendering"));
|
||||
};
|
||||
if end.epoch != session.script_ui.project_epoch {
|
||||
return Some(fail("the project changed while the script was rendering"));
|
||||
}
|
||||
if let Err(std::sync::mpsc::SendError(msg)) = end.tx.send(ToHost::ResumeRender(std::mem::take(session))) {
|
||||
*session = msg.into_session();
|
||||
return Some(fail("the script host stopped while rendering"));
|
||||
}
|
||||
let out = receive(session, host, end);
|
||||
// A second render in this invocation keeps the original control reply pending.
|
||||
out.render_host.is_none().then(|| out.to_json())
|
||||
}
|
||||
|
||||
/// Record the windows a host published (named after the script that made them).
|
||||
@@ -429,6 +509,9 @@ pub(crate) fn tick_threaded(session: &mut Session, now: Instant, report: &mut cr
|
||||
}
|
||||
drop(stale);
|
||||
for host in due {
|
||||
if session.script_ui.render_host.is_some() {
|
||||
break;
|
||||
}
|
||||
let Some(end) = hosts().lock().ok().and_then(|mut h| h.remove(&host)) else { continue };
|
||||
if let Err(std::sync::mpsc::SendError(msg)) = end.tx.send(ToHost::Tick(std::mem::take(session), now)) {
|
||||
*session = msg.into_session();
|
||||
@@ -490,7 +573,7 @@ fn tick_inline_host(session: &mut Session, host: u32, name: &str, now: Instant)
|
||||
drop(guard);
|
||||
crate::tasks::set_epoch(host, session.script_ui.project_epoch);
|
||||
publish(session, host, name, windows);
|
||||
crate::Outcome { result: json!(ran), output, error, waiting: false }
|
||||
crate::Outcome { result: json!(ran), output, error, ..Default::default() }
|
||||
}
|
||||
|
||||
/// Forget the session's task hosts that have ended (inline ones: that have no tasks left).
|
||||
|
||||
Reference in new issue
Block a user