diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 744f0b1830..b00406d0f5 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1424,18 +1424,20 @@ (set-shape-layout shape) (set-layout-data shape) (let [is-text? (= type :text) - pending_thumbnails (into [] (concat - (when is-text? (set-shape-text-content id content)) + text-content-pending (when is-text? (set-shape-text-content id content)) + pending-thumbnails (into [] (concat + text-content-pending (when is-text? (set-shape-text-images id content true)) (set-shape-fills id fills true) (set-shape-strokes id strokes true))) - pending_full (into [] (concat + pending-full (into [] (concat (when is-text? (set-shape-text-images id content false)) (set-shape-fills id fills false) (set-shape-strokes id strokes false)))] (perf/end-measure "set-object") - {:thumbnails pending_thumbnails - :full pending_full})))) + {:thumbnails pending-thumbnails + :full pending-full + :font-pending-ids (if (some :callback text-content-pending) [id] [])})))) (defn- update-text-layouts "Synchronously update text layouts for all shapes and send rect updates @@ -1443,8 +1445,29 @@ [text-ids] (run! f/update-text-layout text-ids)) +(defn- force-update-text-layouts + "Like update-text-layouts but forces a relayout. Use after pending fonts + resolve so layouts (and the extrect/tiles derived from them) use real glyph + metrics instead of fallback-font estimates." + [text-ids] + (run! f/force-update-text-layout text-ids)) + +(defn- relayout-after-fonts! + "Relayout text shapes once their pending fonts have resolved. Shapes in + `font-pending-ids` had a font fetched, so they get a forced relayout to pick + up the real glyph metrics; the remaining text shapes get a normal layout." + [shapes font-pending-ids] + (let [force-ids (set font-pending-ids) + text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes) + forced (filterv force-ids text-ids) + rest-ids (filterv (complement force-ids) text-ids)] + (when (seq forced) + (force-update-text-layouts forced)) + (when (seq rest-ids) + (update-text-layouts rest-ids)))) + (defn process-pending - [shapes thumbnails full on-complete] + [shapes thumbnails full font-pending-ids on-complete] (let [pending-thumbnails (d/index-by :key :callback thumbnails) @@ -1468,11 +1491,7 @@ (rx/catch #(rx/empty)))) (rx/subs! (fn [_] - ;; Fonts are now loaded — recompute text layouts so Skia - ;; uses the real metrics instead of fallback-font estimates. - (let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)] - (when (seq text-ids) - (update-text-layouts text-ids))) + (relayout-after-fonts! shapes font-pending-ids) (request-render "images-loaded")) noop-fn (fn [] (when (fn? on-complete) (on-complete))))) @@ -1481,8 +1500,8 @@ (defn process-object [shape] - (let [{:keys [thumbnails full]} (set-object shape)] - (process-pending [shape] thumbnails full noop-fn))) + (let [{:keys [thumbnails full font-pending-ids]} (set-object shape)] + (process-pending [shape] thumbnails full font-pending-ids noop-fn))) (defn process-objects "Like process-object but for multiple shapes at once. Accumulates all @@ -1491,37 +1510,43 @@ just the first shape that triggered the fetch." [shapes] (let [total-shapes (count shapes) - {:keys [thumbnails full]} - (loop [index 0 thumbnails-acc (transient []) full-acc (transient [])] + {:keys [thumbnails full font-pending-ids]} + (loop [index 0 thumbnails-acc (transient []) full-acc (transient []) font-acc (transient [])] (if (< index total-shapes) (let [shape (nth shapes index) - {:keys [thumbnails full]} (set-object shape)] + {:keys [thumbnails full font-pending-ids]} (set-object shape)] (recur (inc index) (reduce conj! thumbnails-acc thumbnails) - (reduce conj! full-acc full))) - {:thumbnails (persistent! thumbnails-acc) :full (persistent! full-acc)}))] - (process-pending shapes thumbnails full noop-fn))) + (reduce conj! full-acc full) + (reduce conj! font-acc font-pending-ids))) + {:thumbnails (persistent! thumbnails-acc) + :full (persistent! full-acc) + :font-pending-ids (persistent! font-acc)}))] + (process-pending shapes thumbnails full font-pending-ids noop-fn))) (defn- process-shapes-chunk "Process shapes starting at `start-index` until the time budget is exhausted. - Returns {:thumbnails [...] :full [...] :next-index n}" - [shapes start-index thumbnails-acc full-acc] + Returns {:thumbnails [...] :full [...] :font-pending-ids [...] :next-index n}" + [shapes start-index thumbnails-acc full-acc font-pending-acc] (let [total (count shapes) deadline (+ (js/performance.now) CHUNK_TIME_BUDGET_MS)] (loop [index start-index t-acc (transient thumbnails-acc) - f-acc (transient full-acc)] + f-acc (transient full-acc) + fp-acc (transient font-pending-acc)] (if (and (< index total) ;; Check performance.now every 8 shapes to reduce overhead (or (pos? (bit-and (- index start-index) 7)) (<= (js/performance.now) deadline))) (let [shape (nth shapes index) - {:keys [thumbnails full]} (set-object shape)] + {:keys [thumbnails full font-pending-ids]} (set-object shape)] (recur (inc index) (reduce conj! t-acc thumbnails) - (reduce conj! f-acc full))) + (reduce conj! f-acc full) + (reduce conj! fp-acc font-pending-ids))) {:thumbnails (persistent! t-acc) :full (persistent! f-acc) + :font-pending-ids (persistent! fp-acc) :next-index index})))) (defn- set-objects-async @@ -1532,16 +1557,16 @@ (let [total-shapes (count shapes)] (p/create (fn [resolve _reject] - (letfn [(process-next-chunk [index thumbnails-acc full-acc] + (letfn [(process-next-chunk [index thumbnails-acc full-acc font-pending-acc] (if (< index total-shapes) ;; Process one time-budgeted chunk - (let [{:keys [thumbnails full next-index]} + (let [{:keys [thumbnails full font-pending-ids next-index]} (process-shapes-chunk shapes index - thumbnails-acc full-acc)] + thumbnails-acc full-acc font-pending-acc)] ;; Yield to browser, then continue with next chunk (-> (yield-to-browser) (p/then (fn [_] - (process-next-chunk next-index thumbnails full))))) + (process-next-chunk next-index thumbnails full font-pending-ids))))) ;; All chunks done - finalize (do (perf/end-measure "set-objects") @@ -1588,13 +1613,11 @@ (rx/reduce conj []))) (rx/subs! (fn [_] - (let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)] - (when (seq text-ids) - (update-text-layouts text-ids))) + (relayout-after-fonts! shapes font-pending-acc) (request-render "images-loaded")) noop-fn noop-fn)))))))] - (process-next-chunk 0 [] [])))))) + (process-next-chunk 0 [] [] [])))))) ;; This is a version of process-pending that doesn't have sideffects @@ -1647,22 +1670,25 @@ "Synchronously process all shapes (for small shape counts)." [shapes render-callback on-shapes-ready] (let [total-shapes (count shapes) - {:keys [thumbnails full]} - (loop [index 0 thumbnails-acc (transient []) full-acc (transient [])] + {:keys [thumbnails full font-pending-ids]} + (loop [index 0 thumbnails-acc (transient []) full-acc (transient []) font-acc (transient [])] (if (< index total-shapes) (let [shape (nth shapes index) - {:keys [thumbnails full]} (set-object shape)] + {:keys [thumbnails full font-pending-ids]} (set-object shape)] (recur (inc index) (reduce conj! thumbnails-acc thumbnails) - (reduce conj! full-acc full))) - {:thumbnails (persistent! thumbnails-acc) :full (persistent! full-acc)}))] + (reduce conj! full-acc full) + (reduce conj! font-acc font-pending-ids))) + {:thumbnails (persistent! thumbnails-acc) + :full (persistent! full-acc) + :font-pending-ids (persistent! font-acc)}))] (perf/end-measure "set-objects") (when on-shapes-ready (on-shapes-ready)) ;; Rebuild the tile index so _render knows which shapes ;; map to which tiles after a page switch. (h/call wasm/internal-module "_set_view_end") (reset! view-interaction-active? false) - (process-pending shapes thumbnails full + (process-pending shapes thumbnails full font-pending-ids (fn [] (if render-callback (render-callback) diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs index 676a00cf78..86946db5ba 100644 --- a/frontend/src/app/render_wasm/api/fonts.cljs +++ b/frontend/src/app/render_wasm/api/fonts.cljs @@ -119,6 +119,16 @@ (aget shape-id-buffer 2) (aget shape-id-buffer 3))))) +(defn force-update-text-layout + [id] + (when wasm/context-initialized? + (let [shape-id-buffer (uuid/get-u32 id)] + (h/call wasm/internal-module "_force_update_shape_text_layout_for" + (aget shape-id-buffer 0) + (aget shape-id-buffer 1) + (aget shape-id-buffer 2) + (aget shape-id-buffer 3))))) + ;; IMPORTANT: Only TTF fonts can be stored. (defn- store-font-buffer [font-data font-array-buffer emoji? fallback?] diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index feed61d674..403a1314ee 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -250,13 +250,15 @@ (api/set-shape-svg-raw-content (api/get-static-markup shape)) (cfh/text-shape? shape) - (let [pending-thumbnails (into [] (concat (api/set-shape-text-content id v))) - pending-full (into [] (concat (api/set-shape-text-images id v)))] + (let [text-content-pending (api/set-shape-text-content id v) + pending-thumbnails (vec text-content-pending) + pending-full (vec (api/set-shape-text-images id v)) + font-pending-ids (when (some :callback text-content-pending) [id])] ;; FIXME: this is a hack to process the pending tasks ;; asynchronously we should probably modify set-wasm-attr! ;; to return a list of callbacks to be executed in a ;; second pass. - (api/process-pending [shape] pending-thumbnails pending-full api/noop-fn) + (api/process-pending [shape] pending-thumbnails pending-full font-pending-ids api/noop-fn) nil)) :grow-type diff --git a/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs b/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs index 740029a3ca..c985b336ae 100644 --- a/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs @@ -51,7 +51,7 @@ (let [shapes [(make-shape :text) (make-shape :text) (make-shape :rect)] visited-ids (atom []) mock-set (fn [s] (swap! visited-ids conj (:id s)) {:thumbnails [] :full []}) - mock-pend (fn [_sh _t _f _cb] nil)] + mock-pend (fn [_sh _t _f _fp _cb] nil)] (with-mocks* mock-set mock-pend #(wasm.api/process-objects shapes)) @@ -66,7 +66,7 @@ (let [shapes [(make-shape :text) (make-shape :text)] captured (atom nil) mock-set (fn [_s] {:thumbnails [] :full []}) - mock-pend (fn [sh t f cb] (reset! captured {:shapes sh :thumbnails t :full f :cb cb}))] + mock-pend (fn [sh t f _fp cb] (reset! captured {:shapes sh :thumbnails t :full f :cb cb}))] (with-mocks* mock-set mock-pend #(wasm.api/process-objects shapes)) @@ -99,7 +99,7 @@ {:thumbnails [] :full []})) mock-pend - (fn [sh t f _cb] (reset! captured {:shapes sh :thumbnails t :full f}))] + (fn [sh t f _fp _cb] (reset! captured {:shapes sh :thumbnails t :full f}))] (with-mocks* mock-set mock-pend #(wasm.api/process-objects shapes)) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ee4ebe3bd0..a4a2e38eb1 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -410,6 +410,11 @@ pub(crate) struct RenderState { /// GPU crops from `Backbuffer` or tile atlas keyed by shape id. Filled on full-frame completion; during /// drag, entries for the moved top-level selection are ensured here pub backbuffer_crop_cache: HashMap, + /// Whether we've already forced a GPU flush+submit before a tile-atlas + /// snapshot this render. The first snapshot of a pass can otherwise capture + /// a tile before its text glyph uploads complete (blank first/center tile). + /// One explicit flush warms the submit path for the rest of the pass. + pub tile_atlas_flushed: bool, } pub struct InteractiveDragCrop { @@ -592,6 +597,7 @@ impl RenderState { interactive_target_seeded: false, preserve_target_during_render: false, backbuffer_crop_cache: HashMap::default(), + tile_atlas_flushed: false, }) } @@ -1020,6 +1026,12 @@ impl RenderState { .as_ref() .ok_or(Error::CriticalError("Current tile not found".to_string()))?; + // Force pending GPU work (text glyph-atlas uploads) + if !self.tile_atlas_flushed { + crate::get_gpu_state().context.flush_and_submit(); + self.tile_atlas_flushed = true; + } + self.surfaces.draw_current_tile_into_tile_atlas( &self.tile_viewbox, ¤t_tile, @@ -2090,6 +2102,7 @@ impl RenderState { self.surfaces.atlas.set_doc_bounds(doc_bounds); self.cache_cleared_this_render = false; + self.tile_atlas_flushed = false; let preserve_target = self.preserve_target_during_render; self.preserve_target_during_render = false; diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 86159ac3df..484acf09f3 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -888,6 +888,11 @@ impl TextContent { .copy_finite_size(result.2, default_width, default_height); } + pub fn force_next_layout_update(&mut self) { + self.layout_width = None; + self.layout.cached_extrect.set(None); + } + pub fn update_layout(&mut self, selrect: Rect) -> TextContentSize { if !self.layout.needs_update() && self.layout_version == self.content_version diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index c97ec81276..6e2575741f 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -370,8 +370,11 @@ pub extern "C" fn intersect_position_in_shape( false } -fn update_text_layout(shape: &mut Shape) { +fn update_text_layout(shape: &mut Shape, force: bool) { if let Type::Text(text_content) = &mut shape.shape_type { + if force { + text_content.force_next_layout_update(); + } text_content.update_layout(shape.selrect); shape.invalidate_extrect(); } @@ -380,7 +383,7 @@ fn update_text_layout(shape: &mut Shape) { #[no_mangle] pub extern "C" fn update_shape_text_layout() { with_current_shape_mut!(state, |shape: &mut Shape| { - update_text_layout(shape); + update_text_layout(shape, false); }); } @@ -389,7 +392,18 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) { with_state!(state, { let shape_id = uuid_from_u32_quartet(a, b, c, d); if let Some(shape) = state.shapes.get_mut(&shape_id) { - update_text_layout(shape); + update_text_layout(shape, false); + } + state.touch_shape(shape_id); + }); +} + +#[no_mangle] +pub extern "C" fn force_update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) { + with_state!(state, { + let shape_id = uuid_from_u32_quartet(a, b, c, d); + if let Some(shape) = state.shapes.get_mut(&shape_id) { + update_text_layout(shape, true); } state.touch_shape(shape_id); });