🐛 Fix asynchronous content rendering

This commit is contained in:
Alejandro Alonso 2025-06-17 08:11:27 +02:00
parent d0425cabda
commit d71fa659d5
12 changed files with 166 additions and 100 deletions

View file

@ -57,7 +57,7 @@
([{:keys [id points selrect] :as shape} content] ([{:keys [id points selrect] :as shape} content]
(wasm.api/use-shape id) (wasm.api/use-shape id)
(wasm.api/set-shape-text content) (wasm.api/set-shape-text id content)
(let [dimension (wasm.api/text-dimensions) (let [dimension (wasm.api/text-dimensions)
resize-v resize-v
(gpt/point (gpt/point

View file

@ -301,7 +301,7 @@
(ted/get-editor-current-content) (ted/get-editor-current-content)
(ted/export-content))] (ted/export-content))]
(wasm.api/use-shape edition) (wasm.api/use-shape edition)
(wasm.api/set-shape-text-content content) (wasm.api/set-shape-text-content edition content)
(let [dimension (wasm.api/text-dimensions)] (let [dimension (wasm.api/text-dimensions)]
(st/emit! (dwt/resize-text-editor edition dimension)) (st/emit! (dwt/resize-text-editor edition dimension))
(wasm.api/clear-drawing-cache) (wasm.api/clear-drawing-cache)

View file

@ -190,9 +190,10 @@
(defn- get-string-length [string] (+ (count string) 1)) (defn- get-string-length [string] (+ (count string) 1))
(defn- fetch-image (defn- fetch-image
[id] [shape-id image-id]
(let [buffer (uuid/get-u32 id) (let [buffer-shape-id (uuid/get-u32 shape-id)
url (cf/resolve-file-media {:id id})] buffer-image-id (uuid/get-u32 image-id)
url (cf/resolve-file-media {:id image-id})]
{:key url {:key url
:callback #(->> (http/send! {:method :get :callback #(->> (http/send! {:method :get
:uri url :uri url
@ -206,10 +207,14 @@
data (js/Uint8Array. image)] data (js/Uint8Array. image)]
(.set heap data offset) (.set heap data offset)
(h/call wasm/internal-module "_store_image" (h/call wasm/internal-module "_store_image"
(aget buffer 0) (aget buffer-shape-id 0)
(aget buffer 1) (aget buffer-shape-id 1)
(aget buffer 2) (aget buffer-shape-id 2)
(aget buffer 3)) (aget buffer-shape-id 3)
(aget buffer-image-id 0)
(aget buffer-image-id 1)
(aget buffer-image-id 2)
(aget buffer-image-id 3))
true))))})) true))))}))
(defn- get-fill-images (defn- get-fill-images
@ -217,7 +222,7 @@
(filter :fill-image (:fills leaf))) (filter :fill-image (:fills leaf)))
(defn- process-fill-image (defn- process-fill-image
[fill] [shape-id fill]
(rx/from (rx/from
(when-let [image (:fill-image fill)] (when-let [image (:fill-image fill)]
(let [id (dm/get-prop image :id) (let [id (dm/get-prop image :id)
@ -228,19 +233,19 @@
(aget buffer 2) (aget buffer 2)
(aget buffer 3))] (aget buffer 3))]
(when (zero? cached-image?) (when (zero? cached-image?)
(fetch-image id)))))) (fetch-image shape-id id))))))
(defn set-shape-text-images (defn set-shape-text-images
[content] [shape-id content]
(let [paragraph-set (first (get content :children)) (let [paragraph-set (first (get content :children))
paragraphs (get paragraph-set :children)] paragraphs (get paragraph-set :children)]
(->> paragraphs (->> paragraphs
(mapcat :children) (mapcat :children)
(mapcat get-fill-images) (mapcat get-fill-images)
(map process-fill-image)))) (map #(process-fill-image shape-id %)))))
(defn set-shape-fills (defn set-shape-fills
[fills] [shape-id fills]
(when (not-empty? fills) (when (not-empty? fills)
(let [fills (take types.fill/MAX-FILLS fills) (let [fills (take types.fill/MAX-FILLS fills)
image-fills (filter :fill-image fills) image-fills (filter :fill-image fills)
@ -270,11 +275,11 @@
(aget buffer 2) (aget buffer 2)
(aget buffer 3))] (aget buffer 3))]
(when (zero? cached-image?) (when (zero? cached-image?)
(fetch-image id)))) (fetch-image shape-id id))))
image-fills)))) image-fills))))
(defn set-shape-strokes (defn set-shape-strokes
[strokes] [shape-id strokes]
(h/call wasm/internal-module "_clear_shape_strokes") (h/call wasm/internal-module "_clear_shape_strokes")
(keep (fn [stroke] (keep (fn [stroke]
(let [opacity (or (:stroke-opacity stroke) 1.0) (let [opacity (or (:stroke-opacity stroke) 1.0)
@ -301,15 +306,15 @@
(h/call wasm/internal-module "_add_shape_stroke_fill")) (h/call wasm/internal-module "_add_shape_stroke_fill"))
(some? image) (some? image)
(let [id (dm/get-prop image :id) (let [image-id (dm/get-prop image :id)
buffer (uuid/get-u32 id) buffer (uuid/get-u32 image-id)
cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))]
(sr-fills/write-image-fill! offset dview id opacity (sr-fills/write-image-fill! offset dview image-id opacity
(dm/get-prop image :width) (dm/get-prop image :width)
(dm/get-prop image :height)) (dm/get-prop image :height))
(h/call wasm/internal-module "_add_shape_stroke_fill") (h/call wasm/internal-module "_add_shape_stroke_fill")
(when (== cached-image? 0) (when (== cached-image? 0)
(fetch-image id))) (fetch-image shape-id image-id)))
(some? color) (some? color)
(do (do
@ -623,7 +628,7 @@
(declare propagate-apply) (declare propagate-apply)
(defn set-shape-text-content (defn set-shape-text-content
[content] [shape-id content]
(h/call wasm/internal-module "_clear_shape_text") (h/call wasm/internal-module "_clear_shape_text")
(let [paragraph-set (first (dm/get-prop content :children)) (let [paragraph-set (first (dm/get-prop content :children))
paragraphs (dm/get-prop paragraph-set :children) paragraphs (dm/get-prop paragraph-set :children)
@ -646,13 +651,13 @@
(-> fonts (-> fonts
(cond-> @emoji? (f/add-emoji-font)) (cond-> @emoji? (f/add-emoji-font))
(f/add-noto-fonts @languages))] (f/add-noto-fonts @languages))]
(f/store-fonts updated-fonts)))) (f/store-fonts shape-id updated-fonts))))
(defn set-shape-text (defn set-shape-text
[content] [shape-id content]
(concat (concat
(set-shape-text-images content) (set-shape-text-images shape-id content)
(set-shape-text-content content))) (set-shape-text-content shape-id content)))
(defn set-shape-grow-type (defn set-shape-grow-type
[grow-type] [grow-type]
@ -759,9 +764,9 @@
(set-grid-layout shape)) (set-grid-layout shape))
(let [pending (into [] (concat (let [pending (into [] (concat
(set-shape-text content) (set-shape-text id content)
(set-shape-fills fills) (set-shape-fills id fills)
(set-shape-strokes strokes)))] (set-shape-strokes id strokes)))]
(perf/end-measure "set-object") (perf/end-measure "set-object")
pending))) pending)))
@ -772,11 +777,10 @@
pending (-> (d/index-by :key :callback pending) vals)] pending (-> (d/index-by :key :callback pending) vals)]
(if (not-empty? pending) (if (not-empty? pending)
(->> (rx/from pending) (->> (rx/from pending)
(rx/mapcat (fn [callback] (callback))) (rx/merge-map (fn [callback] (callback)))
(rx/tap (fn [_] (request-render "set-objects")))
(rx/reduce conj []) (rx/reduce conj [])
(rx/subs! (fn [_] (rx/subs! (fn [_]
(clear-drawing-cache)
(request-render "set-objects")
(.dispatchEvent ^js js/document event)))) (.dispatchEvent ^js js/document event))))
(.dispatchEvent ^js js/document event)))) (.dispatchEvent ^js js/document event))))

View file

@ -77,18 +77,23 @@
;; IMPORTANT: It should be noted that only TTF fonts can be stored. ;; IMPORTANT: It should be noted that only TTF fonts can be stored.
(defn- store-font-buffer (defn- store-font-buffer
[font-data font-array-buffer emoji? fallback?] [shape-id font-data font-array-buffer emoji? fallback?]
(let [id-buffer (:family-id-buffer font-data) (let [font-id-buffer (:family-id-buffer font-data)
shape-id-buffer (uuid/get-u32 shape-id)
size (.-byteLength font-array-buffer) size (.-byteLength font-array-buffer)
ptr (h/call wasm/internal-module "_alloc_bytes" size) ptr (h/call wasm/internal-module "_alloc_bytes" size)
heap (gobj/get ^js wasm/internal-module "HEAPU8") heap (gobj/get ^js wasm/internal-module "HEAPU8")
mem (js/Uint8Array. (.-buffer heap) ptr size)] mem (js/Uint8Array. (.-buffer heap) ptr size)]
(.set mem (js/Uint8Array. font-array-buffer)) (.set mem (js/Uint8Array. font-array-buffer))
(h/call wasm/internal-module "_store_font" (h/call wasm/internal-module "_store_font"
(aget id-buffer 0) (aget shape-id-buffer 0)
(aget id-buffer 1) (aget shape-id-buffer 1)
(aget id-buffer 2) (aget shape-id-buffer 2)
(aget id-buffer 3) (aget shape-id-buffer 3)
(aget font-id-buffer 0)
(aget font-id-buffer 1)
(aget font-id-buffer 2)
(aget font-id-buffer 3)
(:weight font-data) (:weight font-data)
(:style font-data) (:style font-data)
emoji? emoji?
@ -96,13 +101,13 @@
true)) true))
(defn- fetch-font (defn- fetch-font
[font-data font-url emoji? fallback?] [shape-id font-data font-url emoji? fallback?]
{:key font-url {:key font-url
:callback #(->> (http/send! {:method :get :callback #(->> (http/send! {:method :get
:uri font-url :uri font-url
:response-type :buffer}) :response-type :buffer})
(rx/map (fn [{:keys [body]}] (rx/map (fn [{:keys [body]}]
(store-font-buffer font-data body emoji? fallback?))))}) (store-font-buffer shape-id font-data body emoji? fallback?))))})
(defn- google-font-ttf-url (defn- google-font-ttf-url
[font-id font-variant-id] [font-id font-variant-id]
@ -122,7 +127,7 @@
(dm/str (u/join cf/public-uri "fonts/" asset-id)))) (dm/str (u/join cf/public-uri "fonts/" asset-id))))
(defn- store-font-id (defn- store-font-id
[font-data asset-id emoji? fallback?] [shape-id font-data asset-id emoji? fallback?]
(when asset-id (when asset-id
(let [uri (font-id->ttf-url (:font-id font-data) asset-id (:font-variant-id font-data)) (let [uri (font-id->ttf-url (:font-id font-data) asset-id (:font-variant-id font-data))
id-buffer (uuid/get-u32 (:wasm-id font-data)) id-buffer (uuid/get-u32 (:wasm-id font-data))
@ -136,7 +141,7 @@
(:style font-data) (:style font-data)
emoji?))] emoji?))]
(when-not font-stored? (when-not font-stored?
(fetch-font font-data uri emoji? fallback?))))) (fetch-font shape-id font-data uri emoji? fallback?)))))
(defn serialize-font-style (defn serialize-font-style
[font-style] [font-style]
@ -167,7 +172,7 @@
(js/Number font-weight)) (js/Number font-weight))
(defn store-font (defn store-font
[font] [shape-id font]
(let [font-id (get font :font-id) (let [font-id (get font :font-id)
font-variant-id (get font :font-variant-id) font-variant-id (get font :font-variant-id)
emoji? (get font :is-emoji false) emoji? (get font :is-emoji false)
@ -184,11 +189,11 @@
:font-variant-id font-variant-id :font-variant-id font-variant-id
:style style :style style
:weight weight}] :weight weight}]
(store-font-id font-data asset-id emoji? fallback?))) (store-font-id shape-id font-data asset-id emoji? fallback?)))
(defn store-fonts (defn store-fonts
[fonts] [shape-id fonts]
(keep (fn [font] (store-font font)) fonts)) (keep (fn [font] (store-font shape-id font)) fonts))
(defn add-emoji-font (defn add-emoji-font

View file

@ -112,7 +112,8 @@
(defn set-wasm-single-attr! (defn set-wasm-single-attr!
[shape k] [shape k]
(let [v (get shape k)] (let [v (get shape k)
id (get shape :id)]
(case k (case k
:parent-id (api/set-parent-id v) :parent-id (api/set-parent-id v)
:type (api/set-shape-type v) :type (api/set-shape-type v)
@ -123,8 +124,8 @@
(api/set-shape-clip-content false)) (api/set-shape-clip-content false))
:rotation (api/set-shape-rotation v) :rotation (api/set-shape-rotation v)
:transform (api/set-shape-transform v) :transform (api/set-shape-transform v)
:fills (into [] (api/set-shape-fills v)) :fills (into [] (api/set-shape-fills id v))
:strokes (into [] (api/set-shape-strokes v)) :strokes (into [] (api/set-shape-strokes id v))
:blend-mode (api/set-shape-blend-mode v) :blend-mode (api/set-shape-blend-mode v)
:opacity (api/set-shape-opacity v) :opacity (api/set-shape-opacity v)
:hidden (api/set-shape-hidden v) :hidden (api/set-shape-hidden v)
@ -158,7 +159,7 @@
(api/set-shape-svg-raw-content (api/get-static-markup shape)) (api/set-shape-svg-raw-content (api/get-static-markup shape))
(= (:type shape) :text) (= (:type shape) :text)
(api/set-shape-text v)) (api/set-shape-text id v))
:grow-type :grow-type
(api/set-shape-grow-type v) (api/set-shape-grow-type v)

View file

@ -290,17 +290,31 @@ pub extern "C" fn set_children() {
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn store_image(a: u32, b: u32, c: u32, d: u32) { pub extern "C" fn store_image(
a1: u32,
b1: u32,
c1: u32,
d1: u32,
a2: u32,
b2: u32,
c2: u32,
d2: u32,
) {
with_state!(state, { with_state!(state, {
let id = uuid_from_u32_quartet(a, b, c, d); let image_id = uuid_from_u32_quartet(a2, b2, c2, d2);
let image_bytes = mem::bytes(); let image_bytes = mem::bytes();
if let Err(msg) = state.render_state().add_image(id, &image_bytes) { if let Err(msg) = state.render_state().add_image(image_id, &image_bytes) {
eprintln!("{}", msg); eprintln!("{}", msg);
} }
mem::free_bytes(); mem::free_bytes();
}); });
with_state!(state, {
let shape_id = uuid_from_u32_quartet(a1, b1, c1, d1);
state.update_tile_for_shape(shape_id);
});
} }
#[no_mangle] #[no_mangle]

View file

@ -220,14 +220,14 @@ impl RenderState {
let tiles = tiles::TileHashMap::new(); let tiles = tiles::TileHashMap::new();
RenderState { RenderState {
gpu_state, gpu_state: gpu_state.clone(),
options: RenderOptions::default(), options: RenderOptions::default(),
surfaces, surfaces,
fonts, fonts,
viewbox, viewbox,
cached_viewbox: Viewbox::new(0., 0.), cached_viewbox: Viewbox::new(0., 0.),
cached_target_snapshot: None, cached_target_snapshot: None,
images: ImageStore::new(), images: ImageStore::new(gpu_state.context.clone()),
background_color: skia::Color::TRANSPARENT, background_color: skia::Color::TRANSPARENT,
render_request_id: None, render_request_id: None,
render_in_progress: false, render_in_progress: false,
@ -257,7 +257,7 @@ impl RenderState {
} }
pub fn add_image(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> { pub fn add_image(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> {
self.images.add(id, image_data, &mut self.gpu_state.context) self.images.add(id, image_data)
} }
pub fn has_image(&mut self, id: &Uuid) -> bool { pub fn has_image(&mut self, id: &Uuid) -> bool {

View file

@ -1,6 +1,7 @@
use skia_safe::gpu::{self, gl::FramebufferInfo, gl::TextureInfo, DirectContext}; use skia_safe::gpu::{self, gl::FramebufferInfo, gl::TextureInfo, DirectContext};
use skia_safe::{self as skia, ISize}; use skia_safe::{self as skia, ISize};
#[derive(Debug, Clone)]
pub struct GpuState { pub struct GpuState {
pub context: DirectContext, pub context: DirectContext,
framebuffer_info: FramebufferInfo, framebuffer_info: FramebufferInfo,

View file

@ -7,60 +7,86 @@ use std::collections::HashMap;
pub type Image = skia::Image; pub type Image = skia::Image;
enum StoredImage {
Raw(Vec<u8>),
Gpu(Image),
}
pub struct ImageStore { pub struct ImageStore {
images: HashMap<Uuid, Image>, images: HashMap<Uuid, StoredImage>,
context: Box<DirectContext>,
} }
impl ImageStore { impl ImageStore {
pub fn new() -> Self { pub fn new(context: DirectContext) -> Self {
Self { Self {
images: HashMap::with_capacity(2048), images: HashMap::with_capacity(2048),
context: Box::new(context),
} }
} }
pub fn add( pub fn add(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> {
&mut self, if self.images.contains_key(&id) {
id: Uuid, return Err("Image already exists".to_string());
image_data: &[u8], }
context: &mut DirectContext,
) -> Result<(), String> {
let image_data = unsafe { skia::Data::new_bytes(image_data) };
let image = Image::from_encoded(image_data).ok_or("Error decoding image data")?;
let width = image.width(); self.images
let height = image.height(); .insert(id, StoredImage::Raw(image_data.to_vec()));
let image_info = skia::ImageInfo::new_n32_premul((width, height), None);
let mut surface = surfaces::render_target(
context,
Budgeted::Yes,
&image_info,
None,
None,
None,
None,
false,
)
.ok_or("Can't create GPU surface")?;
let dest_rect = MathRect::from_xywh(0.0, 0.0, width as f32, height as f32);
surface
.canvas()
.draw_image_rect(&image, None, dest_rect, &skia::Paint::default());
let gpu_image = surface.image_snapshot();
// This way we store the image as a texture
self.images.insert(id, gpu_image);
Ok(()) Ok(())
} }
pub fn contains(&mut self, id: &Uuid) -> bool { pub fn contains(&self, id: &Uuid) -> bool {
self.images.contains_key(id) self.images.contains_key(id)
} }
pub fn get(&self, id: &Uuid) -> Option<&Image> { pub fn get(&mut self, id: &Uuid) -> Option<&Image> {
self.images.get(id) // Use entry API to mutate the HashMap in-place if needed
if let Some(entry) = self.images.get_mut(id) {
match entry {
StoredImage::Gpu(ref img) => Some(img),
StoredImage::Raw(raw_data) => {
// Decode and upload to GPU
let data = unsafe { skia::Data::new_bytes(raw_data) };
let image = Image::from_encoded(data)?;
let width = image.width();
let height = image.height();
let image_info = skia::ImageInfo::new_n32_premul((width, height), None);
let mut surface = surfaces::render_target(
&mut self.context,
Budgeted::Yes,
&image_info,
None,
None,
None,
None,
false,
)?;
let dest_rect = MathRect::from_xywh(0.0, 0.0, width as f32, height as f32);
surface.canvas().draw_image_rect(
&image,
None,
dest_rect,
&skia::Paint::default(),
);
let gpu_image = surface.image_snapshot();
// Replace raw data with GPU image
*entry = StoredImage::Gpu(gpu_image);
if let StoredImage::Gpu(ref img) = entry {
Some(img)
} else {
None
}
}
}
} else {
None
}
} }
} }

View file

@ -388,13 +388,13 @@ fn draw_image_stroke_in_container(
image_fill: &ImageFill, image_fill: &ImageFill,
antialias: bool, antialias: bool,
) { ) {
let scale = render_state.get_scale();
let image = render_state.images.get(&image_fill.id()); let image = render_state.images.get(&image_fill.id());
if image.is_none() { if image.is_none() {
return; return;
} }
let size = image_fill.size(); let size = image_fill.size();
let scale = render_state.get_scale();
let canvas = render_state.surfaces.canvas(SurfaceId::Strokes); let canvas = render_state.surfaces.canvas(SurfaceId::Strokes);
let container = &shape.selrect; let container = &shape.selrect;
let path_transform = shape.to_path_transform(); let path_transform = shape.to_path_transform();

View file

@ -191,6 +191,12 @@ impl<'a> State<'a> {
} }
} }
pub fn update_tile_for_shape(&mut self, shape_id: Uuid) {
if let Some(shape) = self.shapes.get_mut(&shape_id) {
self.render_state.update_tile_for(shape);
}
}
pub fn update_tile_for_current_shape(&mut self) { pub fn update_tile_for_current_shape(&mut self) {
match self.current_shape.as_mut() { match self.current_shape.as_mut() {
Some(shape) => { Some(shape) => {

View file

@ -7,17 +7,21 @@ use crate::shapes::FontFamily;
#[no_mangle] #[no_mangle]
pub extern "C" fn store_font( pub extern "C" fn store_font(
a: u32, a1: u32,
b: u32, b1: u32,
c: u32, c1: u32,
d: u32, d1: u32,
a2: u32,
b2: u32,
c2: u32,
d2: u32,
weight: u32, weight: u32,
style: u8, style: u8,
is_emoji: bool, is_emoji: bool,
is_fallback: bool, is_fallback: bool,
) { ) {
with_state!(state, { with_state!(state, {
let id = uuid_from_u32_quartet(a, b, c, d); let id = uuid_from_u32_quartet(a2, b2, c2, d2);
let font_bytes = mem::bytes(); let font_bytes = mem::bytes();
let family = FontFamily::new(id, weight, style.into()); let family = FontFamily::new(id, weight, style.into());
@ -28,6 +32,11 @@ pub extern "C" fn store_font(
mem::free_bytes(); mem::free_bytes();
}); });
with_state!(state, {
let shape_id = uuid_from_u32_quartet(a1, b1, c1, d1);
state.update_tile_for_shape(shape_id);
});
} }
#[no_mangle] #[no_mangle]