Merge pull request #10191 from penpot/superalex-fix-viewer-webgl-issues

🐛 Fix some viewer webgl issues
This commit is contained in:
Alejandro Alonso 2026-06-15 18:54:32 +02:00 committed by GitHub
commit b06942c668
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 107 additions and 73 deletions

View File

@ -12,6 +12,7 @@
[app.render-wasm.wasm :as wasm]
[app.util.dom :as dom]
[app.util.timers :as ts]
[app.util.webapi :as webapi]
[goog.events :as events]
[promesa.core :as p]
[rumext.v2 :as mf]))
@ -30,14 +31,16 @@
(atom {:os-canvas nil
:page-key nil
:canvas-w 0
:canvas-h 0}))
:canvas-h 0
:dpr 1}))
(defn- reset-viewer-snapshot! []
(reset! viewer-snapshot
{:os-canvas nil
:page-key nil
:canvas-w 0
:canvas-h 0}))
:canvas-h 0
:dpr 1}))
(defn- draw-bitmap!
[canvas os-canvas object-id vis-w vis-h finish]
@ -135,9 +138,12 @@
(resolve nil))
vis-w (.-width canvas)
vis-h (.-height canvas)
dpr (wasm.api/get-dpr)
snap @viewer-snapshot
same-page? (and (some? page-key) (identical? page-key (:page-key snap)))
same-size? (and (= vis-w (:canvas-w snap)) (= vis-h (:canvas-h snap)))
same-size? (and (= vis-w (:canvas-w snap))
(= vis-h (:canvas-h snap))
(= dpr (:dpr snap)))
os (:os-canvas snap)
do-render! (fn [os-canvas]
(viewer-do-render! page-objects canvas os-canvas object-id
@ -151,7 +157,7 @@
(do
(when-not same-size?
(wasm.api/resize-offscreen-canvas! os vis-w vis-h)
(swap! viewer-snapshot assoc :canvas-w vis-w :canvas-h vis-h))
(swap! viewer-snapshot assoc :canvas-w vis-w :canvas-h vis-h :dpr dpr))
(do-render! os))
(let [os-canvas (js/OffscreenCanvas. vis-w vis-h)]
(when (wasm.api/initialized?)
@ -162,7 +168,8 @@
{:os-canvas os-canvas
:page-key page-key
:canvas-w vis-w
:canvas-h vis-h})
:canvas-h vis-h
:dpr dpr})
(wasm.api/initialize-viewport
page-objects scale size
:background-opacity 0
@ -192,50 +199,71 @@
(let [key (events/listen section "scroll" (fn [_] (sync!)))]
#(events/unlistenByKey key))))))))
(defn- use-viewer-dpr-key
"Bump a counter when browser zoom changes devicePixelRatio so WASM canvases
are resized like the workspace viewport."
[]
(let [dpr-key (mf/use-state 0)]
(mf/use-effect
(mf/deps [])
(fn []
(webapi/on-dpr-change (fn [_] (swap! dpr-key inc)))))
@dpr-key))
(defn- use-viewer-wasm-layers!
[page-id page-objects size scale frame-id not-fixed-ref fixed-ref
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids]
(mf/use-layout-effect
(mf/deps page-id page-objects size scale frame-id
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids)
(fn []
(when (get page-objects frame-id)
(->> @wasm.api/module
(p/fmap
(fn [ready?]
(when ready?
(let [not-fixed-canvas (mf/ref-val not-fixed-ref)
fixed-canvas (mf/ref-val fixed-ref)
passes
(cond-> []
not-fixed-canvas
(conj {:canvas not-fixed-canvas
:opts (cond-> {}
(seq not-fixed-include-ids)
(assoc :include-ids not-fixed-include-ids))})
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids delta dpr-key]
;; The hot-areas SVG shifts every object by `-(size + delta)` so the frame
;; `selrect` lands flush against the overlay snap side, ignoring the extra
;; padding reserved for shadows/blur/strokes. Bake the same `delta` into the
;; WASM view origin so the rendered canvas aligns with that SVG (otherwise it
;; appears offset by the shadow margin).
(let [render-size (-> size
(update :x + (:x delta 0))
(update :y + (:y delta 0)))]
(mf/use-layout-effect
(mf/deps page-id page-objects render-size scale frame-id dpr-key
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids)
(fn []
(when (get page-objects frame-id)
(->> @wasm.api/module
(p/fmap
(fn [ready?]
(when ready?
(let [not-fixed-canvas (mf/ref-val not-fixed-ref)
fixed-canvas (mf/ref-val fixed-ref)
passes
(cond-> []
not-fixed-canvas
(conj {:canvas not-fixed-canvas
:opts (cond-> {}
(seq not-fixed-include-ids)
(assoc :include-ids not-fixed-include-ids))})
(and fixed-canvas (seq fixed-include-ids))
(conj {:canvas fixed-canvas
:opts (cond-> {:include-ids fixed-include-ids}
(seq fixed-clear-fills-ids)
(assoc :clear-fills-ids fixed-clear-fills-ids))}))]
(when (seq passes)
(enqueue-wasm-render!
(fn []
(reduce (fn [chain {:keys [canvas opts]}]
(p/then chain
#(render-viewer-frame* page-id page-objects
canvas size scale frame-id
nil opts)))
(p/resolved nil)
passes)))))))))))))
(and fixed-canvas (seq fixed-include-ids))
(conj {:canvas fixed-canvas
:opts (cond-> {:include-ids fixed-include-ids}
(seq fixed-clear-fills-ids)
(assoc :clear-fills-ids fixed-clear-fills-ids))}))]
(when (seq passes)
(enqueue-wasm-render!
(fn []
(reduce (fn [chain {:keys [canvas opts]}]
(p/then chain
#(render-viewer-frame* page-id page-objects
canvas render-size scale frame-id
nil opts)))
(p/resolved nil)
passes))))))))))))))
(defn use-viewer-wasm-viewport!
"WASM render passes and fixed-scroll DOM sync for the viewer viewport."
[page-id page-objects size scale frame-id
not-fixed-ref fixed-ref fixed-scroll-layer-ref
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids]
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids delta]
(use-fixed-scroll-sync! (some? fixed-scroll-layer-ref) fixed-scroll-layer-ref)
(use-viewer-wasm-layers! page-id page-objects size scale frame-id
not-fixed-ref fixed-ref
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids))
(let [dpr-key (use-viewer-dpr-key)]
(use-viewer-wasm-layers! page-id page-objects size scale frame-id
not-fixed-ref fixed-ref
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids
delta dpr-key)))

View File

@ -12,12 +12,17 @@
[app.main.render-viewer-wasm :as rwv]
[app.main.ui.viewer.shapes :as shapes]
[app.main.ui.viewer.viewport-common :as vpc]
[app.render-wasm.api :as wasm.api]
[rumext.v2 :as mf]))
(defn- canvas-dimensions
"Physical canvas pixels (CSS layout size × DPR), matching the workspace WASM path."
[scale size]
{:width (js/Math.round (* scale (:base-width size)))
:height (js/Math.round (* scale (:base-height size)))})
(let [css-w (js/Math.round (* scale (:base-width size)))
css-h (js/Math.round (* scale (:base-height size)))
dpr (wasm.api/get-dpr)]
{:width (js/Math.round (* css-w dpr))
:height (js/Math.round (* css-h dpr))}))
(mf/defc wasm-hotspots-svg*
[{:keys [vbox size class prepared prepared-all prepared-frame shape-filter]}]
@ -145,7 +150,8 @@
page-id objects size scale frame-id
not-fixed-wasm-ref fixed-wasm-ref
(when has-fixed? fixed-layer-ref)
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids)
not-fixed-include-ids fixed-include-ids fixed-clear-fills-ids
delta)
[:& (mf/provider shapes/base-frame-ctx) {:value (get prepared-all (:id base))}
[:& (mf/provider shapes/frame-offset-ctx) {:value offset}

View File

@ -1920,15 +1920,6 @@
(h/call wasm/internal-module "_set_view_end")
(reset! view-interaction-active? false)))
(defn resize-offscreen-canvas!
"Resize a persistent OffscreenCanvas to new physical-pixel dimensions and
update the WASM render surfaces accordingly (via `_resize_viewbox`). The
design state (shape pool) is preserved so `set-objects` is not needed again."
[canvas new-physical-w new-physical-h]
(set! (.-width canvas) new-physical-w)
(set! (.-height canvas) new-physical-h)
(resize-viewbox (/ new-physical-w dpr) (/ new-physical-h dpr)))
(defn- debug-flags
[]
(cond-> 0
@ -1939,6 +1930,22 @@
(contains? cf/flags :render-wasm-info)
(bit-or 2r00000000000000000000000000001000)))
(defn set-render-options!
"Updates WASM render options with a new DPR value."
[new-dpr]
(h/call wasm/internal-module "_set_render_options" (debug-flags) new-dpr))
(defn resize-offscreen-canvas!
"Resize a persistent OffscreenCanvas to new physical-pixel dimensions and
update the WASM render surfaces accordingly (via `_resize_viewbox`). The
design state (shape pool) is preserved so `set-objects` is not needed again."
[canvas new-physical-w new-physical-h]
(let [dpr (get-dpr)]
(set! (.-width canvas) new-physical-w)
(set! (.-height canvas) new-physical-h)
(set-render-options! dpr)
(resize-viewbox (/ new-physical-w dpr) (/ new-physical-h dpr))))
(defn- wasm-get-numeric-value
[name]
(when-let [raw (let [p (rt/get-params @st/state)]
@ -1953,11 +1960,6 @@
(let [setter-name (str/concat "_set_" (name param-name))]
(h/call wasm/internal-module setter-name value))))
(defn set-render-options!
"Updates WASM render options with a new DPR value."
[new-dpr]
(h/call wasm/internal-module "_set_render_options" (debug-flags) new-dpr))
(defn- canvas-css-size
"Return canvas size in CSS pixels.

View File

@ -907,6 +907,14 @@ impl RenderState {
pub fn reset_canvas(&mut self) {
self.surfaces.reset(self.background_color);
self.surfaces.clear_backbuffer(self.background_color);
self.surfaces.clear_target(self.background_color);
}
/// Drop cached tile textures before a one-shot `render_sync_shape` render.
pub fn prepare_sync_shape_render(&mut self) {
self.surfaces.clear_tile_atlas();
self.surfaces.invalidate_tile_cache();
}
/// NOTE:
@ -1085,11 +1093,6 @@ impl RenderState {
self.include_filter.is_some()
}
fn reset_viewer_masked_surfaces(&mut self) {
self.surfaces.clear_backbuffer(self.background_color);
self.surfaces.clear_tile_atlas();
}
/// True when the shape or any descendant is whitelisted.
pub fn shape_visible_in_include_filter(&self, shape_id: &Uuid, tree: ShapesPoolRef) -> bool {
let Some(ref include) = self.include_filter else {
@ -2192,13 +2195,6 @@ impl RenderState {
self.interactive_target_seeded = false;
}
// Viewer fixed-scroll passes reuse the same WASM context; `reset` does not
// clear Backbuffer, so pass 2 would otherwise keep pass-1 pixels in regions
// that render no shapes for the current mask. Target is cleared in present_frame.
if self.viewer_masked_pass() {
self.reset_viewer_masked_surfaces();
}
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::InnerShadows as u32

View File

@ -80,7 +80,9 @@ impl State {
}
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<FrameType> {
get_render_state().start_render_loop(Some(id), &self.shapes, timestamp, true)
let render_state = get_render_state();
render_state.prepare_sync_shape_render();
render_state.start_render_loop(Some(id), &self.shapes, timestamp, true)
}
pub fn render_shape_pixels(