mirror of
https://github.com/penpot/penpot.git
synced 2026-05-11 19:13:49 +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();
|
||||
});
|
||||
|
||||
test("BUG 13541 - Shows error page when WebGL context is lost", async ({
|
||||
test("Shows toast when WebGL context is lost", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
@ -31,12 +31,9 @@ test("BUG 13541 - Shows error page when WebGL context is lost", async ({
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByText("Oops! The canvas context was lost"),
|
||||
page.getByText("WebGL context was lost"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("WebGL has stopped working"),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Reload page")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Refresh" })).toBeVisible();
|
||||
});
|
||||
|
||||
test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
(ns app.main.data.render-wasm
|
||||
(:require
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn context-lost
|
||||
@ -7,11 +8,37 @@
|
||||
(ptk/reify ::context-lost
|
||||
ptk/UpdateEvent
|
||||
(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
|
||||
[]
|
||||
(ptk/reify ::context-restored
|
||||
ptk/UpdateEvent
|
||||
(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.streams :as ms]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.gesture :as wasm-gesture]
|
||||
[app.render-wasm.shape :as wasm.shape]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
@ -44,16 +45,17 @@
|
||||
;; Paired with `set-modifiers-start` / `set-modifiers-end` so the
|
||||
;; native side only toggles once per gesture, regardless of how many
|
||||
;; `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!
|
||||
[]
|
||||
(when (compare-and-set! interactive-transform-active? false true)
|
||||
(when (wasm-gesture/try-begin-interactive-transform!)
|
||||
(wasm.api/set-modifiers-start)))
|
||||
|
||||
(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)))
|
||||
|
||||
(def ^:private transform-attrs
|
||||
|
||||
@ -20,6 +20,10 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn- render-context-lost?
|
||||
[state]
|
||||
(true? (get-in state [:render-state :lost])))
|
||||
|
||||
(defn initialize-viewport
|
||||
[{:keys [width height] :as size}]
|
||||
|
||||
@ -101,7 +105,9 @@
|
||||
(ptk/reify ::update-viewport-position-center
|
||||
ptk/UpdateEvent
|
||||
(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
|
||||
[{:keys [x y] :or {x identity y identity}}]
|
||||
@ -118,11 +124,13 @@
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :vbox]
|
||||
(fn [vbox]
|
||||
(-> vbox
|
||||
(update :x x)
|
||||
(update :y y)))))))
|
||||
(if (render-context-lost? state)
|
||||
state
|
||||
(update-in state [:workspace-local :vbox]
|
||||
(fn [vbox]
|
||||
(-> vbox
|
||||
(update :x x)
|
||||
(update :y y))))))))
|
||||
|
||||
(defn update-viewport-size
|
||||
[resize-type {:keys [width height] :as size}]
|
||||
@ -172,7 +180,8 @@
|
||||
(watch [_ state stream]
|
||||
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
|
||||
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/of #(-> % (assoc-in [:workspace-local :panning] true)))
|
||||
(->> stream
|
||||
|
||||
@ -21,6 +21,10 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn- render-context-lost?
|
||||
[state]
|
||||
(true? (get-in state [:render-state :lost])))
|
||||
|
||||
(defn impl-update-zoom
|
||||
[{:keys [vbox] :as local} center zoom]
|
||||
(let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom)
|
||||
@ -43,9 +47,11 @@
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [center (if (= center ::auto) @ms/mouse-position center)]
|
||||
(update state :workspace-local
|
||||
#(impl-update-zoom % center (fn [z] (min (* z 1.3) 200)))))))))
|
||||
(if (render-context-lost? state)
|
||||
state
|
||||
(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
|
||||
([]
|
||||
@ -56,9 +62,11 @@
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(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)))))))))
|
||||
(if (render-context-lost? state)
|
||||
state
|
||||
(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
|
||||
([scale]
|
||||
@ -69,68 +77,76 @@
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [vp (dm/get-in state [:workspace-local :vbox])
|
||||
x (+ (:x vp) (/ (:width vp) 2))
|
||||
y (+ (:y vp) (/ (:height vp) 2))
|
||||
center (d/nilv center (gpt/point x y))]
|
||||
(update state :workspace-local
|
||||
#(impl-update-zoom % center (fn [z] (-> (* z scale)
|
||||
(max 0.01)
|
||||
(min 200))))))))))
|
||||
(if (render-context-lost? state)
|
||||
state
|
||||
(let [vp (dm/get-in state [:workspace-local :vbox])
|
||||
x (+ (:x vp) (/ (:width vp) 2))
|
||||
y (+ (:y vp) (/ (:height vp) 2))
|
||||
center (d/nilv center (gpt/point x y))]
|
||||
(update state :workspace-local
|
||||
#(impl-update-zoom % center (fn [z] (-> (* z scale)
|
||||
(max 0.01)
|
||||
(min 200)))))))))))
|
||||
|
||||
(def reset-zoom
|
||||
(ptk/reify ::reset-zoom
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-local
|
||||
#(impl-update-zoom % nil 1)))))
|
||||
(if (render-context-lost? state)
|
||||
state
|
||||
(update state :workspace-local
|
||||
#(impl-update-zoom % nil 1))))))
|
||||
|
||||
(def zoom-to-fit-all
|
||||
(ptk/reify ::zoom-to-fit-all
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
shapes (cfh/get-immediate-children objects)
|
||||
srect (gsh/shapes->rect shapes)]
|
||||
(if (empty? shapes)
|
||||
state
|
||||
(update state :workspace-local
|
||||
(fn [{:keys [vport] :as local}]
|
||||
(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))]
|
||||
(if (render-context-lost? state)
|
||||
state
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
shapes (cfh/get-immediate-children objects)
|
||||
srect (gsh/shapes->rect shapes)]
|
||||
(if (empty? shapes)
|
||||
state
|
||||
(update state :workspace-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))]
|
||||
(-> 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]
|
||||
(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
|
||||
[ids]
|
||||
(ptk/reify ::fit-to-shapes
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(if (empty? ids)
|
||||
(if (or (render-context-lost? state) (empty? ids))
|
||||
state
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
@ -155,7 +171,8 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(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/of #(-> % (assoc-in [:workspace-local :zooming] true)))
|
||||
(->> stream
|
||||
|
||||
@ -221,9 +221,11 @@
|
||||
(when-let [cause (::instance error)]
|
||||
(ex/print-throwable cause)
|
||||
(let [code (get error :code)]
|
||||
(if (or (= code :panic)
|
||||
(= code :webgl-context-lost))
|
||||
(cond
|
||||
(= code :panic)
|
||||
(st/emit! (rt/assign-exception error))
|
||||
|
||||
:else
|
||||
(flash :type :handled :cause cause)))))
|
||||
|
||||
;; We receive a explicit authentication error; If the uri is for
|
||||
|
||||
@ -491,13 +491,6 @@
|
||||
:service-unavailable
|
||||
[:> service-unavailable*]
|
||||
|
||||
:wasm-error
|
||||
(case (get data :code)
|
||||
:webgl-context-lost
|
||||
[:> webgl-context-lost*]
|
||||
|
||||
[:> internal-error* props])
|
||||
|
||||
[:> internal-error* props])))
|
||||
|
||||
(mf/defc context-wrapper*
|
||||
|
||||
@ -219,6 +219,7 @@
|
||||
[{:keys [objects selected page-id file-id on-change-section on-expand]}]
|
||||
(let [permissions
|
||||
(mf/use-ctx ctx/permissions)
|
||||
render-context-lost? (mf/deref refs/render-context-lost?)
|
||||
|
||||
options-mode
|
||||
(mf/deref refs/options-mode-global)
|
||||
@ -228,7 +229,7 @@
|
||||
(sequence (keep (d/getf objects)) selected))]
|
||||
|
||||
[:div {:class (stl/css :tool-window)}
|
||||
(if (:can-edit permissions)
|
||||
(if (and (:can-edit permissions) (not render-context-lost?))
|
||||
[:> tab-switcher* {:tabs options-tabs
|
||||
:on-change on-option-tab-change
|
||||
:selected (name options-mode)
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
(:require
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.refs :as refs]
|
||||
[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.path-actions :refer [path-actions*]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@ -23,18 +25,24 @@
|
||||
[]
|
||||
(let [on-close
|
||||
(mf/use-fn
|
||||
#(st/emit! :interrupt
|
||||
(dw/set-options-mode :design)
|
||||
(dwc/set-workspace-read-only false)))]
|
||||
(fn []
|
||||
(st/emit! :interrupt
|
||||
(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-container)}
|
||||
[:div {:class (stl/css :viewport-actions-title)}
|
||||
[:> i18n/tr-html*
|
||||
{:tag-name "span"
|
||||
:content (tr "workspace.top-bar.view-only")}]]
|
||||
[:button {:class (stl/css :done-btn)
|
||||
:on-click on-close}
|
||||
(tr "workspace.top-bar.read-only.done")]]]))
|
||||
:content (tr (if render-context-lost?
|
||||
"workspace.top-bar.webgl-context-lost"
|
||||
"workspace.top-bar.view-only"))}]]
|
||||
(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*
|
||||
[{:keys [layout edit-path-state shape]}]
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
|
||||
top: calc(var(--actions-toolbar-position-y) + var(--actions-toolbar-offset-y));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: deprecated.$z-index-20;
|
||||
}
|
||||
|
||||
@ -31,11 +32,10 @@
|
||||
box-shadow: 0 0 deprecated.$s-12 0 var(--menu-shadow-color);
|
||||
gap: deprecated.$s-8;
|
||||
height: deprecated.$s-48;
|
||||
margin-left: -50%;
|
||||
padding: deprecated.$s-8;
|
||||
cursor: initial;
|
||||
pointer-events: initial;
|
||||
width: deprecated.$s-400;
|
||||
min-width: deprecated.$s-400;
|
||||
border: deprecated.$s-2 solid var(--panel-border-color);
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
font-size: deprecated.$fs-12;
|
||||
color: var(--color-foreground-secondary);
|
||||
padding-left: deprecated.$s-8;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.done-btn {
|
||||
|
||||
@ -254,6 +254,7 @@
|
||||
|
||||
;; True when we are opening a new file or switching to a new page
|
||||
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-context-menu (actions/on-context-menu hover hover-ids read-only?)
|
||||
@ -544,8 +545,9 @@
|
||||
:style {:background-color background
|
||||
:pointer-events "none"}}]
|
||||
|
||||
;; Show the transition image when we are opening a new file or switching to a new page
|
||||
(when (and page-transition? (some? transition-image-url))
|
||||
;; Show the transition image when switching pages or recovering from WebGL context loss.
|
||||
(when (and (or page-transition? context-loss-overlay?)
|
||||
(some? transition-image-url))
|
||||
(let [src transition-image-url]
|
||||
[:img {:data-testid "canvas-wasm-transition"
|
||||
:src src
|
||||
@ -556,6 +558,7 @@
|
||||
:height "100%"
|
||||
:object-fit "cover"
|
||||
:pointer-events "none"
|
||||
;; use (when page-transition? "blur(4px)") if we don't want the blur on context loss
|
||||
:filter "blur(4px)"}}]))
|
||||
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.focus :as cpf]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
@ -22,6 +23,9 @@
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[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.refs :as refs]
|
||||
[app.main.router :as rt]
|
||||
@ -32,6 +36,7 @@
|
||||
[app.render-wasm.api.texts :as t]
|
||||
[app.render-wasm.api.webgl :as webgl]
|
||||
[app.render-wasm.deserializers :as dr]
|
||||
[app.render-wasm.gesture :as wasm-gesture]
|
||||
[app.render-wasm.helpers :as h]
|
||||
[app.render-wasm.mem :as mem]
|
||||
[app.render-wasm.mem.heap32 :as mem.h32]
|
||||
@ -45,6 +50,7 @@
|
||||
[app.util.dom :as dom]
|
||||
[app.util.functions :as fns]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.modules :as mod]
|
||||
[app.util.text.content :as tc]
|
||||
[beicon.v2.core :as rx]
|
||||
@ -69,11 +75,14 @@
|
||||
;; - `transition-tiles-handler*`: the currently installed DOM event handler for
|
||||
;; `penpot:wasm:tiles-complete`, so we can remove/replace it safely.
|
||||
(defonce page-transition? (atom false))
|
||||
(defonce context-loss-overlay? (atom false))
|
||||
(defonce transition-image-url* (atom nil))
|
||||
(defonce transition-epoch* (atom 0))
|
||||
(defonce transition-tiles-handler* (atom nil))
|
||||
(defonce snapshot-tiles-handler* (atom nil))
|
||||
|
||||
(def ^:private transition-blur-css "blur(4px)")
|
||||
(def ^:private snapshot-capture-debounce-ms 250)
|
||||
|
||||
(defn- set-transition-blur!
|
||||
[]
|
||||
@ -113,9 +122,7 @@
|
||||
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
|
||||
(reset! transition-tiles-handler* nil)
|
||||
(reset! transition-image-url* nil)
|
||||
(clear-transition-blur!)
|
||||
;; Clear captured pixels so future transitions must explicitly capture again.
|
||||
(set! wasm/canvas-snapshot-url nil))
|
||||
(clear-transition-blur!))
|
||||
|
||||
(defn- set-transition-tiles-complete-handler!
|
||||
"Installs a tiles-complete handler bound to the current transition epoch.
|
||||
@ -147,6 +154,16 @@
|
||||
(let [epoch (begin-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!
|
||||
"Registers a one-shot listener for `penpot:wasm:tiles-complete`, dispatched from WASM
|
||||
when a full tile pass finishes."
|
||||
@ -157,6 +174,32 @@
|
||||
(f))
|
||||
#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?
|
||||
[]
|
||||
(or (contains? cf/flags :feature-text-editor-wasm)
|
||||
@ -267,6 +310,36 @@
|
||||
;; forward declare helpers so render can call them
|
||||
(declare request-render)
|
||||
(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.
|
||||
(defn- render
|
||||
@ -1578,6 +1651,57 @@
|
||||
(h/call wasm/internal-module "_init_shapes_pool" total-shapes)
|
||||
(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
|
||||
#js {:antialias false
|
||||
:depth true
|
||||
@ -1654,96 +1778,202 @@
|
||||
(defn- on-webgl-context-lost
|
||||
[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)
|
||||
(ex/raise :type :wasm-error
|
||||
:code :webgl-context-lost
|
||||
:hint "WASM Error: WebGL context lost"))
|
||||
(st/async-emit!
|
||||
(ntf/show {:content (tr "webgl.webgl-context-lost.toast")
|
||||
: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
|
||||
[canvas]
|
||||
(let [gl (unchecked-get wasm/internal-module "GL")
|
||||
flags (debug-flags)
|
||||
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
|
||||
context (.getContext ^js canvas context-id default-context-options)
|
||||
context-init? (not (nil? context))
|
||||
browser (sr/translate-browser cf/browser)]
|
||||
(when-not (nil? context)
|
||||
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
|
||||
(.makeContextCurrent ^js gl handle)
|
||||
(set! wasm/gl-context-handle handle)
|
||||
(set! wasm/gl-context context)
|
||||
(if-not (wasm/module-ready?)
|
||||
false
|
||||
(let [gl (unchecked-get wasm/internal-module "GL")
|
||||
flags (debug-flags)
|
||||
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
|
||||
context (.getContext ^js canvas context-id default-context-options)
|
||||
context-init? (not (nil? context))
|
||||
browser (sr/translate-browser cf/browser)]
|
||||
(when-not (nil? context)
|
||||
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
|
||||
(.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
|
||||
(.getExtension context "WEBGL_debug_renderer_info")
|
||||
;; Force the WEBGL_debug_renderer_info extension as emscripten does not enable it
|
||||
(.getExtension context "WEBGL_debug_renderer_info")
|
||||
|
||||
;; Initialize Wasm Render Engine
|
||||
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
|
||||
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
||||
(when-let [t (wasm-aa-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_antialias_threshold" t))
|
||||
(when-let [t (wasm-viewport-interest-area-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_viewport_interest_area_threshold" t))
|
||||
(when-let [t (wasm-max-blocking-time-ms-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_max_blocking_time_ms" t))
|
||||
(when-let [t (wasm-node-batch-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_node_batch_threshold" t))
|
||||
(when-let [t (wasm-blur-downscale-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_blur_downscale_threshold" t))
|
||||
(when-let [max-tex (webgl/max-texture-size context)]
|
||||
(h/call wasm/internal-module "_set_max_atlas_texture_size" max-tex))
|
||||
;; Initialize Wasm Render Engine
|
||||
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
|
||||
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
||||
(when-let [t (wasm-aa-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_antialias_threshold" t))
|
||||
(when-let [t (wasm-viewport-interest-area-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_viewport_interest_area_threshold" t))
|
||||
(when-let [t (wasm-max-blocking-time-ms-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_max_blocking_time_ms" t))
|
||||
(when-let [t (wasm-node-batch-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_node_batch_threshold" t))
|
||||
(when-let [t (wasm-blur-downscale-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_blur_downscale_threshold" t))
|
||||
(when-let [max-tex (webgl/max-texture-size context)]
|
||||
(h/call wasm/internal-module "_set_max_atlas_texture_size" max-tex))
|
||||
|
||||
;; Set browser and canvas size only after initialization
|
||||
(h/call wasm/internal-module "_set_browser" browser)
|
||||
(set-canvas-size canvas)
|
||||
;; Set browser and canvas size only after initialization
|
||||
(h/call wasm/internal-module "_set_browser" browser)
|
||||
(set-canvas-size canvas)
|
||||
|
||||
;; Add event listeners for WebGL context lost
|
||||
(set! wasm/canvas canvas)
|
||||
(.addEventListener canvas "webglcontextlost" on-webgl-context-lost)
|
||||
(set! wasm/context-initialized? true)))
|
||||
;; Add event listeners for WebGL context lost
|
||||
(set! wasm/canvas canvas)
|
||||
(.addEventListener canvas "webglcontextlost" on-webgl-context-lost)
|
||||
(.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
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(try
|
||||
(set! wasm/context-initialized? false)
|
||||
([]
|
||||
(clear-canvas {}))
|
||||
([{:keys [lose-browser-context?]
|
||||
:or {lose-browser-context? true}}]
|
||||
(try
|
||||
(set! wasm/context-initialized? false)
|
||||
|
||||
;; Cancel any pending animation frame to prevent race conditions
|
||||
(when wasm/internal-frame-id
|
||||
(js/cancelAnimationFrame wasm/internal-frame-id)
|
||||
(set! wasm/internal-frame-id nil))
|
||||
;; Cancel any pending animation frame to prevent race conditions.
|
||||
(when wasm/internal-frame-id
|
||||
(js/cancelAnimationFrame wasm/internal-frame-id))
|
||||
|
||||
;; Reset render flags to prevent new renders from being scheduled
|
||||
(reset! pending-render false)
|
||||
(reset! shapes-loading? false)
|
||||
(reset! deferred-render? false)
|
||||
;; Reset render flags to prevent new renders from being scheduled.
|
||||
(reset! pending-render false)
|
||||
(reset! shapes-loading? 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/canvas
|
||||
(.removeEventListener wasm/canvas "webglcontextlost" on-webgl-context-lost)
|
||||
(set! wasm/canvas nil))
|
||||
(when (wasm/module-ready?)
|
||||
(free-gpu-resources)
|
||||
(h/call wasm/internal-module "_clean_up"))
|
||||
|
||||
;; Ensure the WebGL context is properly disposed so browsers do not keep
|
||||
;; accumulating active contexts between page switches.
|
||||
(when-let [gl (unchecked-get wasm/internal-module "GL")]
|
||||
(when-let [handle wasm/gl-context-handle]
|
||||
(try
|
||||
;; Ask the browser to release resources explicitly if available.
|
||||
(when-let [ctx wasm/gl-context]
|
||||
(when-let [lose-ext (.getExtension ^js ctx "WEBGL_lose_context")]
|
||||
(.loseContext ^js lose-ext)))
|
||||
(.deleteContext ^js gl handle)
|
||||
(finally
|
||||
(set! wasm/gl-context-handle nil)
|
||||
(set! wasm/gl-context nil)))))
|
||||
;; Ensure the WebGL context is properly disposed so browsers do not keep
|
||||
;; accumulating active contexts between page switches.
|
||||
(when-let [gl (unchecked-get wasm/internal-module "GL")]
|
||||
(when-let [handle wasm/gl-context-handle]
|
||||
(try
|
||||
;; For hard teardown we can explicitly lose browser context.
|
||||
;; For reload->reinit flows we skip this because immediate context
|
||||
;; recreation may fail on some browsers/GPUs while context is lost.
|
||||
(when lose-browser-context?
|
||||
(when-let [ctx wasm/gl-context]
|
||||
(when-let [lose-ext (.getExtension ^js ctx "WEBGL_lose_context")]
|
||||
(.loseContext ^js lose-ext))))
|
||||
(.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
|
||||
;; with hot-reload in develop
|
||||
(catch :default error
|
||||
(.error js/console error)))))
|
||||
(wasm-gesture/reset-after-wasm-reload!)
|
||||
(wasm/reset-context-state!)
|
||||
true
|
||||
|
||||
;; 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
|
||||
[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`
|
||||
(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
|
||||
#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"
|
||||
msgstr "Oops! The canvas context was lost"
|
||||
|
||||
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1051
|
||||
msgid "errors.webhooks.connection"
|
||||
msgstr "Connection error, URL not reacheable"
|
||||
@ -8973,6 +8975,12 @@ msgstr "Done"
|
||||
msgid "workspace.top-bar.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
|
||||
msgid "workspace.undo.empty"
|
||||
msgstr "There are no history changes so far"
|
||||
@ -9234,6 +9242,14 @@ msgstr ""
|
||||
msgid "workspace.versions.warning.text"
|
||||
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"
|
||||
msgstr "Oops! WebGL is not available"
|
||||
|
||||
|
||||
@ -8701,6 +8701,12 @@ msgstr "Hecho"
|
||||
msgid "workspace.top-bar.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
|
||||
msgid "workspace.undo.empty"
|
||||
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"
|
||||
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"
|
||||
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<()> {
|
||||
// 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();
|
||||
unsafe { STATE = None }
|
||||
unsafe {
|
||||
#[allow(static_mut_refs)]
|
||||
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()?;
|
||||
Ok(())
|
||||
}
|
||||
@ -1072,6 +1080,11 @@ pub extern "C" fn render_stats() {
|
||||
get_render_state().print_stats();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn free_gpu_resources() {
|
||||
get_render_state().free_gpu_resources();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_gl!();
|
||||
|
||||
@ -3775,10 +3775,17 @@ impl RenderState {
|
||||
pub fn print_stats(&self) {
|
||||
self.stats.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RenderState {
|
||||
fn drop(&mut self) {
|
||||
pub fn prepare_context_loss_cleanup(&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();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user