mirror of
https://github.com/penpot/penpot.git
synced 2026-09-06 20:18:39 +00:00
✨ Implement WebGL context restoring (#9317)
* ✨ Implement asset re-uploading to wasm * ✨ Show toast instead of error screen when webgl context is lost * 🎉 Recover context after webgl context restored event * 🎉 Set Read-only mode when the context has been lost * ✨ Disable scroll & zoom when context loss * ✨ Fix stale reload payload * ✨ Use existing debounce util to take screenshots * ✨ Implement design / ux specs * ✨ Fix playwright test by looking for toast, not error page
This commit is contained in:
parent
cd4a4da0f2
commit
0639ca53de
@ -16,7 +16,7 @@ test.skip("BUG 10867 - Crash when loading comments", async ({ page }) => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("BUG 13541 - Shows error page when WebGL context is lost", async ({
|
test("Shows toast when WebGL context is lost", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const workspacePage = new WasmWorkspacePage(page);
|
const workspacePage = new WasmWorkspacePage(page);
|
||||||
@ -31,12 +31,9 @@ test("BUG 13541 - Shows error page when WebGL context is lost", async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText("Oops! The canvas context was lost"),
|
page.getByText("WebGL context was lost"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(page.getByRole("button", { name: "Refresh" })).toBeVisible();
|
||||||
page.getByText("WebGL has stopped working"),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(page.getByText("Reload page")).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
(ns app.main.data.render-wasm
|
(ns app.main.data.render-wasm
|
||||||
(:require
|
(:require
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(defn context-lost
|
(defn context-lost
|
||||||
@ -7,11 +8,37 @@
|
|||||||
(ptk/reify ::context-lost
|
(ptk/reify ::context-lost
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update state :render-state #(assoc % :lost true)))))
|
(let [already-lost? (get-in state [:render-state :lost])
|
||||||
|
prev-read-only? (get-in state [:workspace-global :read-only?])
|
||||||
|
prev-options-mode (get-in state [:workspace-global :options-mode])]
|
||||||
|
(-> state
|
||||||
|
(update :render-state
|
||||||
|
(fn [render-state]
|
||||||
|
(cond-> (assoc render-state :lost true)
|
||||||
|
(not already-lost?)
|
||||||
|
(assoc :pre-context-lost-read-only? prev-read-only?
|
||||||
|
:pre-context-lost-options-mode prev-options-mode))))
|
||||||
|
(assoc-in [:workspace-global :options-mode] :inspect)
|
||||||
|
(assoc-in [:workspace-global :read-only?] true))))
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/of :interrupt))))
|
||||||
|
|
||||||
(defn context-restored
|
(defn context-restored
|
||||||
[]
|
[]
|
||||||
(ptk/reify ::context-restored
|
(ptk/reify ::context-restored
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update state :render-state #(dissoc % :lost)))))
|
(let [restored-read-only? (get-in state [:render-state :pre-context-lost-read-only?]
|
||||||
|
(get-in state [:workspace-global :read-only?]))
|
||||||
|
restored-options-mode (get-in state [:render-state :pre-context-lost-options-mode]
|
||||||
|
(get-in state [:workspace-global :options-mode]))]
|
||||||
|
(-> state
|
||||||
|
(update :render-state #(dissoc % :lost
|
||||||
|
:pre-context-lost-read-only?
|
||||||
|
:pre-context-lost-options-mode))
|
||||||
|
(assoc-in [:workspace-global :options-mode] restored-options-mode)
|
||||||
|
(assoc-in [:workspace-global :read-only?] restored-read-only?))))
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(rx/of :interrupt))))
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.streams :as ms]
|
[app.main.streams :as ms]
|
||||||
[app.render-wasm.api :as wasm.api]
|
[app.render-wasm.api :as wasm.api]
|
||||||
|
[app.render-wasm.gesture :as wasm-gesture]
|
||||||
[app.render-wasm.shape :as wasm.shape]
|
[app.render-wasm.shape :as wasm.shape]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
@ -44,16 +45,17 @@
|
|||||||
;; Paired with `set-modifiers-start` / `set-modifiers-end` so the
|
;; Paired with `set-modifiers-start` / `set-modifiers-end` so the
|
||||||
;; native side only toggles once per gesture, regardless of how many
|
;; native side only toggles once per gesture, regardless of how many
|
||||||
;; `set-wasm-modifiers` calls fire in between.
|
;; `set-wasm-modifiers` calls fire in between.
|
||||||
(defonce ^:private interactive-transform-active? (atom false))
|
;; State lives in `app.render-wasm.gesture` so `reload-renderer!` can reset it after
|
||||||
|
;; `_clean_up` without an api ↔ modifiers circular dependency.
|
||||||
|
|
||||||
(defn- ensure-interactive-transform-start!
|
(defn- ensure-interactive-transform-start!
|
||||||
[]
|
[]
|
||||||
(when (compare-and-set! interactive-transform-active? false true)
|
(when (wasm-gesture/try-begin-interactive-transform!)
|
||||||
(wasm.api/set-modifiers-start)))
|
(wasm.api/set-modifiers-start)))
|
||||||
|
|
||||||
(defn- ensure-interactive-transform-end!
|
(defn- ensure-interactive-transform-end!
|
||||||
[]
|
[]
|
||||||
(when (compare-and-set! interactive-transform-active? true false)
|
(when (wasm-gesture/try-end-interactive-transform!)
|
||||||
(wasm.api/set-modifiers-end)))
|
(wasm.api/set-modifiers-end)))
|
||||||
|
|
||||||
(def ^:private transform-attrs
|
(def ^:private transform-attrs
|
||||||
|
|||||||
@ -20,6 +20,10 @@
|
|||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
|
(defn- render-context-lost?
|
||||||
|
[state]
|
||||||
|
(true? (get-in state [:render-state :lost])))
|
||||||
|
|
||||||
(defn initialize-viewport
|
(defn initialize-viewport
|
||||||
[{:keys [width height] :as size}]
|
[{:keys [width height] :as size}]
|
||||||
|
|
||||||
@ -101,7 +105,9 @@
|
|||||||
(ptk/reify ::update-viewport-position-center
|
(ptk/reify ::update-viewport-position-center
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update state :workspace-local calculate-centered-viewbox position))))
|
(if (render-context-lost? state)
|
||||||
|
state
|
||||||
|
(update state :workspace-local calculate-centered-viewbox position)))))
|
||||||
|
|
||||||
(defn update-viewport-position
|
(defn update-viewport-position
|
||||||
[{:keys [x y] :or {x identity y identity}}]
|
[{:keys [x y] :or {x identity y identity}}]
|
||||||
@ -118,11 +124,13 @@
|
|||||||
|
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update-in state [:workspace-local :vbox]
|
(if (render-context-lost? state)
|
||||||
(fn [vbox]
|
state
|
||||||
(-> vbox
|
(update-in state [:workspace-local :vbox]
|
||||||
(update :x x)
|
(fn [vbox]
|
||||||
(update :y y)))))))
|
(-> vbox
|
||||||
|
(update :x x)
|
||||||
|
(update :y y))))))))
|
||||||
|
|
||||||
(defn update-viewport-size
|
(defn update-viewport-size
|
||||||
[resize-type {:keys [width height] :as size}]
|
[resize-type {:keys [width height] :as size}]
|
||||||
@ -172,7 +180,8 @@
|
|||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
|
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
|
||||||
zoom (get-in state [:workspace-local :zoom])]
|
zoom (get-in state [:workspace-local :zoom])]
|
||||||
(when-not (get-in state [:workspace-local :panning])
|
(when (and (not (render-context-lost? state))
|
||||||
|
(not (get-in state [:workspace-local :panning])))
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of #(-> % (assoc-in [:workspace-local :panning] true)))
|
(rx/of #(-> % (assoc-in [:workspace-local :panning] true)))
|
||||||
(->> stream
|
(->> stream
|
||||||
|
|||||||
@ -21,6 +21,10 @@
|
|||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
|
(defn- render-context-lost?
|
||||||
|
[state]
|
||||||
|
(true? (get-in state [:render-state :lost])))
|
||||||
|
|
||||||
(defn impl-update-zoom
|
(defn impl-update-zoom
|
||||||
[{:keys [vbox] :as local} center zoom]
|
[{:keys [vbox] :as local} center zoom]
|
||||||
(let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom)
|
(let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom)
|
||||||
@ -43,9 +47,11 @@
|
|||||||
|
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [center (if (= center ::auto) @ms/mouse-position center)]
|
(if (render-context-lost? state)
|
||||||
(update state :workspace-local
|
state
|
||||||
#(impl-update-zoom % center (fn [z] (min (* z 1.3) 200)))))))))
|
(let [center (if (= center ::auto) @ms/mouse-position center)]
|
||||||
|
(update state :workspace-local
|
||||||
|
#(impl-update-zoom % center (fn [z] (min (* z 1.3) 200))))))))))
|
||||||
|
|
||||||
(defn decrease-zoom
|
(defn decrease-zoom
|
||||||
([]
|
([]
|
||||||
@ -56,9 +62,11 @@
|
|||||||
|
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [center (if (= center ::auto) @ms/mouse-position center)]
|
(if (render-context-lost? state)
|
||||||
(update state :workspace-local
|
state
|
||||||
#(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01)))))))))
|
(let [center (if (= center ::auto) @ms/mouse-position center)]
|
||||||
|
(update state :workspace-local
|
||||||
|
#(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))))))
|
||||||
|
|
||||||
(defn set-zoom
|
(defn set-zoom
|
||||||
([scale]
|
([scale]
|
||||||
@ -69,68 +77,76 @@
|
|||||||
|
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [vp (dm/get-in state [:workspace-local :vbox])
|
(if (render-context-lost? state)
|
||||||
x (+ (:x vp) (/ (:width vp) 2))
|
state
|
||||||
y (+ (:y vp) (/ (:height vp) 2))
|
(let [vp (dm/get-in state [:workspace-local :vbox])
|
||||||
center (d/nilv center (gpt/point x y))]
|
x (+ (:x vp) (/ (:width vp) 2))
|
||||||
(update state :workspace-local
|
y (+ (:y vp) (/ (:height vp) 2))
|
||||||
#(impl-update-zoom % center (fn [z] (-> (* z scale)
|
center (d/nilv center (gpt/point x y))]
|
||||||
(max 0.01)
|
(update state :workspace-local
|
||||||
(min 200))))))))))
|
#(impl-update-zoom % center (fn [z] (-> (* z scale)
|
||||||
|
(max 0.01)
|
||||||
|
(min 200)))))))))))
|
||||||
|
|
||||||
(def reset-zoom
|
(def reset-zoom
|
||||||
(ptk/reify ::reset-zoom
|
(ptk/reify ::reset-zoom
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update state :workspace-local
|
(if (render-context-lost? state)
|
||||||
#(impl-update-zoom % nil 1)))))
|
state
|
||||||
|
(update state :workspace-local
|
||||||
|
#(impl-update-zoom % nil 1))))))
|
||||||
|
|
||||||
(def zoom-to-fit-all
|
(def zoom-to-fit-all
|
||||||
(ptk/reify ::zoom-to-fit-all
|
(ptk/reify ::zoom-to-fit-all
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [page-id (:current-page-id state)
|
(if (render-context-lost? state)
|
||||||
objects (dsh/lookup-page-objects state page-id)
|
state
|
||||||
shapes (cfh/get-immediate-children objects)
|
(let [page-id (:current-page-id state)
|
||||||
srect (gsh/shapes->rect shapes)]
|
objects (dsh/lookup-page-objects state page-id)
|
||||||
(if (empty? shapes)
|
shapes (cfh/get-immediate-children objects)
|
||||||
state
|
srect (gsh/shapes->rect shapes)]
|
||||||
(update state :workspace-local
|
(if (empty? shapes)
|
||||||
(fn [{:keys [vport] :as local}]
|
state
|
||||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 160 :min-zoom 0.01})
|
|
||||||
zoom (/ (:width vport) (:width srect))]
|
|
||||||
(-> local
|
|
||||||
(assoc :zoom zoom)
|
|
||||||
(assoc :zoom-inverse (/ 1 zoom))
|
|
||||||
(update :vbox merge srect))))))))))
|
|
||||||
|
|
||||||
(def zoom-to-selected-shape
|
|
||||||
(ptk/reify ::zoom-to-selected-shape
|
|
||||||
ptk/UpdateEvent
|
|
||||||
(update [_ state]
|
|
||||||
(let [selected (dsh/lookup-selected state)]
|
|
||||||
(if (empty? selected)
|
|
||||||
state
|
|
||||||
(let [page-id (:current-page-id state)
|
|
||||||
objects (dsh/lookup-page-objects state page-id)
|
|
||||||
srect (->> selected
|
|
||||||
(map #(get objects %))
|
|
||||||
(gsh/shapes->rect))]
|
|
||||||
(update state :workspace-local
|
(update state :workspace-local
|
||||||
(fn [{:keys [vport] :as local}]
|
(fn [{:keys [vport] :as local}]
|
||||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 40 :min-zoom 0.01})
|
(let [srect (gal/adjust-to-viewport vport srect {:padding 160 :min-zoom 0.01})
|
||||||
zoom (/ (:width vport) (:width srect))]
|
zoom (/ (:width vport) (:width srect))]
|
||||||
(-> local
|
(-> local
|
||||||
(assoc :zoom zoom)
|
(assoc :zoom zoom)
|
||||||
(assoc :zoom-inverse (/ 1 zoom))
|
(assoc :zoom-inverse (/ 1 zoom))
|
||||||
(update :vbox merge srect)))))))))))
|
(update :vbox merge srect)))))))))))
|
||||||
|
|
||||||
|
(def zoom-to-selected-shape
|
||||||
|
(ptk/reify ::zoom-to-selected-shape
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(if (render-context-lost? state)
|
||||||
|
state
|
||||||
|
(let [selected (dsh/lookup-selected state)]
|
||||||
|
(if (empty? selected)
|
||||||
|
state
|
||||||
|
(let [page-id (:current-page-id state)
|
||||||
|
objects (dsh/lookup-page-objects state page-id)
|
||||||
|
srect (->> selected
|
||||||
|
(map #(get objects %))
|
||||||
|
(gsh/shapes->rect))]
|
||||||
|
(update state :workspace-local
|
||||||
|
(fn [{:keys [vport] :as local}]
|
||||||
|
(let [srect (gal/adjust-to-viewport vport srect {:padding 40 :min-zoom 0.01})
|
||||||
|
zoom (/ (:width vport) (:width srect))]
|
||||||
|
(-> local
|
||||||
|
(assoc :zoom zoom)
|
||||||
|
(assoc :zoom-inverse (/ 1 zoom))
|
||||||
|
(update :vbox merge srect))))))))))))
|
||||||
|
|
||||||
(defn fit-to-shapes
|
(defn fit-to-shapes
|
||||||
[ids]
|
[ids]
|
||||||
(ptk/reify ::fit-to-shapes
|
(ptk/reify ::fit-to-shapes
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(if (empty? ids)
|
(if (or (render-context-lost? state) (empty? ids))
|
||||||
state
|
state
|
||||||
(let [page-id (:current-page-id state)
|
(let [page-id (:current-page-id state)
|
||||||
objects (dsh/lookup-page-objects state page-id)
|
objects (dsh/lookup-page-objects state page-id)
|
||||||
@ -155,7 +171,8 @@
|
|||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-zooming)))]
|
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-zooming)))]
|
||||||
(when-not (get-in state [:workspace-local :zooming])
|
(when (and (not (render-context-lost? state))
|
||||||
|
(not (get-in state [:workspace-local :zooming])))
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
|
(rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
|
||||||
(->> stream
|
(->> stream
|
||||||
|
|||||||
@ -221,9 +221,11 @@
|
|||||||
(when-let [cause (::instance error)]
|
(when-let [cause (::instance error)]
|
||||||
(ex/print-throwable cause)
|
(ex/print-throwable cause)
|
||||||
(let [code (get error :code)]
|
(let [code (get error :code)]
|
||||||
(if (or (= code :panic)
|
(cond
|
||||||
(= code :webgl-context-lost))
|
(= code :panic)
|
||||||
(st/emit! (rt/assign-exception error))
|
(st/emit! (rt/assign-exception error))
|
||||||
|
|
||||||
|
:else
|
||||||
(flash :type :handled :cause cause)))))
|
(flash :type :handled :cause cause)))))
|
||||||
|
|
||||||
;; We receive a explicit authentication error; If the uri is for
|
;; We receive a explicit authentication error; If the uri is for
|
||||||
|
|||||||
@ -491,13 +491,6 @@
|
|||||||
:service-unavailable
|
:service-unavailable
|
||||||
[:> service-unavailable*]
|
[:> service-unavailable*]
|
||||||
|
|
||||||
:wasm-error
|
|
||||||
(case (get data :code)
|
|
||||||
:webgl-context-lost
|
|
||||||
[:> webgl-context-lost*]
|
|
||||||
|
|
||||||
[:> internal-error* props])
|
|
||||||
|
|
||||||
[:> internal-error* props])))
|
[:> internal-error* props])))
|
||||||
|
|
||||||
(mf/defc context-wrapper*
|
(mf/defc context-wrapper*
|
||||||
|
|||||||
@ -219,6 +219,7 @@
|
|||||||
[{:keys [objects selected page-id file-id on-change-section on-expand]}]
|
[{:keys [objects selected page-id file-id on-change-section on-expand]}]
|
||||||
(let [permissions
|
(let [permissions
|
||||||
(mf/use-ctx ctx/permissions)
|
(mf/use-ctx ctx/permissions)
|
||||||
|
render-context-lost? (mf/deref refs/render-context-lost?)
|
||||||
|
|
||||||
options-mode
|
options-mode
|
||||||
(mf/deref refs/options-mode-global)
|
(mf/deref refs/options-mode-global)
|
||||||
@ -228,7 +229,7 @@
|
|||||||
(sequence (keep (d/getf objects)) selected))]
|
(sequence (keep (d/getf objects)) selected))]
|
||||||
|
|
||||||
[:div {:class (stl/css :tool-window)}
|
[:div {:class (stl/css :tool-window)}
|
||||||
(if (:can-edit permissions)
|
(if (and (:can-edit permissions) (not render-context-lost?))
|
||||||
[:> tab-switcher* {:tabs options-tabs
|
[:> tab-switcher* {:tabs options-tabs
|
||||||
:on-change on-option-tab-change
|
:on-change on-option-tab-change
|
||||||
:selected (name options-mode)
|
:selected (name options-mode)
|
||||||
|
|||||||
@ -9,7 +9,9 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.common :as dwc]
|
[app.main.data.workspace.common :as dwc]
|
||||||
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
[app.main.ui.workspace.viewport.grid-layout-editor :refer [grid-edition-actions]]
|
[app.main.ui.workspace.viewport.grid-layout-editor :refer [grid-edition-actions]]
|
||||||
[app.main.ui.workspace.viewport.path-actions :refer [path-actions*]]
|
[app.main.ui.workspace.viewport.path-actions :refer [path-actions*]]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
@ -23,18 +25,24 @@
|
|||||||
[]
|
[]
|
||||||
(let [on-close
|
(let [on-close
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
#(st/emit! :interrupt
|
(fn []
|
||||||
(dw/set-options-mode :design)
|
(st/emit! :interrupt
|
||||||
(dwc/set-workspace-read-only false)))]
|
(dw/set-options-mode :design)
|
||||||
|
(dwc/set-workspace-read-only false))))
|
||||||
|
render-context-lost? (mf/deref refs/render-context-lost?)]
|
||||||
[:div {:class (stl/css :viewport-actions)}
|
[:div {:class (stl/css :viewport-actions)}
|
||||||
[:div {:class (stl/css :viewport-actions-container)}
|
[:div {:class (stl/css :viewport-actions-container)}
|
||||||
[:div {:class (stl/css :viewport-actions-title)}
|
[:div {:class (stl/css :viewport-actions-title)}
|
||||||
[:> i18n/tr-html*
|
[:> i18n/tr-html*
|
||||||
{:tag-name "span"
|
{:tag-name "span"
|
||||||
:content (tr "workspace.top-bar.view-only")}]]
|
:content (tr (if render-context-lost?
|
||||||
[:button {:class (stl/css :done-btn)
|
"workspace.top-bar.webgl-context-lost"
|
||||||
:on-click on-close}
|
"workspace.top-bar.view-only"))}]]
|
||||||
(tr "workspace.top-bar.read-only.done")]]]))
|
(if render-context-lost?
|
||||||
|
[:> button* {:variant "primary" :on-click (fn [] (js/location.reload))}
|
||||||
|
(tr "workspace.top-bar.webgl-context-lost.reload")]
|
||||||
|
[:> button* {:on-click on-close}
|
||||||
|
(tr "workspace.top-bar.read-only.done")])]]))
|
||||||
|
|
||||||
(mf/defc path-edition-bar*
|
(mf/defc path-edition-bar*
|
||||||
[{:keys [layout edit-path-state shape]}]
|
[{:keys [layout edit-path-state shape]}]
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
top: calc(var(--actions-toolbar-position-y) + var(--actions-toolbar-offset-y));
|
top: calc(var(--actions-toolbar-position-y) + var(--actions-toolbar-offset-y));
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
z-index: deprecated.$z-index-20;
|
z-index: deprecated.$z-index-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,11 +32,10 @@
|
|||||||
box-shadow: 0 0 deprecated.$s-12 0 var(--menu-shadow-color);
|
box-shadow: 0 0 deprecated.$s-12 0 var(--menu-shadow-color);
|
||||||
gap: deprecated.$s-8;
|
gap: deprecated.$s-8;
|
||||||
height: deprecated.$s-48;
|
height: deprecated.$s-48;
|
||||||
margin-left: -50%;
|
|
||||||
padding: deprecated.$s-8;
|
padding: deprecated.$s-8;
|
||||||
cursor: initial;
|
cursor: initial;
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
width: deprecated.$s-400;
|
min-width: deprecated.$s-400;
|
||||||
border: deprecated.$s-2 solid var(--panel-border-color);
|
border: deprecated.$s-2 solid var(--panel-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +44,7 @@
|
|||||||
font-size: deprecated.$fs-12;
|
font-size: deprecated.$fs-12;
|
||||||
color: var(--color-foreground-secondary);
|
color: var(--color-foreground-secondary);
|
||||||
padding-left: deprecated.$s-8;
|
padding-left: deprecated.$s-8;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.done-btn {
|
.done-btn {
|
||||||
|
|||||||
@ -254,6 +254,7 @@
|
|||||||
|
|
||||||
;; True when we are opening a new file or switching to a new page
|
;; True when we are opening a new file or switching to a new page
|
||||||
page-transition? (mf/deref wasm.api/page-transition?)
|
page-transition? (mf/deref wasm.api/page-transition?)
|
||||||
|
context-loss-overlay? (mf/deref wasm.api/context-loss-overlay?)
|
||||||
|
|
||||||
on-click (actions/on-click hover selected edition path-drawing? drawing-tool space? selrect z?)
|
on-click (actions/on-click hover selected edition path-drawing? drawing-tool space? selrect z?)
|
||||||
on-context-menu (actions/on-context-menu hover hover-ids read-only?)
|
on-context-menu (actions/on-context-menu hover hover-ids read-only?)
|
||||||
@ -544,8 +545,9 @@
|
|||||||
:style {:background-color background
|
:style {:background-color background
|
||||||
:pointer-events "none"}}]
|
:pointer-events "none"}}]
|
||||||
|
|
||||||
;; Show the transition image when we are opening a new file or switching to a new page
|
;; Show the transition image when switching pages or recovering from WebGL context loss.
|
||||||
(when (and page-transition? (some? transition-image-url))
|
(when (and (or page-transition? context-loss-overlay?)
|
||||||
|
(some? transition-image-url))
|
||||||
(let [src transition-image-url]
|
(let [src transition-image-url]
|
||||||
[:img {:data-testid "canvas-wasm-transition"
|
[:img {:data-testid "canvas-wasm-transition"
|
||||||
:src src
|
:src src
|
||||||
@ -556,6 +558,7 @@
|
|||||||
:height "100%"
|
:height "100%"
|
||||||
:object-fit "cover"
|
:object-fit "cover"
|
||||||
:pointer-events "none"
|
:pointer-events "none"
|
||||||
|
;; use (when page-transition? "blur(4px)") if we don't want the blur on context loss
|
||||||
:filter "blur(4px)"}}]))
|
:filter "blur(4px)"}}]))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.files.focus :as cpf]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.logging :as log]
|
[app.common.logging :as log]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
@ -22,6 +23,9 @@
|
|||||||
[app.common.types.text :as txt]
|
[app.common.types.text :as txt]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
[app.main.data.helpers :as dsh]
|
||||||
|
[app.main.data.notifications :as ntf]
|
||||||
|
[app.main.data.render-wasm :as drw]
|
||||||
[app.main.data.workspace.texts-v3 :as texts]
|
[app.main.data.workspace.texts-v3 :as texts]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
@ -32,6 +36,7 @@
|
|||||||
[app.render-wasm.api.texts :as t]
|
[app.render-wasm.api.texts :as t]
|
||||||
[app.render-wasm.api.webgl :as webgl]
|
[app.render-wasm.api.webgl :as webgl]
|
||||||
[app.render-wasm.deserializers :as dr]
|
[app.render-wasm.deserializers :as dr]
|
||||||
|
[app.render-wasm.gesture :as wasm-gesture]
|
||||||
[app.render-wasm.helpers :as h]
|
[app.render-wasm.helpers :as h]
|
||||||
[app.render-wasm.mem :as mem]
|
[app.render-wasm.mem :as mem]
|
||||||
[app.render-wasm.mem.heap32 :as mem.h32]
|
[app.render-wasm.mem.heap32 :as mem.h32]
|
||||||
@ -45,6 +50,7 @@
|
|||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.functions :as fns]
|
[app.util.functions :as fns]
|
||||||
[app.util.globals :as ug]
|
[app.util.globals :as ug]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
[app.util.modules :as mod]
|
[app.util.modules :as mod]
|
||||||
[app.util.text.content :as tc]
|
[app.util.text.content :as tc]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
@ -69,11 +75,14 @@
|
|||||||
;; - `transition-tiles-handler*`: the currently installed DOM event handler for
|
;; - `transition-tiles-handler*`: the currently installed DOM event handler for
|
||||||
;; `penpot:wasm:tiles-complete`, so we can remove/replace it safely.
|
;; `penpot:wasm:tiles-complete`, so we can remove/replace it safely.
|
||||||
(defonce page-transition? (atom false))
|
(defonce page-transition? (atom false))
|
||||||
|
(defonce context-loss-overlay? (atom false))
|
||||||
(defonce transition-image-url* (atom nil))
|
(defonce transition-image-url* (atom nil))
|
||||||
(defonce transition-epoch* (atom 0))
|
(defonce transition-epoch* (atom 0))
|
||||||
(defonce transition-tiles-handler* (atom nil))
|
(defonce transition-tiles-handler* (atom nil))
|
||||||
|
(defonce snapshot-tiles-handler* (atom nil))
|
||||||
|
|
||||||
(def ^:private transition-blur-css "blur(4px)")
|
(def ^:private transition-blur-css "blur(4px)")
|
||||||
|
(def ^:private snapshot-capture-debounce-ms 250)
|
||||||
|
|
||||||
(defn- set-transition-blur!
|
(defn- set-transition-blur!
|
||||||
[]
|
[]
|
||||||
@ -113,9 +122,7 @@
|
|||||||
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||||
(reset! transition-tiles-handler* nil)
|
(reset! transition-tiles-handler* nil)
|
||||||
(reset! transition-image-url* nil)
|
(reset! transition-image-url* nil)
|
||||||
(clear-transition-blur!)
|
(clear-transition-blur!))
|
||||||
;; Clear captured pixels so future transitions must explicitly capture again.
|
|
||||||
(set! wasm/canvas-snapshot-url nil))
|
|
||||||
|
|
||||||
(defn- set-transition-tiles-complete-handler!
|
(defn- set-transition-tiles-complete-handler!
|
||||||
"Installs a tiles-complete handler bound to the current transition epoch.
|
"Installs a tiles-complete handler bound to the current transition epoch.
|
||||||
@ -147,6 +154,16 @@
|
|||||||
(let [epoch (begin-page-transition!)]
|
(let [epoch (begin-page-transition!)]
|
||||||
(set-transition-tiles-complete-handler! epoch end-page-transition!))))
|
(set-transition-tiles-complete-handler! epoch end-page-transition!))))
|
||||||
|
|
||||||
|
(defn- start-context-loss-overlay!
|
||||||
|
[]
|
||||||
|
(reset! context-loss-overlay? true))
|
||||||
|
|
||||||
|
(defn- end-context-loss-overlay!
|
||||||
|
[]
|
||||||
|
(reset! context-loss-overlay? false)
|
||||||
|
(when-not @page-transition?
|
||||||
|
(reset! transition-image-url* nil)))
|
||||||
|
|
||||||
(defn listen-tiles-render-complete-once!
|
(defn listen-tiles-render-complete-once!
|
||||||
"Registers a one-shot listener for `penpot:wasm:tiles-complete`, dispatched from WASM
|
"Registers a one-shot listener for `penpot:wasm:tiles-complete`, dispatched from WASM
|
||||||
when a full tile pass finishes."
|
when a full tile pass finishes."
|
||||||
@ -157,6 +174,32 @@
|
|||||||
(f))
|
(f))
|
||||||
#js {:once true}))
|
#js {:once true}))
|
||||||
|
|
||||||
|
(defonce ^:private schedule-canvas-snapshot-capture!
|
||||||
|
(fns/debounce
|
||||||
|
(fn []
|
||||||
|
(when (and wasm/context-initialized?
|
||||||
|
(not @wasm/context-lost?)
|
||||||
|
(some? wasm/canvas))
|
||||||
|
(-> (webgl/capture-canvas-snapshot-url)
|
||||||
|
(p/catch (fn [_] nil)))))
|
||||||
|
snapshot-capture-debounce-ms))
|
||||||
|
|
||||||
|
(defn- start-canvas-snapshot-listener!
|
||||||
|
[]
|
||||||
|
(when-let [prev @snapshot-tiles-handler*]
|
||||||
|
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||||
|
(let [handler (fn [_] (schedule-canvas-snapshot-capture!))]
|
||||||
|
(reset! snapshot-tiles-handler* handler)
|
||||||
|
(.addEventListener ^js ug/document "penpot:wasm:tiles-complete" handler)))
|
||||||
|
|
||||||
|
(defn- stop-canvas-snapshot-listener!
|
||||||
|
[]
|
||||||
|
(when-let [prev @snapshot-tiles-handler*]
|
||||||
|
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||||
|
(reset! snapshot-tiles-handler* nil)
|
||||||
|
(when-let [cancel (unchecked-get schedule-canvas-snapshot-capture! "cancel")]
|
||||||
|
(cancel)))
|
||||||
|
|
||||||
(defn text-editor-wasm?
|
(defn text-editor-wasm?
|
||||||
[]
|
[]
|
||||||
(or (contains? cf/flags :feature-text-editor-wasm)
|
(or (contains? cf/flags :feature-text-editor-wasm)
|
||||||
@ -267,6 +310,36 @@
|
|||||||
;; forward declare helpers so render can call them
|
;; forward declare helpers so render can call them
|
||||||
(declare request-render)
|
(declare request-render)
|
||||||
(declare set-shape-vertical-align fonts-from-text-content)
|
(declare set-shape-vertical-align fonts-from-text-content)
|
||||||
|
(declare reload-renderer!)
|
||||||
|
|
||||||
|
(defn- build-reload-payload
|
||||||
|
"Builds renderer reload payload from current application state.
|
||||||
|
Avoids keeping heavyweight object snapshots in memory."
|
||||||
|
[]
|
||||||
|
(let [state @st/state
|
||||||
|
file-id (:current-file-id state)
|
||||||
|
page-id (:current-page-id state)
|
||||||
|
page (dsh/lookup-page state file-id page-id)
|
||||||
|
objects (dsh/lookup-page-objects state file-id page-id)
|
||||||
|
focus (:workspace-focus-selected state)
|
||||||
|
local (:workspace-local state)
|
||||||
|
zoom (:zoom local)
|
||||||
|
vbox (:vbox local)
|
||||||
|
canvas wasm/canvas
|
||||||
|
background (get page :background)]
|
||||||
|
{:canvas canvas
|
||||||
|
:base-objects (cpf/focus-objects objects focus)
|
||||||
|
:zoom zoom
|
||||||
|
:vbox vbox
|
||||||
|
:background background}))
|
||||||
|
|
||||||
|
(defn free-gpu-resources
|
||||||
|
[]
|
||||||
|
;; check if the context has not been lost already or we will get warnings about
|
||||||
|
;; removing objects from a non-current context
|
||||||
|
(when (and wasm/context-initialized?
|
||||||
|
(not @wasm/context-lost?))
|
||||||
|
(h/call wasm/internal-module "_free_gpu_resources")))
|
||||||
|
|
||||||
;; This should never be called from the outside.
|
;; This should never be called from the outside.
|
||||||
(defn- render
|
(defn- render
|
||||||
@ -1578,6 +1651,57 @@
|
|||||||
(h/call wasm/internal-module "_init_shapes_pool" total-shapes)
|
(h/call wasm/internal-module "_init_shapes_pool" total-shapes)
|
||||||
(set-objects base-objects on-render on-shapes-ready force-sync)))
|
(set-objects base-objects on-render on-shapes-ready force-sync)))
|
||||||
|
|
||||||
|
(defn- run-resource-callbacks!
|
||||||
|
[entries]
|
||||||
|
(if (seq entries)
|
||||||
|
(p/create
|
||||||
|
(fn [resolve _reject]
|
||||||
|
(->> (rx/from (vals (d/index-by :key :callback entries)))
|
||||||
|
(rx/merge-map (fn [callback] (if (fn? callback) (callback) (rx/empty))))
|
||||||
|
(rx/reduce conj [])
|
||||||
|
(rx/subs! (fn [_] (resolve nil))
|
||||||
|
(fn [_cause] (resolve nil))
|
||||||
|
(fn [] (resolve nil))))))
|
||||||
|
(p/resolved nil)))
|
||||||
|
|
||||||
|
(defn- replay-font-resources!
|
||||||
|
[fonts]
|
||||||
|
(let [pending (into [] (f/store-fonts fonts))]
|
||||||
|
(run-resource-callbacks! pending)))
|
||||||
|
|
||||||
|
(defn- derive-font-resources
|
||||||
|
[base-objects payload-fonts]
|
||||||
|
(let [object-fonts
|
||||||
|
(->> (vals base-objects)
|
||||||
|
(filter cfh/text-shape?)
|
||||||
|
(mapcat (fn [shape]
|
||||||
|
(let [content (ensure-text-content (:content shape))
|
||||||
|
direct-fonts (f/get-content-fonts content)
|
||||||
|
;; `true` would call `write-shape-text`, which requires
|
||||||
|
;; an active current shape in WASM and can panic during
|
||||||
|
;; reload pre-processing. We only need fallback font
|
||||||
|
;; discovery here, so use side-effect free mode.
|
||||||
|
fallback-fonts (fonts-from-text-content content false)]
|
||||||
|
(concat direct-fonts fallback-fonts))))
|
||||||
|
(into #{}))]
|
||||||
|
(into [] (set (concat payload-fonts object-fonts)))))
|
||||||
|
|
||||||
|
(defn- replay-image-resources!
|
||||||
|
[image-resources]
|
||||||
|
(let [pending
|
||||||
|
(into []
|
||||||
|
(keep (fn [{:keys [shape-id image-id thumbnail?]}]
|
||||||
|
(when (and (uuid? image-id) (or (nil? shape-id) (uuid? shape-id)))
|
||||||
|
(fetch-image (or shape-id uuid/zero) image-id (boolean thumbnail?)))))
|
||||||
|
image-resources)]
|
||||||
|
(run-resource-callbacks! pending)))
|
||||||
|
|
||||||
|
(defn- wait-next-frame!
|
||||||
|
[]
|
||||||
|
(p/create
|
||||||
|
(fn [resolve _reject]
|
||||||
|
(js/requestAnimationFrame (fn [] (resolve nil))))))
|
||||||
|
|
||||||
(def ^:private default-context-options
|
(def ^:private default-context-options
|
||||||
#js {:antialias false
|
#js {:antialias false
|
||||||
:depth true
|
:depth true
|
||||||
@ -1654,96 +1778,202 @@
|
|||||||
(defn- on-webgl-context-lost
|
(defn- on-webgl-context-lost
|
||||||
[event]
|
[event]
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
|
;; Keep the last rendered pixels visible while context is lost/recovering.
|
||||||
|
(start-context-loss-overlay!)
|
||||||
|
(when-let [url wasm/canvas-snapshot-url]
|
||||||
|
(when (string? url)
|
||||||
|
(reset! transition-image-url* url)))
|
||||||
(reset! wasm/context-lost? true)
|
(reset! wasm/context-lost? true)
|
||||||
(ex/raise :type :wasm-error
|
(st/async-emit!
|
||||||
:code :webgl-context-lost
|
(ntf/show {:content (tr "webgl.webgl-context-lost.toast")
|
||||||
:hint "WASM Error: WebGL context lost"))
|
:type :toast
|
||||||
|
:level :warning
|
||||||
|
:timeout 5000}))
|
||||||
|
(st/emit! (drw/context-lost)))
|
||||||
|
|
||||||
|
(defn- on-webgl-context-restored
|
||||||
|
[event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(reset! wasm/context-lost? false)
|
||||||
|
(st/emit! (drw/context-restored))
|
||||||
|
(let [payload (build-reload-payload)]
|
||||||
|
(-> (reload-renderer! payload)
|
||||||
|
(p/then (fn [_]
|
||||||
|
(listen-tiles-render-complete-once! end-context-loss-overlay!)
|
||||||
|
(st/async-emit!
|
||||||
|
(ntf/show {:content (tr "webgl.webgl-context-recovered.toast")
|
||||||
|
:type :toast
|
||||||
|
:level :success
|
||||||
|
:timeout 3000}))))
|
||||||
|
(p/catch (fn [cause]
|
||||||
|
(end-context-loss-overlay!)
|
||||||
|
(log/error :hint "wasm reload after context restore failed"
|
||||||
|
:cause cause)
|
||||||
|
nil)))))
|
||||||
|
|
||||||
(defn init-canvas-context
|
(defn init-canvas-context
|
||||||
[canvas]
|
[canvas]
|
||||||
(let [gl (unchecked-get wasm/internal-module "GL")
|
(if-not (wasm/module-ready?)
|
||||||
flags (debug-flags)
|
false
|
||||||
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
|
(let [gl (unchecked-get wasm/internal-module "GL")
|
||||||
context (.getContext ^js canvas context-id default-context-options)
|
flags (debug-flags)
|
||||||
context-init? (not (nil? context))
|
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
|
||||||
browser (sr/translate-browser cf/browser)]
|
context (.getContext ^js canvas context-id default-context-options)
|
||||||
(when-not (nil? context)
|
context-init? (not (nil? context))
|
||||||
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
|
browser (sr/translate-browser cf/browser)]
|
||||||
(.makeContextCurrent ^js gl handle)
|
(when-not (nil? context)
|
||||||
(set! wasm/gl-context-handle handle)
|
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
|
||||||
(set! wasm/gl-context context)
|
(.makeContextCurrent ^js gl handle)
|
||||||
|
(set! wasm/gl-context-handle handle)
|
||||||
|
(set! wasm/gl-context context)
|
||||||
|
|
||||||
;; Force the WEBGL_debug_renderer_info extension as emscripten does not enable it
|
;; Force the WEBGL_debug_renderer_info extension as emscripten does not enable it
|
||||||
(.getExtension context "WEBGL_debug_renderer_info")
|
(.getExtension context "WEBGL_debug_renderer_info")
|
||||||
|
|
||||||
;; Initialize Wasm Render Engine
|
;; Initialize Wasm Render Engine
|
||||||
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
|
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
|
||||||
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
||||||
(when-let [t (wasm-aa-threshold-from-route-params)]
|
(when-let [t (wasm-aa-threshold-from-route-params)]
|
||||||
(h/call wasm/internal-module "_set_antialias_threshold" t))
|
(h/call wasm/internal-module "_set_antialias_threshold" t))
|
||||||
(when-let [t (wasm-viewport-interest-area-threshold-from-route-params)]
|
(when-let [t (wasm-viewport-interest-area-threshold-from-route-params)]
|
||||||
(h/call wasm/internal-module "_set_viewport_interest_area_threshold" t))
|
(h/call wasm/internal-module "_set_viewport_interest_area_threshold" t))
|
||||||
(when-let [t (wasm-max-blocking-time-ms-from-route-params)]
|
(when-let [t (wasm-max-blocking-time-ms-from-route-params)]
|
||||||
(h/call wasm/internal-module "_set_max_blocking_time_ms" t))
|
(h/call wasm/internal-module "_set_max_blocking_time_ms" t))
|
||||||
(when-let [t (wasm-node-batch-threshold-from-route-params)]
|
(when-let [t (wasm-node-batch-threshold-from-route-params)]
|
||||||
(h/call wasm/internal-module "_set_node_batch_threshold" t))
|
(h/call wasm/internal-module "_set_node_batch_threshold" t))
|
||||||
(when-let [t (wasm-blur-downscale-threshold-from-route-params)]
|
(when-let [t (wasm-blur-downscale-threshold-from-route-params)]
|
||||||
(h/call wasm/internal-module "_set_blur_downscale_threshold" t))
|
(h/call wasm/internal-module "_set_blur_downscale_threshold" t))
|
||||||
(when-let [max-tex (webgl/max-texture-size context)]
|
(when-let [max-tex (webgl/max-texture-size context)]
|
||||||
(h/call wasm/internal-module "_set_max_atlas_texture_size" max-tex))
|
(h/call wasm/internal-module "_set_max_atlas_texture_size" max-tex))
|
||||||
|
|
||||||
;; Set browser and canvas size only after initialization
|
;; Set browser and canvas size only after initialization
|
||||||
(h/call wasm/internal-module "_set_browser" browser)
|
(h/call wasm/internal-module "_set_browser" browser)
|
||||||
(set-canvas-size canvas)
|
(set-canvas-size canvas)
|
||||||
|
|
||||||
;; Add event listeners for WebGL context lost
|
;; Add event listeners for WebGL context lost
|
||||||
(set! wasm/canvas canvas)
|
(set! wasm/canvas canvas)
|
||||||
(.addEventListener canvas "webglcontextlost" on-webgl-context-lost)
|
(.addEventListener canvas "webglcontextlost" on-webgl-context-lost)
|
||||||
(set! wasm/context-initialized? true)))
|
(.addEventListener canvas "webglcontextrestored" on-webgl-context-restored)
|
||||||
|
(start-canvas-snapshot-listener!)
|
||||||
|
(reset! wasm/context-lost? false)
|
||||||
|
(set! wasm/context-initialized? true)))
|
||||||
|
|
||||||
context-init?))
|
context-init?)))
|
||||||
|
|
||||||
(defn clear-canvas
|
(defn clear-canvas
|
||||||
[]
|
([]
|
||||||
(when wasm/context-initialized?
|
(clear-canvas {}))
|
||||||
(try
|
([{:keys [lose-browser-context?]
|
||||||
(set! wasm/context-initialized? false)
|
:or {lose-browser-context? true}}]
|
||||||
|
(try
|
||||||
|
(set! wasm/context-initialized? false)
|
||||||
|
|
||||||
;; Cancel any pending animation frame to prevent race conditions
|
;; Cancel any pending animation frame to prevent race conditions.
|
||||||
(when wasm/internal-frame-id
|
(when wasm/internal-frame-id
|
||||||
(js/cancelAnimationFrame wasm/internal-frame-id)
|
(js/cancelAnimationFrame wasm/internal-frame-id))
|
||||||
(set! wasm/internal-frame-id nil))
|
|
||||||
|
|
||||||
;; Reset render flags to prevent new renders from being scheduled
|
;; Reset render flags to prevent new renders from being scheduled.
|
||||||
(reset! pending-render false)
|
(reset! pending-render false)
|
||||||
(reset! shapes-loading? false)
|
(reset! shapes-loading? false)
|
||||||
(reset! deferred-render? false)
|
(reset! deferred-render? false)
|
||||||
|
|
||||||
(h/call wasm/internal-module "_clean_up")
|
;; Remove listener before losing/deleting context.
|
||||||
|
(when wasm/canvas
|
||||||
|
(.removeEventListener wasm/canvas "webglcontextlost" on-webgl-context-lost)
|
||||||
|
(.removeEventListener wasm/canvas "webglcontextrestored" on-webgl-context-restored))
|
||||||
|
(stop-canvas-snapshot-listener!)
|
||||||
|
|
||||||
;; Remove event listener for WebGL context lost
|
(when (wasm/module-ready?)
|
||||||
(when wasm/canvas
|
(free-gpu-resources)
|
||||||
(.removeEventListener wasm/canvas "webglcontextlost" on-webgl-context-lost)
|
(h/call wasm/internal-module "_clean_up"))
|
||||||
(set! wasm/canvas nil))
|
|
||||||
|
|
||||||
;; Ensure the WebGL context is properly disposed so browsers do not keep
|
;; Ensure the WebGL context is properly disposed so browsers do not keep
|
||||||
;; accumulating active contexts between page switches.
|
;; accumulating active contexts between page switches.
|
||||||
(when-let [gl (unchecked-get wasm/internal-module "GL")]
|
(when-let [gl (unchecked-get wasm/internal-module "GL")]
|
||||||
(when-let [handle wasm/gl-context-handle]
|
(when-let [handle wasm/gl-context-handle]
|
||||||
(try
|
(try
|
||||||
;; Ask the browser to release resources explicitly if available.
|
;; For hard teardown we can explicitly lose browser context.
|
||||||
(when-let [ctx wasm/gl-context]
|
;; For reload->reinit flows we skip this because immediate context
|
||||||
(when-let [lose-ext (.getExtension ^js ctx "WEBGL_lose_context")]
|
;; recreation may fail on some browsers/GPUs while context is lost.
|
||||||
(.loseContext ^js lose-ext)))
|
(when lose-browser-context?
|
||||||
(.deleteContext ^js gl handle)
|
(when-let [ctx wasm/gl-context]
|
||||||
(finally
|
(when-let [lose-ext (.getExtension ^js ctx "WEBGL_lose_context")]
|
||||||
(set! wasm/gl-context-handle nil)
|
(.loseContext ^js lose-ext))))
|
||||||
(set! wasm/gl-context nil)))))
|
(.deleteContext ^js gl handle)
|
||||||
|
(catch :default dispose-error
|
||||||
|
(.error js/console dispose-error)))))
|
||||||
|
|
||||||
;; If this calls panics we don't want to crash. This happens sometimes
|
(wasm-gesture/reset-after-wasm-reload!)
|
||||||
;; with hot-reload in develop
|
(wasm/reset-context-state!)
|
||||||
(catch :default error
|
true
|
||||||
(.error js/console error)))))
|
|
||||||
|
;; If this panics we don't want to crash. This happens sometimes with
|
||||||
|
;; hot-reload in development.
|
||||||
|
(catch :default error
|
||||||
|
(.error js/console error)
|
||||||
|
(wasm-gesture/reset-after-wasm-reload!)
|
||||||
|
(wasm/reset-context-state!)
|
||||||
|
false))))
|
||||||
|
|
||||||
|
(defn reload-renderer!
|
||||||
|
[{:keys [canvas
|
||||||
|
base-objects
|
||||||
|
zoom
|
||||||
|
vbox
|
||||||
|
fonts
|
||||||
|
image-resources
|
||||||
|
background
|
||||||
|
background-opacity
|
||||||
|
on-render
|
||||||
|
on-shapes-ready
|
||||||
|
force-sync]
|
||||||
|
:or {fonts []
|
||||||
|
image-resources []
|
||||||
|
background-opacity 1
|
||||||
|
force-sync false}
|
||||||
|
:as payload}]
|
||||||
|
(ug/dispatch! (ug/event "penpot:wasm:reload-start"))
|
||||||
|
(let [fonts (derive-font-resources base-objects fonts)]
|
||||||
|
(-> (p/resolved nil)
|
||||||
|
;; Keep teardown strict (`_clean_up` + deleteContext) but do not
|
||||||
|
;; force `loseContext` because we immediately create a new context.
|
||||||
|
(p/then (fn [_]
|
||||||
|
(let [was-cleared? (clear-canvas {:lose-browser-context? false})]
|
||||||
|
(when-not was-cleared?
|
||||||
|
(ex/raise :type :wasm-error
|
||||||
|
:code :wasm-reload-context-failure
|
||||||
|
:hint "WASM renderer cleanup failed")))))
|
||||||
|
;; Give browser a frame to settle context deletion before init.
|
||||||
|
(p/then (fn [_] (wait-next-frame!)))
|
||||||
|
(p/then (fn [_]
|
||||||
|
(let [context-ready? (init-canvas-context canvas)]
|
||||||
|
(when-not context-ready?
|
||||||
|
(ex/raise :type :wasm-error
|
||||||
|
:code :wasm-reload-context-failure
|
||||||
|
:hint "WASM renderer could not create a new WebGL context"))
|
||||||
|
;; Gesture bookkeeping (`modifiers.cljs`) uses compare-and-set on an atom
|
||||||
|
;; that survives WASM teardown; reset so it matches fresh `_init` state.
|
||||||
|
(wasm-gesture/reset-after-wasm-reload!))))
|
||||||
|
;; Ensure render surfaces are blank before replay to avoid overpainting.
|
||||||
|
(p/then (fn [_] (h/call wasm/internal-module "_reset_canvas")))
|
||||||
|
(p/then (fn [_] (replay-font-resources! fonts)))
|
||||||
|
(p/then (fn [_] (replay-image-resources! image-resources)))
|
||||||
|
(p/then
|
||||||
|
(fn []
|
||||||
|
(initialize-viewport base-objects zoom vbox
|
||||||
|
:background background
|
||||||
|
:background-opacity background-opacity
|
||||||
|
:on-render on-render
|
||||||
|
:on-shapes-ready on-shapes-ready
|
||||||
|
:force-sync force-sync)
|
||||||
|
(request-render "reload-renderer")
|
||||||
|
(ug/dispatch! (ug/event "penpot:wasm:reload-complete"))
|
||||||
|
payload))
|
||||||
|
(p/catch
|
||||||
|
(fn [cause]
|
||||||
|
(ug/dispatch! (ug/event "penpot:wasm:reload-failed"))
|
||||||
|
(clear-canvas)
|
||||||
|
(p/rejected cause))))))
|
||||||
|
|
||||||
(defn show-grid
|
(defn show-grid
|
||||||
[id]
|
[id]
|
||||||
|
|||||||
29
frontend/src/app/render_wasm/gesture.cljs
Normal file
29
frontend/src/app/render_wasm/gesture.cljs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.render-wasm.gesture
|
||||||
|
"WASM-linked pointer gestures (interactive transforms, like D&D)")
|
||||||
|
|
||||||
|
(defonce ^:private interactive-transform-active? (atom false))
|
||||||
|
|
||||||
|
(defn reset-after-wasm-reload!
|
||||||
|
"Call after `_clean_up` + `_init` (new GL context). WASM interactive_transform /
|
||||||
|
fast_mode are reset to defaults; this atom must match or compare-and-set helpers in
|
||||||
|
modifiers.cljs will skip `_set_modifiers_start` / `_set_modifiers_end` incorrectly."
|
||||||
|
[]
|
||||||
|
(reset! interactive-transform-active? false))
|
||||||
|
|
||||||
|
(defn try-begin-interactive-transform!
|
||||||
|
"Returns true iff we transitioned inactive → active and native `_set_modifiers_start`
|
||||||
|
must run."
|
||||||
|
[]
|
||||||
|
(compare-and-set! interactive-transform-active? false true))
|
||||||
|
|
||||||
|
(defn try-end-interactive-transform!
|
||||||
|
"Returns true iff we transitioned active → inactive and native `_set_modifiers_end`
|
||||||
|
must run."
|
||||||
|
[]
|
||||||
|
(compare-and-set! interactive-transform-active? true false))
|
||||||
@ -29,6 +29,20 @@
|
|||||||
;; When we're rendering in a sync way we want to stop the asynchrous `request-render`
|
;; When we're rendering in a sync way we want to stop the asynchrous `request-render`
|
||||||
(defonce disable-request-render? (atom false))
|
(defonce disable-request-render? (atom false))
|
||||||
|
|
||||||
|
(defn module-ready?
|
||||||
|
[]
|
||||||
|
(and internal-module (fn? (unchecked-get internal-module "_init"))))
|
||||||
|
|
||||||
|
(defn reset-context-state!
|
||||||
|
[]
|
||||||
|
(set! internal-frame-id nil)
|
||||||
|
(set! canvas nil)
|
||||||
|
(set! canvas-snapshot-url nil)
|
||||||
|
(set! gl-context-handle nil)
|
||||||
|
(set! gl-context nil)
|
||||||
|
(set! context-initialized? false)
|
||||||
|
(reset! context-lost? false))
|
||||||
|
|
||||||
|
|
||||||
(defonce serializers
|
(defonce serializers
|
||||||
#js {:blur-type shared/RawBlurType
|
#js {:blur-type shared/RawBlurType
|
||||||
|
|||||||
@ -1715,6 +1715,8 @@ msgstr "WebGL has stopped working. Please reload the page to reset it"
|
|||||||
msgid "errors.webgl-context-lost.main-message"
|
msgid "errors.webgl-context-lost.main-message"
|
||||||
msgstr "Oops! The canvas context was lost"
|
msgstr "Oops! The canvas context was lost"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs:1051
|
#: src/app/main/ui/dashboard/team.cljs:1051
|
||||||
msgid "errors.webhooks.connection"
|
msgid "errors.webhooks.connection"
|
||||||
msgstr "Connection error, URL not reacheable"
|
msgstr "Connection error, URL not reacheable"
|
||||||
@ -8973,6 +8975,12 @@ msgstr "Done"
|
|||||||
msgid "workspace.top-bar.view-only"
|
msgid "workspace.top-bar.view-only"
|
||||||
msgstr "**Inspecting code** (View Only)"
|
msgstr "**Inspecting code** (View Only)"
|
||||||
|
|
||||||
|
msgid "workspace.top-bar.webgl-context-lost"
|
||||||
|
msgstr "Rendering unavailable. Refresh to restore editing."
|
||||||
|
|
||||||
|
msgid "workspace.top-bar.webgl-context-lost.reload"
|
||||||
|
msgstr "Refresh"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/history.cljs:333
|
#: src/app/main/ui/workspace/sidebar/history.cljs:333
|
||||||
msgid "workspace.undo.empty"
|
msgid "workspace.undo.empty"
|
||||||
msgstr "There are no history changes so far"
|
msgstr "There are no history changes so far"
|
||||||
@ -9234,6 +9242,14 @@ msgstr ""
|
|||||||
msgid "workspace.versions.warning.text"
|
msgid "workspace.versions.warning.text"
|
||||||
msgstr "Autosaved versions will be kept for %s days."
|
msgstr "Autosaved versions will be kept for %s days."
|
||||||
|
|
||||||
|
#: src/app/render_wasm/api.cljs
|
||||||
|
msgid "webgl.webgl-context-lost.toast"
|
||||||
|
msgstr "WebGL context was lost"
|
||||||
|
|
||||||
|
#: src/app/render_wasm/api.cljs
|
||||||
|
msgid "webgl.webgl-context-recovered.toast"
|
||||||
|
msgstr "WebGL context was recovered"
|
||||||
|
|
||||||
msgid "webgl.modals.webgl-unavailable.title"
|
msgid "webgl.modals.webgl-unavailable.title"
|
||||||
msgstr "Oops! WebGL is not available"
|
msgstr "Oops! WebGL is not available"
|
||||||
|
|
||||||
|
|||||||
@ -8701,6 +8701,12 @@ msgstr "Hecho"
|
|||||||
msgid "workspace.top-bar.view-only"
|
msgid "workspace.top-bar.view-only"
|
||||||
msgstr "**Inspeccionando código** (View only)"
|
msgstr "**Inspeccionando código** (View only)"
|
||||||
|
|
||||||
|
msgid "workspace.top-bar.webgl-context-lost"
|
||||||
|
msgstr "Renderizado no disponible. Recarga la página para restaurar la edición."
|
||||||
|
|
||||||
|
msgid "workspace.top-bar.webgl-context-lost.reload"
|
||||||
|
msgstr "Recargar"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/history.cljs:333
|
#: src/app/main/ui/workspace/sidebar/history.cljs:333
|
||||||
msgid "workspace.undo.empty"
|
msgid "workspace.undo.empty"
|
||||||
msgstr "Todavía no hay cambios en el histórico"
|
msgstr "Todavía no hay cambios en el histórico"
|
||||||
@ -8942,6 +8948,14 @@ msgstr "Si quieres aumentar este límite, contáctanos en [support@penpot.app](%
|
|||||||
msgid "workspace.versions.warning.text"
|
msgid "workspace.versions.warning.text"
|
||||||
msgstr "Los autoguardados duran %s días."
|
msgstr "Los autoguardados duran %s días."
|
||||||
|
|
||||||
|
#: src/app/render_wasm/api.cljs
|
||||||
|
msgid "webgl.webgl-context-lost.toast"
|
||||||
|
msgstr "Se perdió el contexto WebGL"
|
||||||
|
|
||||||
|
#: src/app/render_wasm/api.cljs
|
||||||
|
msgid "webgl.webgl-context-recovered.toast"
|
||||||
|
msgstr "Se recuperó el contexto WebGL"
|
||||||
|
|
||||||
msgid "webgl.modals.webgl-unavailable.title"
|
msgid "webgl.modals.webgl-unavailable.title"
|
||||||
msgstr "Vaya, WebGL no está disponible"
|
msgstr "Vaya, WebGL no está disponible"
|
||||||
|
|
||||||
|
|||||||
@ -178,9 +178,17 @@ pub extern "C" fn set_browser(browser: u8) -> Result<()> {
|
|||||||
pub extern "C" fn clean_up() -> Result<()> {
|
pub extern "C" fn clean_up() -> Result<()> {
|
||||||
// Cancel the current animation frame if it exists so
|
// Cancel the current animation frame if it exists so
|
||||||
// it won't try to render without context
|
// it won't try to render without context
|
||||||
let render_state = get_render_state();
|
unsafe {
|
||||||
render_state.cancel_animation_frame();
|
#[allow(static_mut_refs)]
|
||||||
unsafe { STATE = None }
|
if STATE.is_some() {
|
||||||
|
// Cancel the current animation frame if it exists so
|
||||||
|
// it won't try to render without context.
|
||||||
|
let render_state = get_render_state();
|
||||||
|
render_state.cancel_animation_frame();
|
||||||
|
render_state.prepare_context_loss_cleanup();
|
||||||
|
}
|
||||||
|
STATE = None;
|
||||||
|
}
|
||||||
mem::free_bytes()?;
|
mem::free_bytes()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -1072,6 +1080,11 @@ pub extern "C" fn render_stats() {
|
|||||||
get_render_state().print_stats();
|
get_render_state().print_stats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub fn free_gpu_resources() {
|
||||||
|
get_render_state().free_gpu_resources();
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
init_gl!();
|
init_gl!();
|
||||||
|
|||||||
@ -3775,10 +3775,17 @@ impl RenderState {
|
|||||||
pub fn print_stats(&self) {
|
pub fn print_stats(&self) {
|
||||||
self.stats.print();
|
self.stats.print();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for RenderState {
|
pub fn prepare_context_loss_cleanup(&mut self) {
|
||||||
fn drop(&mut self) {
|
// Drop cached GPU-backed snapshots before dropping the render state.
|
||||||
|
self.backbuffer_crop_cache.clear();
|
||||||
|
self.surfaces.invalidate_tile_cache();
|
||||||
|
// Mark context as abandoned so resource destructors avoid issuing
|
||||||
|
// GL commands when the browser has already lost/restored the context.
|
||||||
|
get_gpu_state().context.abandon();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free_gpu_resources(&mut self) {
|
||||||
get_gpu_state().context.free_gpu_resources();
|
get_gpu_state().context.free_gpu_resources();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user