Render WASM dashboard thumbnails

This commit is contained in:
alonso.torres
2025-11-19 17:10:11 +01:00
parent b8c0c5c310
commit 59845b756f
11 changed files with 400 additions and 169 deletions

View File

@@ -96,7 +96,7 @@
;; loading all pages into memory for find the frame set for thumbnail.
(defn get-file-data-for-thumbnail
[{:keys [::db/conn] :as cfg} {:keys [data id] :as file}]
[{:keys [::db/conn] :as cfg} {:keys [data id] :as file} strip-frames-with-thumbnails]
(letfn [;; function responsible on finding the frame marked to be
;; used as thumbnail; the returned frame always have
;; the :page-id set to the page that it belongs.
@@ -173,7 +173,7 @@
;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecessary data.
:always
strip-frames-with-thumbnails
(update :objects assoc-thumbnails page-id thumbs)))))
(def ^:private
@@ -186,7 +186,8 @@
[:map {:title "PartialFile"}
[:id ::sm/uuid]
[:revn {:min 0} ::sm/int]
[:page [:map-of :keyword ::sm/any]]])
[:page [:map-of :keyword ::sm/any]]
[:strip-frames-with-thumbnails {:optional true} ::sm/boolean]])
(sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used
@@ -195,7 +196,7 @@
::doc/module :files
::sm/params schema:get-file-data-for-thumbnail
::sm/result schema:partial-file}
[cfg {:keys [::rpc/profile-id file-id] :as params}]
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id)
@@ -205,14 +206,18 @@
file (bfc/get-file cfg file-id
:realize? true
:read-only? true)]
:read-only? true)
strip-frames-with-thumbnails
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
(true? strip-frames-with-thumbnails))]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file)))
{:file-id file-id
:revn (:revn file)
:page (get-file-data-for-thumbnail cfg file)}))))
:page (get-file-data-for-thumbnail cfg file strip-frames-with-thumbnails)}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS

View File

@@ -83,7 +83,7 @@
:source-map-detail-level :all}}}
:worker
{:target :browser
{:target :esm
:output-dir "resources/public/js/worker/"
:asset-path "/js/worker"
:devtools {:browser-inject :main

View File

@@ -67,7 +67,7 @@
:height (:height vbox)
:fill color}])
(defn- calculate-dimensions
(defn calculate-dimensions
[objects aspect-ratio]
(let [root-objects (ctst/get-root-objects objects)]
(if (empty? root-objects)

View File

@@ -18,6 +18,7 @@
[app.main.data.notifications :as ntf]
[app.main.data.project :as dpj]
[app.main.data.team :as dtm]
[app.main.features :as features]
[app.main.fonts :as fonts]
[app.main.rasterizer :as thr]
[app.main.refs :as refs]
@@ -46,6 +47,8 @@
(log/set-level! :debug)
(def thumbnail-width 252)
;; --- Grid Item Thumbnail
(defn- persist-thumbnail
@@ -56,15 +59,22 @@
(defn render-thumbnail
[file-id revn]
(->> (mw/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id})
(rx/mapcat (fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]
(assoc result
:styles styles
:width 252))))))))
(if (features/active-feature? @st/state "render-wasm/v1")
(->> (mw/ask! {:cmd :thumbnails/generate-for-file-wasm
:revn revn
:file-id file-id
:width thumbnail-width}))
(->> (mw/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id
:width thumbnail-width})
(rx/mapcat
(fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]
(-> result
(assoc :styles styles
:width thumbnail-width))))))))))
(defn- ask-for-thumbnail
"Creates some hooks to handle the files thumbnails cache"

View File

@@ -94,6 +94,23 @@
(set! wasm/internal-frame-id nil)
(ug/dispatch! (ug/event "penpot:wasm:render"))))
(defn render-sync
[]
(when wasm/context-initialized?
(h/call wasm/internal-module "_render_sync")
(set! wasm/internal-frame-id nil)))
(defn render-sync-shape
[id]
(when wasm/context-initialized?
(let [buffer (uuid/get-u32 id)]
(h/call wasm/internal-module "_render_sync_shape"
(aget buffer 0)
(aget buffer 1)
(aget buffer 2)
(aget buffer 3))
(set! wasm/internal-frame-id nil))))
(def set-view-render
(fns/debounce
(fn [ts]
@@ -290,6 +307,13 @@
(aset textures new-id texture)
new-id))
(defn- retrieve-image
[url]
(rx/from
(-> (js/fetch url)
(p/then (fn [^js response] (.blob response)))
(p/then (fn [^js image] (js/createImageBitmap image))))))
(defn- fetch-image
"Loads an image and creates a WebGL texture from it, passing the texture ID to WASM.
This avoids decoding the image twice (once in browser, once in WASM)."
@@ -297,56 +321,50 @@
(let [url (cf/resolve-file-media {:id image-id} thumbnail?)]
{:key url
:thumbnail? thumbnail?
:callback #(->> (p/create
(fn [resolve reject]
(let [img (js/Image.)
on-load (fn []
(resolve img))
on-error (fn [err]
(reject err))]
(set! (.-crossOrigin img) "anonymous")
(.addEventListener img "load" on-load)
(.addEventListener img "error" on-error)
(set! (.-src img) url))))
(rx/from)
(rx/map (fn [img]
(when-let [gl (get-webgl-context)]
(let [texture (create-webgl-texture-from-image gl img)
texture-id (get-texture-id-for-gl-object texture)
width (.-width ^js img)
height (.-height ^js img)
;; Header: 32 bytes (2 UUIDs) + 4 bytes (thumbnail) + 4 bytes (texture ID) + 8 bytes (dimensions)
total-bytes 48
offset (mem/alloc->offset-32 total-bytes)
heap32 (mem/get-heap-u32)]
:callback
(fn []
(->> (retrieve-image url)
(rx/map
(fn [img]
(when-let [gl (get-webgl-context)]
(let [texture (create-webgl-texture-from-image gl img)
texture-id (get-texture-id-for-gl-object texture)
width (.-width ^js img)
height (.-height ^js img)
;; Header: 32 bytes (2 UUIDs) + 4 bytes (thumbnail)
;; + 4 bytes (texture ID) + 8 bytes (dimensions)
total-bytes 48
offset (mem/alloc->offset-32 total-bytes)
heap32 (mem/get-heap-u32)]
;; 1. Set shape id (offset + 0 to offset + 3)
(mem.h32/write-uuid offset heap32 shape-id)
;; 1. Set shape id (offset + 0 to offset + 3)
(mem.h32/write-uuid offset heap32 shape-id)
;; 2. Set image id (offset + 4 to offset + 7)
(mem.h32/write-uuid (+ offset 4) heap32 image-id)
;; 2. Set image id (offset + 4 to offset + 7)
(mem.h32/write-uuid (+ offset 4) heap32 image-id)
;; 3. Set thumbnail flag as u32 (offset + 8)
(aset heap32 (+ offset 8) (if thumbnail? 1 0))
;; 3. Set thumbnail flag as u32 (offset + 8)
(aset heap32 (+ offset 8) (if thumbnail? 1 0))
;; 4. Set texture ID (offset + 9)
(aset heap32 (+ offset 9) texture-id)
;; 4. Set texture ID (offset + 9)
(aset heap32 (+ offset 9) texture-id)
;; 5. Set width (offset + 10)
(aset heap32 (+ offset 10) width)
;; 5. Set width (offset + 10)
(aset heap32 (+ offset 10) width)
;; 6. Set height (offset + 11)
(aset heap32 (+ offset 11) height)
;; 6. Set height (offset + 11)
(aset heap32 (+ offset 11) height)
(h/call wasm/internal-module "_store_image_from_texture")
true))))
(rx/catch (fn [cause]
(log/error :hint "Could not fetch image"
:image-id image-id
:thumbnail? thumbnail?
:url url
:cause cause)
(rx/empty))))}))
(h/call wasm/internal-module "_store_image_from_texture")
true))))
(rx/catch
(fn [cause]
(log/error :hint "Could not fetch image"
:image-id image-id
:thumbnail? thumbnail?
:url url
:cause cause)
(rx/empty)))))}))
(defn- get-fill-images
[leaf]
@@ -961,26 +979,30 @@
:dimensions (get-text-dimensions id)})))))
(defn process-pending
[shapes thumbnails full on-complete]
(let [pending-thumbnails
(d/index-by :key :callback thumbnails)
([shapes thumbnails full on-complete]
(process-pending shapes thumbnails full nil on-complete))
([shapes thumbnails full on-render on-complete]
(let [pending-thumbnails
(d/index-by :key :callback thumbnails)
pending-full
(d/index-by :key :callback full)]
pending-full
(d/index-by :key :callback full)]
(->> (rx/concat
(->> (rx/from (vals pending-thumbnails))
(rx/merge-map (fn [callback] (callback)))
(rx/reduce conj []))
(->> (rx/from (vals pending-full))
(rx/mapcat (fn [callback] (callback)))
(rx/reduce conj [])))
(rx/subs!
(fn [_]
(update-text-layouts shapes)
(request-render "pending-finished"))
noop-fn
on-complete))))
(->> (rx/concat
(->> (rx/from (vals pending-thumbnails))
(rx/merge-map (fn [callback] (callback)))
(rx/reduce conj []))
(->> (rx/from (vals pending-full))
(rx/mapcat (fn [callback] (callback)))
(rx/reduce conj [])))
(rx/subs!
(fn [_]
(update-text-layouts shapes)
(if on-render
(on-render)
(request-render "pending-finished")))
noop-fn
on-complete)))))
(defn process-object
[shape]
@@ -988,24 +1010,26 @@
(process-pending [shape] thumbnails full noop-fn)))
(defn set-objects
[objects]
(perf/begin-measure "set-objects")
(let [shapes (into [] (vals objects))
total-shapes (count shapes)
;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]}
{:keys [thumbnails full]}
(loop [index 0 thumbnails-acc [] full-acc []]
(if (< index total-shapes)
(let [shape (nth shapes index)
{:keys [thumbnails full]} (set-object objects shape)]
(recur (inc index)
(into thumbnails-acc thumbnails)
(into full-acc full)))
{:thumbnails thumbnails-acc :full full-acc}))]
(perf/end-measure "set-objects")
(process-pending shapes thumbnails full
(fn []
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))
([objects]
(set-objects objects nil))
([objects render-callback]
(perf/begin-measure "set-objects")
(let [shapes (into [] (vals objects))
total-shapes (count shapes)
;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]}
{:keys [thumbnails full]}
(loop [index 0 thumbnails-acc [] full-acc []]
(if (< index total-shapes)
(let [shape (nth shapes index)
{:keys [thumbnails full]} (set-object objects shape)]
(recur (inc index)
(into thumbnails-acc thumbnails)
(into full-acc full)))
{:thumbnails thumbnails-acc :full full-acc}))]
(perf/end-measure "set-objects")
(process-pending shapes thumbnails full render-callback
(fn []
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(defn clear-focus-mode
[]
@@ -1132,14 +1156,16 @@
(request-render "set-modifiers")))))
(defn initialize-viewport
[base-objects zoom vbox background]
(let [rgba (sr-clr/hex->u32argb background 1)
shapes (into [] (vals base-objects))
total-shapes (count shapes)]
(h/call wasm/internal-module "_set_canvas_background" rgba)
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
(h/call wasm/internal-module "_init_shapes_pool" total-shapes)
(set-objects base-objects)))
([base-objects zoom vbox background]
(initialize-viewport base-objects zoom vbox background nil))
([base-objects zoom vbox background callback]
(let [rgba (sr-clr/hex->u32argb background 1)
shapes (into [] (vals base-objects))
total-shapes (count shapes)]
(h/call wasm/internal-module "_set_canvas_background" rgba)
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
(h/call wasm/internal-module "_init_shapes_pool" total-shapes)
(set-objects base-objects callback))))
(def ^:private default-context-options
#js {:antialias false
@@ -1160,8 +1186,10 @@
(defn set-canvas-size
[canvas]
(set! (.-width canvas) (* dpr (.-clientWidth ^js canvas)))
(set! (.-height canvas) (* dpr (.-clientHeight ^js canvas))))
(let [width (or (.-clientWidth ^js canvas) (.-width ^js canvas))
height (or (.-clientHeight ^js canvas) (.-height ^js canvas))]
(set! (.-width canvas) (* dpr width))
(set! (.-height canvas) (* dpr height))))
(defn- get-browser
[]
@@ -1274,51 +1302,59 @@
(mem/free)
content)))
(defn init-wasm-module
[module]
(let [default-fn (unchecked-get module "default")
serializers
#js
{:blur-type (unchecked-get module "RawBlurType")
:blend-mode (unchecked-get module "RawBlendMode")
:bool-type (unchecked-get module "RawBoolType")
:font-style (unchecked-get module "RawFontStyle")
:flex-direction (unchecked-get module "RawFlexDirection")
:grid-direction (unchecked-get module "RawGridDirection")
:grow-type (unchecked-get module "RawGrowType")
:align-items (unchecked-get module "RawAlignItems")
:align-self (unchecked-get module "RawAlignSelf")
:align-content (unchecked-get module "RawAlignContent")
:justify-items (unchecked-get module "RawJustifyItems")
:justify-content (unchecked-get module "RawJustifyContent")
:justify-self (unchecked-get module "RawJustifySelf")
:wrap-type (unchecked-get module "RawWrapType")
:grid-track-type (unchecked-get module "RawGridTrackType")
:shadow-style (unchecked-get module "RawShadowStyle")
:stroke-style (unchecked-get module "RawStrokeStyle")
:stroke-cap (unchecked-get module "RawStrokeCap")
:shape-type (unchecked-get module "RawShapeType")
:constraint-h (unchecked-get module "RawConstraintH")
:constraint-v (unchecked-get module "RawConstraintV")
:sizing (unchecked-get module "RawSizing")
:vertical-align (unchecked-get module "RawVerticalAlign")
:fill-data (unchecked-get module "RawFillData")
:text-align (unchecked-get module "RawTextAlign")
:text-direction (unchecked-get module "RawTextDirection")
:text-decoration (unchecked-get module "RawTextDecoration")
:text-transform (unchecked-get module "RawTextTransform")
:segment-data (unchecked-get module "RawSegmentData")
:stroke-linecap (unchecked-get module "RawStrokeLineCap")
:stroke-linejoin (unchecked-get module "RawStrokeLineJoin")
:fill-rule (unchecked-get module "RawFillRule")}]
(set! wasm/serializers serializers)
(default-fn)))
(defonce module
(delay
(if (exists? js/dynamicImport)
(let [uri (cf/resolve-static-asset "js/render_wasm.js")]
(->> (js/dynamicImport (str uri))
(p/mcat (fn [module]
(let [default (unchecked-get module "default")
serializers #js{:blur-type (unchecked-get module "RawBlurType")
:blend-mode (unchecked-get module "RawBlendMode")
:bool-type (unchecked-get module "RawBoolType")
:font-style (unchecked-get module "RawFontStyle")
:flex-direction (unchecked-get module "RawFlexDirection")
:grid-direction (unchecked-get module "RawGridDirection")
:grow-type (unchecked-get module "RawGrowType")
:align-items (unchecked-get module "RawAlignItems")
:align-self (unchecked-get module "RawAlignSelf")
:align-content (unchecked-get module "RawAlignContent")
:justify-items (unchecked-get module "RawJustifyItems")
:justify-content (unchecked-get module "RawJustifyContent")
:justify-self (unchecked-get module "RawJustifySelf")
:wrap-type (unchecked-get module "RawWrapType")
:grid-track-type (unchecked-get module "RawGridTrackType")
:shadow-style (unchecked-get module "RawShadowStyle")
:stroke-style (unchecked-get module "RawStrokeStyle")
:stroke-cap (unchecked-get module "RawStrokeCap")
:shape-type (unchecked-get module "RawShapeType")
:constraint-h (unchecked-get module "RawConstraintH")
:constraint-v (unchecked-get module "RawConstraintV")
:sizing (unchecked-get module "RawSizing")
:vertical-align (unchecked-get module "RawVerticalAlign")
:fill-data (unchecked-get module "RawFillData")
:text-align (unchecked-get module "RawTextAlign")
:text-direction (unchecked-get module "RawTextDirection")
:text-decoration (unchecked-get module "RawTextDecoration")
:text-transform (unchecked-get module "RawTextTransform")
:segment-data (unchecked-get module "RawSegmentData")
:stroke-linecap (unchecked-get module "RawStrokeLineCap")
:stroke-linejoin (unchecked-get module "RawStrokeLineJoin")
:fill-rule (unchecked-get module "RawFillRule")}]
(set! wasm/serializers serializers)
(default))))
(p/fmap (fn [default]
(set! wasm/internal-module default)
true))
(p/merr (fn [cause]
(js/console.error cause)
(p/resolved false)))))
(p/mcat init-wasm-module)
(p/fmap
(fn [default]
(set! wasm/internal-module default)
true))
(p/merr
(fn [cause]
(js/console.error cause)
(p/resolved false)))))
(p/resolved false))))

View File

@@ -89,7 +89,7 @@
(defn init
"Return a initialized webworker instance."
[path on-error]
(let [instance (js/Worker. path)
(let [instance (js/Worker. path #js {:type "module"})
bus (rx/subject)
worker (Worker. instance (rx/to-observable bus))

View File

@@ -7,16 +7,24 @@
(ns app.worker.thumbnails
(:require
["react-dom/server" :as rds]
[app.common.data.macros :as dm]
[app.common.geom.rect :as grc]
[app.common.geom.shapes.bounds :as gsb]
[app.common.logging :as log]
[app.common.types.color :as cc]
[app.common.uri :as u]
[app.config :as cf]
[app.main.fonts :as fonts]
[app.main.render :as render]
[app.render-wasm.api :as wasm.api]
[app.render-wasm.wasm :as wasm]
[app.util.http :as http]
[app.worker.impl :as impl]
[beicon.v2.core :as rx]
[okulary.core :as l]
[rumext.v2 :as mf]))
[promesa.core :as p]
[rumext.v2 :as mf]
[shadow.esm :refer (dynamic-import)]))
(log/set-level! :trace)
@@ -42,11 +50,11 @@
:http-body body})))
(defn- request-data-for-thumbnail
[file-id revn]
[file-id revn strip-frames-with-thumbnails]
(let [path "api/main/methods/get-file-data-for-thumbnail"
params {:file-id file-id
:revn revn
:strip-frames-with-thumbnails true}
:strip-frames-with-thumbnails strip-frames-with-thumbnails}
request {:method :get
:uri (u/join cf/public-uri path)
:credentials "include"
@@ -86,5 +94,89 @@
(defmethod impl/handler :thumbnails/generate-for-file
[{:keys [file-id revn] :as message} _]
(->> (request-data-for-thumbnail file-id revn)
(->> (request-data-for-thumbnail file-id revn true)
(rx/map render-thumbnail)))
(def init-wasm
(delay
(let [uri (cf/resolve-static-asset "js/render_wasm.js")]
(-> (dynamic-import (str uri))
(p/then #(wasm.api/init-wasm-module %))
(p/then #(set! wasm/internal-module %))))))
(mf/defc svg-wrapper
[{:keys [data-uri background width height]}]
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:style {:width "100%"
:height "100%"
:background background}
:fill "none"
:viewBox (dm/str "0 0 " width " " height)}
[:image {:xlinkHref data-uri
:width width
:height height}]])
(defn blob->uri
[blob]
(.readAsDataURL (js/FileReaderSync.) blob))
(def thumbnail-aspect-ratio (/ 2 3))
(defmethod impl/handler :thumbnails/generate-for-file-wasm
[{:keys [file-id revn width] :as message} _]
(->> (rx/from @init-wasm)
(rx/mapcat #(request-data-for-thumbnail file-id revn false))
(rx/mapcat
(fn [{:keys [page] :as file}]
(rx/create
(fn [subs]
(try
(let [background-color (or (:background page) cc/canvas)
height (* width thumbnail-aspect-ratio)
canvas (js/OffscreenCanvas. width height)
init? (wasm.api/init-canvas-context canvas)]
(if init?
(let [objects (:objects page)
frame (some->> page :thumbnail-frame-id (get objects))
vbox (if frame
(-> (gsb/get-object-bounds objects frame)
(grc/fix-aspect-ratio thumbnail-aspect-ratio))
(render/calculate-dimensions objects thumbnail-aspect-ratio))
zoom (/ width (:width vbox))]
(wasm.api/initialize-viewport
objects zoom vbox background-color
(fn []
(if frame
(wasm.api/render-sync-shape (:id frame))
(wasm.api/render-sync))
(-> (.convertToBlob canvas)
(p/then
(fn [blob]
(let [data
(rds/renderToStaticMarkup
(mf/element
svg-wrapper
#js {:data-uri (blob->uri blob)
:width width
:height height
:background background-color}))]
(rx/push! subs {:data data :file-id file-id :revn revn}))))
(p/catch #(do (.error js/console %)
(rx/error! subs %)))
(p/finally #(rx/end! subs))))))
(do (rx/error! subs "Error loading webgl context")
(rx/end! subs)))
nil)
(catch :default err
(.error js/console err)
(rx/error! subs err)
(rx/end! subs)))))))))

View File

@@ -1,8 +1,16 @@
addToLibrary({
wapi_requestAnimationFrame: function wapi_requestAnimationFrame() {
return window.requestAnimationFrame(Module._process_animation_frame);
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
setTimeout(Module._process_animation_frame);
} else {
return window.requestAnimationFrame(Module._process_animation_frame);
}
},
wapi_cancelAnimationFrame: function wapi_cancelAnimationFrame(frameId) {
return window.cancelAnimationFrame(frameId);
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
clearTimeout(frameId);
} else {
return window.cancelAnimationFrame(frameId);
}
}
});

View File

@@ -149,6 +149,27 @@ pub extern "C" fn render(_: i32) {
});
}
#[no_mangle]
pub extern "C" fn render_sync() {
with_state_mut!(state, {
state.rebuild_tiles();
state
.render_sync(performance::get_time())
.expect("Error rendering");
});
}
#[no_mangle]
pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) {
with_state_mut!(state, {
let id = uuid_from_u32_quartet(a, b, c, d);
state.rebuild_tiles_from(Some(&id));
state
.render_sync_shape(&id, performance::get_time())
.expect("Error rendering");
});
}
#[no_mangle]
pub extern "C" fn render_from_cache(_: i32) {
with_state_mut!(state, {

View File

@@ -867,7 +867,13 @@ impl RenderState {
}
}
pub fn start_render_loop(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> {
pub fn start_render_loop(
&mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
sync_render: bool,
) -> Result<(), String> {
let scale = self.get_scale();
self.tile_viewbox.update(self.viewbox, scale);
@@ -917,20 +923,27 @@ impl RenderState {
self.current_tile = None;
self.render_in_progress = true;
self.apply_drawing_to_render_canvas(None);
self.process_animation_frame(tree, timestamp)?;
if sync_render {
self.render_shape_tree_sync(base_object, tree, timestamp)?;
} else {
self.process_animation_frame(base_object, tree, timestamp)?;
}
performance::end_measure!("start_render_loop");
Ok(())
}
pub fn process_animation_frame(
&mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(), String> {
performance::begin_measure!("process_animation_frame");
if self.render_in_progress {
if tree.len() != 0 {
self.render_shape_tree_partial(tree, timestamp)?;
self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
} else {
println!("Empty tree");
}
@@ -947,6 +960,22 @@ impl RenderState {
Ok(())
}
pub fn render_shape_tree_sync(
&mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(), String> {
if tree.len() != 0 {
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
} else {
println!("Empty tree");
}
self.flush_and_submit();
Ok(())
}
#[inline]
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
iteration % NODE_BATCH_THRESHOLD == 0
@@ -1215,6 +1244,7 @@ impl RenderState {
&mut self,
tree: ShapesPoolRef,
timestamp: i32,
allow_stop: bool,
) -> Result<(bool, bool), String> {
let mut iteration = 0;
let mut is_empty = true;
@@ -1495,7 +1525,7 @@ impl RenderState {
}
// We try to avoid doing too many calls to get_time
if self.should_stop_rendering(iteration, timestamp) {
if allow_stop && self.should_stop_rendering(iteration, timestamp) {
return Ok((is_empty, true));
}
iteration += 1;
@@ -1505,8 +1535,10 @@ impl RenderState {
pub fn render_shape_tree_partial(
&mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
allow_stop: bool,
) -> Result<(), String> {
let mut should_stop = false;
while !should_stop {
@@ -1532,7 +1564,7 @@ impl RenderState {
} else {
performance::begin_measure!("render_shape_tree::uncached");
let (is_empty, early_return) =
self.render_shape_tree_partial_uncached(tree, timestamp)?;
self.render_shape_tree_partial_uncached(tree, timestamp, allow_stop)?;
if early_return {
return Ok(());
@@ -1564,10 +1596,16 @@ impl RenderState {
.canvas(SurfaceId::Current)
.clear(self.background_color);
let Some(root) = tree.get(&Uuid::nil()) else {
return Err(String::from("Root shape not found"));
let root_ids = {
if let Some(shape_id) = base_object {
vec![*shape_id]
} else {
let Some(root) = tree.get(&Uuid::nil()) else {
return Err(String::from("Root shape not found"));
};
root.children_ids(false)
}
};
let root_ids = root.children_ids(false);
// If we finish processing every node rendering is complete
// let's check if there are more pending nodes
@@ -1711,13 +1749,19 @@ impl RenderState {
performance::end_measure!("rebuild_tiles_shallow");
}
pub fn rebuild_tiles(&mut self, tree: ShapesPoolRef) {
pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) {
performance::begin_measure!("rebuild_tiles");
self.tiles.invalidate();
let mut all_tiles = HashSet::<tiles::Tile>::new();
let mut nodes = vec![Uuid::nil()];
let mut nodes = {
if let Some(base_id) = base_id {
vec![*base_id]
} else {
vec![Uuid::nil()]
}
};
while let Some(shape_id) = nodes.pop() {
if let Some(shape) = tree.get(&shape_id) {
@@ -1737,7 +1781,6 @@ impl RenderState {
for tile in all_tiles {
self.remove_cached_tile(tile);
}
performance::end_measure!("rebuild_tiles");
}

View File

@@ -63,15 +63,27 @@ impl<'a> State<'a> {
self.render_state.render_from_cache(&self.shapes);
}
pub fn render_sync(&mut self, timestamp: i32) -> Result<(), String> {
self.render_state
.start_render_loop(None, &self.shapes, timestamp, true)?;
Ok(())
}
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<(), String> {
self.render_state
.start_render_loop(Some(id), &self.shapes, timestamp, true)?;
Ok(())
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
self.render_state
.start_render_loop(&self.shapes, timestamp)?;
.start_render_loop(None, &self.shapes, timestamp, false)?;
Ok(())
}
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> {
self.render_state
.process_animation_frame(&self.shapes, timestamp)?;
.process_animation_frame(None, &self.shapes, timestamp)?;
Ok(())
}
@@ -162,7 +174,11 @@ impl<'a> State<'a> {
}
pub fn rebuild_tiles(&mut self) {
self.render_state.rebuild_tiles(&self.shapes);
self.render_state.rebuild_tiles_from(&self.shapes, None);
}
pub fn rebuild_tiles_from(&mut self, base_id: Option<&Uuid>) {
self.render_state.rebuild_tiles_from(&self.shapes, base_id);
}
pub fn rebuild_touched_tiles(&mut self) {