diff --git a/src/endor_host.rs b/src/endor_host.rs index 341cbea..a7da969 100644 --- a/src/endor_host.rs +++ b/src/endor_host.rs @@ -200,12 +200,19 @@ fn compile_plan_with_globals( /// One real VM, owned only by the existing execution worker. No history replay. /// The UI bridge shares the same managed object heap and publishes atomic frames. +struct PendingPaint { + window: u64, + document: RenderDocument, + work: Box, +} + struct RefreshWork { documents: std::collections::BTreeMap, windows: std::collections::VecDeque, current: Option, root: Option>, pending: Option>, + paint: Option>, pass_start: bool, pass_changed: bool, changed: bool, @@ -595,6 +602,7 @@ impl ReplSession { current: None, root: None, pending: None, + paint: None, pass_start: true, pass_changed: false, changed: false, @@ -625,6 +633,9 @@ impl ReplSession { } Err(error) => { self.faulted = true; + if let Some(paint) = work.paint.take() { + self.bridge.cancel_paint_frame(&mut self.candidate.vm, paint.window, *paint.work)?; + } if let Some(pending) = work.pending.take() { self.bridge .abort_layout_frame(&mut self.candidate.vm, *pending)?; @@ -656,6 +667,11 @@ impl ReplSession { return Ok(true); }; self.bridge.select_window(id)?; + if let Some(paint) = work.paint.take() { + let PendingPaint { window, document, work: paint_work } = *paint; + let progress = self.bridge.resume_paint_frame(&mut self.candidate.vm, *paint_work)?; + return Self::accept_window_paint(work, window, document, progress); + } let progress = if let Some(pending) = work.pending.take() { self.bridge .resume_layout_frame(&mut self.candidate.vm, *pending)? @@ -694,15 +710,32 @@ impl ReplSession { if work.work > 200_000 { bail!("layout dependencies did not stabilize within the rendering work budget"); } - self.bridge.paint_frame( + let progress = self.bridge.begin_paint_frame( &mut self.candidate.vm, - document.ui_root.as_deref(), - &mut document.frame, + document.ui_root.clone(), + std::mem::take(&mut document.frame), )?; - document.validate()?; - work.documents.insert(id, document); - work.pass_changed = true; - work.changed = true; + Self::accept_window_paint(work, id, document, progress) + } + + fn accept_window_paint( + work: &mut RefreshWork, + id: u64, + mut document: RenderDocument, + progress: crate::source_paint::Progress, + ) -> Result { + match progress { + crate::source_paint::Progress::Pending(paint) => { + work.paint = Some(Box::new(PendingPaint { window: id, document, work: paint })); + } + crate::source_paint::Progress::Complete(draft) => { + document.frame = draft.frame; + document.validate()?; + work.documents.insert(id, document); + work.pass_changed = true; + work.changed = true; + } + } Ok(false) } @@ -759,6 +792,13 @@ impl ReplSession { return Ok(()); }; let cancelled = work.pending.is_some(); + let cancelled_paint = work.paint.is_some(); + if let Some(paint) = work.paint.take() + && let Err(error) = self.bridge.cancel_paint_frame(&mut self.candidate.vm, paint.window, *paint.work) + { + self.faulted = true; + return Err(error); + } if let Some(pending) = work.pending.take() && let Err(error) = self .bridge @@ -767,7 +807,7 @@ impl ReplSession { self.faulted = true; return Err(error); } - if let Err(error) = self.bridge.note_refresh_restart(cancelled) { + if let Err(error) = self.bridge.note_refresh_restart(cancelled, cancelled_paint) { self.faulted = true; return Err(error); } @@ -1023,14 +1063,14 @@ impl ReplSession { impl Drop for ReplSession { fn drop(&mut self) { - if let Some(mut work) = self.refresh.take() - && let Some(pending) = work.pending.take() - { - // Close the read-only continuation and checkpoint roots before - // Candidate releases the globals/services of a discarded domain. - let _ = self - .bridge - .cancel_layout_frame(&mut self.candidate.vm, *pending); + if let Some(mut work) = self.refresh.take() { + if let Some(paint) = work.paint.take() { + let _ = self.bridge.cancel_paint_frame(&mut self.candidate.vm, paint.window, *paint.work); + } + if let Some(pending) = work.pending.take() { + // Close the read-only call/checkpoint before Candidate roots. + let _ = self.bridge.cancel_layout_frame(&mut self.candidate.vm, *pending); + } } } } diff --git a/src/source_paint.rs b/src/source_paint.rs index dfe7276..34d646c 100644 --- a/src/source_paint.rs +++ b/src/source_paint.rs @@ -271,13 +271,77 @@ impl NativeCallable for Prepare { } } -pub fn render( +pub struct Draft { + pub frame: Frame, + pub reads: BTreeSet, + pub slices: usize, +} + +#[must_use = "pending paint must be resumed or cancelled on its owning VM"] +pub enum Progress { + Pending(Box), + Complete(Box), +} + +/// Only unpublished native commands and an owning prepare root cross a pause. +pub struct Work { + desktop: ObjectRef, + prepare: ObjectRef, + output: Output, + root: Option>, + frame: Frame, + slices: usize, +} + +impl Work { + pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result { + self.slices += 1; + let progress = vm.resume_read_only_call(slice); + self.accept(vm, progress) + } + + pub fn cancel(self, vm: &mut Vm) -> Result<()> { + let cancelled = vm.cancel_read_only_call(); + let released = vm.release(self.prepare); + cancelled?; + released?; + Ok(()) + } + + fn accept( + self, + vm: &mut Vm, + progress: Result, + ) -> Result { + match progress { + Ok(endor_core::ExecutionProgress::Pending { .. }) => Ok(Progress::Pending(Box::new(self))), + Ok(endor_core::ExecutionProgress::Complete { value, reads, .. }) => { + vm.release(self.prepare)?; + vm.release_value(value)?; + finish(vm, self, reads) + } + Err(error) => { + let diagnostic = crate::endor_host::runtime_error(vm, error); + self.cancel(vm)?; + Err(diagnostic) + } + } + } +} + +pub fn start( vm: &mut Vm, desktop: ObjectRef, mounts: Vec<(u64, WeakObjectRef)>, - root: Option<&UiNode>, - frame: &mut Frame, -) -> Result> { + root: Option>, + frame: Frame, + slice: usize, +) -> Result { + anyhow::ensure!(slice > 0, "paint slice must be positive"); + anyhow::ensure!( + !vm.has_suspended_call() && !vm.object_store().is_read_only(), + "cannot start paint while another read-only execution is active" + ); let output = Arc::new(Mutex::new(PaintOutput::default())); let prepare = vm.allocate(ContextNativeFunction::new( "prepare source paint", @@ -288,23 +352,30 @@ pub fn render( }, vec![desktop], ))?; - let result = vm.call_read_only_with_limits( + let work = Work { desktop, prepare, output, root, frame, slices: 1 }; + let result = vm.start_read_only_call( prepare, &[], - endor_core::ExecutionLimits { - max_frames: 128, - max_instructions: 1_000_000, - }, + endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000 }, + slice, ); - vm.release(prepare)?; - let (value, reads) = result.map_err(|error| crate::endor_host::runtime_error(vm, error))?; - vm.release_value(value)?; - let output = output + work.accept(vm, result) +} + +fn finish(vm: &mut Vm, mut work: Work, reads: Vec) -> Result { + let output = work.output .lock() .map_err(|_| anyhow::anyhow!("paint output poisoned"))?; if output.commands.is_empty() { - return Ok(BTreeSet::new()); + return Ok(Progress::Complete(Box::new(Draft { + frame: work.frame, + reads: BTreeSet::new(), + slices: work.slices, + }))); } + let root = work.root.as_deref(); + let desktop = work.desktop; + let frame = &mut work.frame; let mut nodes = HashMap::new(); let mut pending: Vec<_> = root.into_iter().collect(); while let Some(node) = pending.pop() { @@ -341,7 +412,7 @@ pub fn render( frame.commands = commands; frame.command_clips = clips; let registry = object(vm.get_attr(desktop, "types")?)?; - Ok(reads + let reads = reads .into_iter() .filter(|object| { *object != desktop @@ -350,5 +421,10 @@ pub fn render( .object(*object) .is_ok_and(|object| !object.payload().as_any().is::()) }) - .collect()) + .collect(); + Ok(Progress::Complete(Box::new(Draft { + frame: work.frame, + reads, + slices: work.slices, + }))) } diff --git a/src/ui_bridge.rs b/src/ui_bridge.rs index e9c7d26..7d53ad4 100644 --- a/src/ui_bridge.rs +++ b/src/ui_bridge.rs @@ -231,6 +231,8 @@ struct Session { refresh_yields: usize, refresh_restarts: usize, layout_cancels: usize, + paint_cancels: usize, + paint_slices: usize, } impl std::ops::Deref for Session { @@ -1159,6 +1161,8 @@ impl NativeCallable for Function { ("refreshYields", session.refresh_yields as i64), ("refreshRestarts", session.refresh_restarts as i64), ("layoutCancels", session.layout_cancels as i64), + ("paintCancels", session.paint_cancels as i64), + ("paintSlices", session.paint_slices as i64), ] { cx.set_attr(stats, name, Value::Number(value.into()))?; } @@ -2451,12 +2455,12 @@ impl UiBridge { result } - pub fn paint_frame( + pub fn begin_paint_frame( &self, vm: &mut Vm, - root: Option<&UiNode>, - frame: &mut crate::document::Frame, - ) -> Result<()> { + root: Option>, + frame: crate::document::Frame, + ) -> Result { let (desktop, mounts) = { let state = self .session @@ -2477,16 +2481,48 @@ impl UiBridge { .collect::>>()?; (state.desktop, mounts) }; - let reads = crate::source_paint::render(vm, desktop, mounts, root, frame)?; - let reads = reads - .into_iter() - .map(|object| vm.change_version(object).map(|version| (object, version))) - .collect::, _>>()?; - self.session - .lock() - .map_err(|_| anyhow::anyhow!("UI session poisoned"))? - .paint_reads = reads; - Ok(()) + let progress = crate::source_paint::start(vm, desktop, mounts, root, frame, 256)?; + self.accept_paint_progress(vm, progress) + } + + pub fn resume_paint_frame( + &self, + vm: &mut Vm, + work: crate::source_paint::Work, + ) -> Result { + let progress = work.resume(vm, 256)?; + self.accept_paint_progress(vm, progress) + } + + fn accept_paint_progress( + &self, + vm: &Vm, + progress: crate::source_paint::Progress, + ) -> Result { + if let crate::source_paint::Progress::Complete(draft) = &progress { + let reads = draft.reads.iter().copied() + .map(|object| vm.change_version(object).map(|version| (object, version))) + .collect::, _>>()?; + let mut state = self.session.lock().map_err(|_| anyhow::anyhow!("UI session poisoned"))?; + state.paint_reads = reads; + state.paint_slices = draft.slices; + } + Ok(progress) + } + + pub fn cancel_paint_frame( + &self, + vm: &mut Vm, + window: u64, + work: crate::source_paint::Work, + ) -> Result<()> { + let cancelled = work.cancel(vm); + let invalidated = self.select_window(window).and_then(|()| { + self.session.lock().map_err(|_| anyhow::anyhow!("UI session poisoned"))?.force_frame = true; + Ok(()) + }); + cancelled?; + invalidated } pub fn begin_layout_frame( @@ -2615,7 +2651,7 @@ impl UiBridge { Ok(()) } - pub fn note_refresh_restart(&self, cancelled_layout: bool) -> Result<()> { + pub fn note_refresh_restart(&self, cancelled_layout: bool, cancelled_paint: bool) -> Result<()> { let mut state = self .session .lock() @@ -2624,6 +2660,7 @@ impl UiBridge { state.layout_cancels = state .layout_cancels .saturating_add(usize::from(cancelled_layout)); + state.paint_cancels = state.paint_cancels.saturating_add(usize::from(cancelled_paint)); Ok(()) } @@ -2827,6 +2864,8 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { refresh_yields: 0, refresh_restarts: 0, layout_cancels: 0, + paint_cancels: 0, + paint_slices: 0, })); let mut public_functions = Vec::new(); let mut factories = Vec::new();