From 48e83a9b45b833dd57d598c4bef679fcfe321528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 02:31:47 -0400 Subject: [PATCH] ElyShow: show layers come one per reply, so large slides play (R2.20) --- crates/ely-lok/src/client/doc.rs | 16 ++++++++- crates/ely-lok/src/client/tests/motion.rs | 18 ++++++++++ crates/ely-lok/src/ipc.rs | 2 ++ crates/ely-lok/src/worker.rs | 1 + crates/ely-lok/src/worker/slides.rs | 41 ++++++++++++++++++----- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/crates/ely-lok/src/client/doc.rs b/crates/ely-lok/src/client/doc.rs index 51ed029..ec445cb 100644 --- a/crates/ely-lok/src/client/doc.rs +++ b/crates/ely-lok/src/client/doc.rs @@ -180,7 +180,21 @@ impl Doc { .u64(u64::from(pixels.0)) .u64(u64::from(pixels.1)) .bytes(); - crate::show::layers(&worker.request(tag::SLIDE_LAYERS, &request)?) + let head = worker.request(tag::SLIDE_LAYERS, &request)?; + let mut unpack = Unpack::new(&head); + let (width, height, count) = (unpack.u64()?, unpack.u64()?, unpack.u64()?); + // One layer per reply keeps each frame small. + let mut whole = Pack::new().u64(width).u64(height).u64(count); + for index in 0..count { + let (json, painted) = (unpack.str()?, unpack.u64()?); + whole = whole.str(&json).u64(painted); + if painted != 0 { + let id = worker.doc_id(self)?; + let reply = worker.request(tag::SLIDE_LAYER, &Pack::new().u64(id).u64(index).bytes())?; + whole = whole.blob(&Unpack::new(&reply).blob()?); + } + } + crate::show::layers(&whole.bytes()) } /// Saves to `url` as `format`; failure surfaces (LOK-09). diff --git a/crates/ely-lok/src/client/tests/motion.rs b/crates/ely-lok/src/client/tests/motion.rs index cb7968d..3e53e84 100644 --- a/crates/ely-lok/src/client/tests/motion.rs +++ b/crates/ely-lok/src/client/tests/motion.rs @@ -220,3 +220,21 @@ fn an_animated_object_is_its_own_layer_under_its_effect_hash() { + +#[test] +fn layers_larger_than_one_frame_arrive_one_by_one() { + if install().is_none() { + eprintln!("SKIP: no pinned LOK build"); + return; + } + let (_dir, mut worker, doc) = open_deck(); + worker.run(&doc, "draw.shape", &["rectangle", "true"]).expect("draws"); + let json = serde_json::json!({ "preset": "ooo-entrance-fade-in" }).to_string(); + worker.run(&doc, "animation.add", &[&json]).expect("adds"); + // Three layers at this size pass one 64 MB frame. + let layers = doc.slide_layers(&mut worker, 0, (5000, 2812)).expect("layers"); + let (w, h) = layers.size; + let bytes = w as usize * h as usize * 4; + assert!(layers.list.len() * bytes > crate::ipc::MAX_FRAME, "{} layers of {bytes}", layers.list.len()); + assert!(layers.list.iter().all(|layer| layer.pixels.len() == bytes), "every layer arrives whole"); +} diff --git a/crates/ely-lok/src/ipc.rs b/crates/ely-lok/src/ipc.rs index c376c8b..da69c0c 100644 --- a/crates/ely-lok/src/ipc.rs +++ b/crates/ely-lok/src/ipc.rs @@ -86,6 +86,8 @@ pub mod tag { /// Refuses unless the document is still at a revision. pub const EXPECT: u8 = 33; pub const REVISION: u8 = 34; + /// One layer's pixels from the last `SLIDE_LAYERS`. + pub const SLIDE_LAYER: u8 = 35; pub const OK: u8 = 100; pub const ERR: u8 = 101; } diff --git a/crates/ely-lok/src/worker.rs b/crates/ely-lok/src/worker.rs index 0a86a0e..e2de5ff 100644 --- a/crates/ely-lok/src/worker.rs +++ b/crates/ely-lok/src/worker.rs @@ -205,6 +205,7 @@ fn answer(lok: &Lok, tag: u8, id: u64, payload: &[u8]) { tag::PAINT_TILE => input::painted(unpack), tag::PAINT_PART => input::part_painted(unpack), tag::SLIDE_LAYERS => slides::slide_layers(unpack), + tag::SLIDE_LAYER => slides::slide_layer(unpack), tag::VERIFY => unpack .str() .and_then(|url| verify(lok, &url)) diff --git a/crates/ely-lok/src/worker/slides.rs b/crates/ely-lok/src/worker/slides.rs index b43b6f5..b4d7f21 100644 --- a/crates/ely-lok/src/worker/slides.rs +++ b/crates/ely-lok/src/worker/slides.rs @@ -1,9 +1,17 @@ -use std::io; +use std::{cell::RefCell, collections::HashMap, io}; use super::with_doc; -use crate::ipc::{self, Pack, Unpack}; +use crate::ipc::{Pack, Unpack}; -/// Slide `part`'s slideshow layers at about the asked pixels. +/// The most a slide's layers may take in memory. +const LAYER_MEMORY: usize = 512 * 1024 * 1024; + +// Rendered layers wait here; each leaves on its own request. +thread_local! { + static WAITING: RefCell>>>> = RefCell::new(HashMap::new()); +} + +/// Slide `part`'s layers, its pixels held back. pub(super) fn slide_layers(unpack: &mut Unpack) -> io::Result> { let (id, part) = (unpack.u64()?, unpack.u64()?); let (width, height) = (unpack.u64()?, unpack.u64()?); @@ -20,18 +28,33 @@ pub(super) fn slide_layers(unpack: &mut Unpack) -> io::Result> { let hash = slide .and_then(|slide| slide["hash"].as_str()) .ok_or_else(|| bad("no such slide"))?; - let ((width, height), layers) = - doc.slide_layers((part, hash), (width, height), ipc::MAX_FRAME - 1024)?; + let ((width, height), layers) = doc.slide_layers((part, hash), (width, height), LAYER_MEMORY)?; let mut body = Pack::new() .u64(u64::from(width)) .u64(u64::from(height)) .u64(layers.len() as u64); - for layer in &layers { + let mut pixels = Vec::with_capacity(layers.len()); + for layer in layers { body = body.str(&layer.json).u64(u64::from(layer.pixels.is_some())); - if let Some(pixels) = &layer.pixels { - body = body.blob(pixels); - } + pixels.push(layer.pixels); } + WAITING.with(|waiting| waiting.borrow_mut().insert(id, pixels)); Ok(body.bytes()) })? } + +/// Hands over layer `index`'s pixels once, then frees them. +pub(super) fn slide_layer(unpack: &mut Unpack) -> io::Result> { + let (id, index) = (unpack.u64()?, unpack.u64()?); + let taken = WAITING.with(|waiting| { + let mut waiting = waiting.borrow_mut(); + let layers = waiting.get_mut(&id)?; + let pixels = layers.get_mut(usize::try_from(index).ok()?)?.take(); + if layers.iter().all(Option::is_none) { + waiting.remove(&id); + } + pixels + }); + let pixels = taken.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, format!("no layer {index} waits")))?; + Ok(Pack::new().blob(&pixels).bytes()) +}