mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
♻️ Factor GL context initialization out of canvas init (#11931)
* ♻️ Factor GL context initialization out of canvas init - fixes setting of gl-context-handle to be after _init so an exception does not leave invalid state behind. - api.init-canvas-context uses a new webgl.init-context! - this decouples config from init, so we can reuse it in benchmarks - some helpers move to the webgl ns AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3 * 🐛 Release acquired GL context when renderer init fails - Properly release resources if init fails - Rethrown exceptions with the original cause - Fix vacuous check in clear-canvas AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3 * ✨ Test webgl context init and release AI-assisted-by: muse-spark, glm-5.3 * 💄 Address review comments * 💄 Drop `!` from some function names
This commit is contained in:
parent
dcf0b197eb
commit
f252439d84
@ -48,7 +48,7 @@
|
||||
(when (and (contains? #{:path :bool} type) (some? (:content shape)))
|
||||
(props/set-shape-path-content (:content shape)))))
|
||||
|
||||
(defn serialize-shape!
|
||||
(defn serialize-shape
|
||||
"Applies every host-independent WASM property of `shape`."
|
||||
[shape]
|
||||
(let [shape (svg-derived/apply-svg-derived shape)]
|
||||
@ -59,11 +59,11 @@
|
||||
|
||||
shape))
|
||||
|
||||
(defn serialize-shapes-batch!
|
||||
(defn serialize-shapes-batch
|
||||
"Structural batch upload plus per-shape svg-attrs/path tail.
|
||||
|
||||
Derives via svg-derived, uploads one `_set_shapes_batch` with `opts`,
|
||||
then selects each shape needing svg-attrs/path content via `use-shape!`
|
||||
then selects each shape needing svg-attrs/path content via `use-shape`
|
||||
and applies the tail. Host text/grid/image sequencing stays in callers.
|
||||
Returns the prepared vector for the downstream host-attrs loop."
|
||||
[shapes opts]
|
||||
|
||||
@ -23,12 +23,12 @@
|
||||
(defn set-shape!
|
||||
"Serializes a single shape into the WASM design state. The host-independent
|
||||
properties (base props, children, blur, shadows, svg-attrs, mask, bool-type,
|
||||
path geometry, grow-type) go through the shared `serialize-shape!` — the same
|
||||
path geometry, grow-type) go through the shared `serialize-shape` — the same
|
||||
code the workspace's `set-object` uses, so the two can't drift. Only the
|
||||
host-specific parts are handled here: fills/strokes (image bytes are provisioned
|
||||
separately) and text content (fonts provisioned separately)."
|
||||
[shape]
|
||||
(let [prepared (serialize-shape/serialize-shape! shape)
|
||||
(let [prepared (serialize-shape/serialize-shape shape)
|
||||
type (:type prepared)]
|
||||
(props/write-shape-fills! (:fills prepared))
|
||||
(when-not (= type :group)
|
||||
|
||||
@ -276,8 +276,6 @@
|
||||
(def text-editor-select-paragraph text-editor/text-editor-select-paragraph)
|
||||
(def text-editor-sync-content text-editor/text-editor-sync-content)
|
||||
|
||||
(def dpr
|
||||
(if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0))
|
||||
|
||||
(defn get-dpr
|
||||
"Returns the current device pixel ratio. Use instead of `dpr` wherever
|
||||
@ -1132,7 +1130,7 @@
|
||||
(h/call wasm/internal-module "_set_shape_hidden" hidden))
|
||||
|
||||
(defn clear-shape-fills!
|
||||
"Clear the fills of the currently-selected shape (call `use-shape!` first).
|
||||
"Clear the fills of the currently-selected shape (call `use-shape` first).
|
||||
Equivalent to `set-shape-fills` with an empty collection."
|
||||
[]
|
||||
(when (initialized?)
|
||||
@ -1590,7 +1588,7 @@
|
||||
`skip-fills-strokes?` is true, fill/stroke records were already in the batch;
|
||||
only image fetches remain.
|
||||
|
||||
Always `use-shape!` first: after a multi-shape batch the WASM current shape is
|
||||
Always `use-shape` first: after a multi-shape batch the WASM current shape is
|
||||
the last record in the chunk, not this shape."
|
||||
[shape skip-layout? & {:keys [skip-fills-strokes?] :or {skip-fills-strokes? false}}]
|
||||
(let [id (dm/get-prop shape :id)
|
||||
@ -1645,7 +1643,7 @@
|
||||
{:thumbnails [] :full [] :font-face-keys #{} :pending-font-face-keys #{}}
|
||||
(do
|
||||
(perf/begin-measure "set-object")
|
||||
(let [prepared (serialize-shape/serialize-shape! shape)
|
||||
(let [prepared (serialize-shape/serialize-shape shape)
|
||||
result (set-object-host-attrs prepared false)]
|
||||
(perf/end-measure "set-object")
|
||||
result))))
|
||||
@ -1813,7 +1811,7 @@
|
||||
end-index (min total (+ start-index BATCH_MAX_SHAPES))
|
||||
chunk (into [] (subvec (if (vector? shapes) shapes (vec shapes))
|
||||
start-index end-index))
|
||||
prepared (serialize-shape/serialize-shapes-batch!
|
||||
prepared (serialize-shape/serialize-shapes-batch
|
||||
chunk
|
||||
{:include-layout? true
|
||||
:include-fills-strokes? true})]
|
||||
@ -1962,7 +1960,7 @@
|
||||
(defn- set-objects-sync
|
||||
"Synchronously process all shapes (for small shape counts)."
|
||||
[shapes render-callback on-shapes-ready]
|
||||
(let [prepared (serialize-shape/serialize-shapes-batch!
|
||||
(let [prepared (serialize-shape/serialize-shapes-batch
|
||||
shapes
|
||||
{:include-layout? true
|
||||
:include-fills-strokes? true})
|
||||
@ -2336,13 +2334,6 @@
|
||||
(fn [resolve _reject]
|
||||
(timers/raf (fn [] (resolve nil))))))
|
||||
|
||||
(def ^:private default-context-options
|
||||
#js {:antialias false
|
||||
:depth true
|
||||
:stencil true
|
||||
:alpha true
|
||||
"preserveDrawingBuffer" true})
|
||||
|
||||
(defn resize-viewbox
|
||||
"Resizes the WASM viewbox. No-ops unless the GL context is live
|
||||
(`wasm/live?`), so callers cannot hit `_resize_viewbox` during
|
||||
@ -2377,54 +2368,19 @@
|
||||
(when (wasm/live?)
|
||||
(h/call wasm/internal-module "_set_render_options" (debug-flags) new-dpr)))
|
||||
|
||||
(def ^:private max-surface-size
|
||||
;; Must match `gpu_state::MAX_SURFACE_SIZE`.
|
||||
8192)
|
||||
|
||||
(defn- clamp-physical-size
|
||||
"Clamp physical pixel dimensions before assigning `canvas.width/height`.
|
||||
Rust `resize` applies the same cap and syncs the effective DPR from the
|
||||
real drawing buffer."
|
||||
[w h]
|
||||
(let [w (mth/max 1 w)
|
||||
h (mth/max 1 h)
|
||||
scale (mth/min 1 (/ max-surface-size w) (/ max-surface-size h))]
|
||||
[(mth/max 1 (mth/floor (* scale w)))
|
||||
(mth/max 1 (mth/floor (* scale h)))]))
|
||||
|
||||
(defn resize-offscreen-canvas!
|
||||
"Resize a persistent OffscreenCanvas to new physical-pixel dimensions and
|
||||
update the WASM render surfaces accordingly (via `_resize_viewbox`). The
|
||||
design state (shape pool) is preserved so `set-objects` is not needed again."
|
||||
[canvas new-physical-w new-physical-h]
|
||||
(when (wasm/live?)
|
||||
(let [dpr (get-dpr)
|
||||
[pw ph] (clamp-physical-size new-physical-w new-physical-h)]
|
||||
(let [dpr (get-dpr)
|
||||
[pw ph] (webgl/clamp-physical-size new-physical-w new-physical-h)]
|
||||
(set! (.-width canvas) pw)
|
||||
(set! (.-height canvas) ph)
|
||||
(set-render-options! dpr)
|
||||
(resize-viewbox (/ new-physical-w dpr) (/ new-physical-h dpr)))))
|
||||
|
||||
(defn- wasm-set-param-from-route-params-if-present
|
||||
[param-name]
|
||||
(when-let [value (wasm-get-numeric-value param-name)]
|
||||
(let [setter-name (str/concat "_set_" (name param-name))]
|
||||
(h/call wasm/internal-module setter-name value))))
|
||||
|
||||
(defn- canvas-css-size
|
||||
"Return canvas size in CSS pixels.
|
||||
|
||||
- For DOM canvases: use `clientWidth/clientHeight`.
|
||||
- For OffscreenCanvas: fall back to `width/height` (physical px) converted by DPR."
|
||||
[canvas dpr]
|
||||
(let [cw (.-clientWidth ^js canvas)
|
||||
ch (.-clientHeight ^js canvas)]
|
||||
(if (and (number? cw) (pos? cw)
|
||||
(number? ch) (pos? ch))
|
||||
[cw ch]
|
||||
[(/ (.-width ^js canvas) dpr)
|
||||
(/ (.-height ^js canvas) dpr)])))
|
||||
|
||||
(defn resize-canvas!
|
||||
"Sizes the canvas drawing buffer, the WASM render surface and the DPR from a
|
||||
single source of truth (the canvas CSS client size) so the GL framebuffer
|
||||
@ -2436,10 +2392,10 @@
|
||||
(resize-canvas! canvas (get-dpr)))
|
||||
([canvas new-dpr]
|
||||
(when (wasm/live?)
|
||||
(let [[css-w css-h] (canvas-css-size canvas new-dpr)
|
||||
css-w (mth/max 1 css-w)
|
||||
css-h (mth/max 1 css-h)
|
||||
[phys-w phys-h] (clamp-physical-size
|
||||
(let [[css-w css-h] (webgl/canvas-css-size canvas new-dpr)
|
||||
css-w (mth/max 1 css-w)
|
||||
css-h (mth/max 1 css-h)
|
||||
[phys-w phys-h] (webgl/clamp-physical-size
|
||||
(mth/floor (* css-w new-dpr))
|
||||
(mth/floor (* css-h new-dpr)))]
|
||||
(set! (.-width ^js canvas) phys-w)
|
||||
@ -2503,45 +2459,40 @@
|
||||
[canvas]
|
||||
(if-not (wasm/module-ready?)
|
||||
false
|
||||
(let [gl (unchecked-get wasm/internal-module "GL")
|
||||
flags (debug-flags)
|
||||
(let [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)
|
||||
dpr (get-dpr)
|
||||
[css-w css-h] (canvas-css-size canvas dpr)
|
||||
browser (sr/translate-browser cf/browser)
|
||||
dpr (get-dpr)
|
||||
[css-w css-h] (webgl/canvas-css-size canvas dpr)
|
||||
;; Avoid 0×0 Skia/GL surfaces (crashes on some browsers).
|
||||
css-w (mth/max 1 css-w)
|
||||
css-h (mth/max 1 css-h)
|
||||
css-w (mth/max 1 css-w)
|
||||
css-h (mth/max 1 css-h)
|
||||
params {:antialias_threshold (wasm-get-numeric-value :antialias_threshold)
|
||||
:viewport_interest_area_threshold (wasm-get-numeric-value :viewport_interest_area_threshold)
|
||||
:max_blocking_time_ms (wasm-get-numeric-value :max_blocking_time_ms)
|
||||
:node_batch_threshold (wasm-get-numeric-value :node_batch_threshold)
|
||||
:blur_downscale_threshold (wasm-get-numeric-value :blur_downscale_threshold)}
|
||||
result (webgl/init-context canvas {:module wasm/internal-module
|
||||
:context-id context-id
|
||||
:css-width css-w
|
||||
:css-height css-h
|
||||
:dpr dpr
|
||||
:flags flags
|
||||
:browser browser
|
||||
:params params})
|
||||
context-init? (some? result)
|
||||
can-listen? (fn? (.-addEventListener ^js canvas))]
|
||||
(when-not (nil? context)
|
||||
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
|
||||
(.makeContextCurrent ^js gl handle)
|
||||
(when result
|
||||
(let [{:keys [context handle]} result]
|
||||
(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")
|
||||
|
||||
;; Initialize Wasm Render Engine
|
||||
(h/call wasm/internal-module "_init" css-w css-h)
|
||||
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
||||
|
||||
;; Configurable parameters.
|
||||
(wasm-set-param-from-route-params-if-present :antialias_threshold)
|
||||
(wasm-set-param-from-route-params-if-present :viewport_interest_area_threshold)
|
||||
(wasm-set-param-from-route-params-if-present :max_blocking_time_ms)
|
||||
(wasm-set-param-from-route-params-if-present :node_batch_threshold)
|
||||
(wasm-set-param-from-route-params-if-present :blur_downscale_threshold)
|
||||
|
||||
;; Set browser after `_init`; mark live before sizing so the
|
||||
;; `init-context` already ran `_init` through `_set_browser` in
|
||||
;; order. Publish the handles, then mark live before sizing so the
|
||||
;; guarded `resize-*` helpers can run (they require `wasm/live?`,
|
||||
;; which is true here even while `reloading?` still blocks app callers).
|
||||
(h/call wasm/internal-module "_set_browser" browser)
|
||||
|
||||
;; Add event listeners for WebGL context lost
|
||||
(set! wasm/canvas canvas)
|
||||
;; Add event listeners for WebGL context lost
|
||||
(when can-listen?
|
||||
(.addEventListener canvas "webglcontextlost" webgl-context-lost-listener)
|
||||
(.addEventListener canvas "webglcontextrestored" webgl-context-restored-listener))
|
||||
@ -2587,19 +2538,18 @@
|
||||
|
||||
;; 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)))))
|
||||
(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))))
|
||||
(webgl/delete-context! wasm/internal-module handle)
|
||||
(catch :default dispose-error
|
||||
(.error js/console dispose-error))))
|
||||
|
||||
(wasm-gesture/reset-after-wasm-reload!)
|
||||
(wasm/reset-context-state!)
|
||||
|
||||
@ -5,12 +5,142 @@
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.render-wasm.api.webgl
|
||||
"WebGL utilities for pixel capture and rendering"
|
||||
"WebGL context creation and utilities for pixel capture and rendering"
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
[app.common.render-wasm.helpers :as h]
|
||||
[app.common.render-wasm.wasm :as wasm]
|
||||
[promesa.core :as p]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Context
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def default-context-options
|
||||
"Shared WebGL2 context attributes"
|
||||
#js {:antialias false
|
||||
:depth true
|
||||
:stencil true
|
||||
:alpha true
|
||||
:preserveDrawingBuffer true})
|
||||
|
||||
(def ^:const ^:private max-surface-size
|
||||
;; Must match `gpu_state::MAX_SURFACE_SIZE`.
|
||||
8192)
|
||||
|
||||
(defn canvas-css-size
|
||||
"Return canvas size in CSS pixels.
|
||||
|
||||
- For DOM canvases: use `clientWidth/clientHeight`.
|
||||
- For OffscreenCanvas: fall back to `width/height` (physical px) converted by DPR."
|
||||
[canvas dpr]
|
||||
(let [cw (.-clientWidth ^js canvas)
|
||||
ch (.-clientHeight ^js canvas)]
|
||||
(if (and (number? cw) (pos? cw)
|
||||
(number? ch) (pos? ch))
|
||||
[cw ch]
|
||||
[(/ (.-width ^js canvas) dpr)
|
||||
(/ (.-height ^js canvas) dpr)])))
|
||||
|
||||
(defn clamp-physical-size
|
||||
"Clamp physical pixel dimensions before assigning `canvas.width/height`.
|
||||
Rust `resize` applies the same cap and syncs the effective DPR from the
|
||||
real drawing buffer."
|
||||
[w h]
|
||||
(let [w (mth/max 1 w)
|
||||
h (mth/max 1 h)
|
||||
scale (mth/min 1 (/ max-surface-size w) (/ max-surface-size h))]
|
||||
[(mth/max 1 (mth/floor (* scale w)))
|
||||
(mth/max 1 (mth/floor (* scale h)))]))
|
||||
|
||||
(defn delete-context!
|
||||
"Deletes an Emscripten GL context by handle.Never throws."
|
||||
[module handle]
|
||||
(when (some? handle)
|
||||
(try
|
||||
;; We look the method up by string key bc dot access would let `:advanced`
|
||||
;; builds rename the Emscripten method
|
||||
(let [gl (unchecked-get module "GL")
|
||||
delete-context (unchecked-get gl "deleteContext")]
|
||||
(when (fn? delete-context)
|
||||
(.call ^js delete-context gl handle)))
|
||||
(catch :default _))))
|
||||
|
||||
(defn release-context!
|
||||
"Releases renderer state and a WebGL context owned by the caller.
|
||||
`handle` may be nil if Emscripten registration failed. Each release is
|
||||
attempted even when an earlier one throws; cleanup must preserve the
|
||||
original initialization failure. Writes no global lifecycle state."
|
||||
[module context handle]
|
||||
(when (some? handle)
|
||||
(try
|
||||
(h/call module "_clean_up")
|
||||
(catch :default _)))
|
||||
(try
|
||||
(when-let [ext (.getExtension ^js context "WEBGL_lose_context")]
|
||||
(.loseContext ^js ext))
|
||||
(catch :default _))
|
||||
(delete-context! module handle))
|
||||
|
||||
(defn init-context
|
||||
"Create a WebGL2 context on `canvas` and run the WASM renderer init sequence.
|
||||
|
||||
Expects all values resolved by the caller. Writes no `wasm/` atoms and installs
|
||||
no listeners.
|
||||
|
||||
- `opts` has keys `module`, `context-id`, `css-width`, `css-height`, `dpr`,
|
||||
`flags`, `browser`, and `params`.
|
||||
- `module` is the Emscripten module object holding `GL`, `_init`,
|
||||
`_set_render_options`, and `_set_browser`. `context-id` is `\"webgl2\"`
|
||||
(or `\"fail\"` to force the debug failure path).
|
||||
- `css-width` and `css-height` are CSS pixel sizes already probed and clamped
|
||||
to at least 1.
|
||||
- `params` maps tunable keywords to values (which pass to `_set_*` unchecked):
|
||||
- `:antialias_threshold`
|
||||
- `:viewport_interest_area_threshold`
|
||||
- `:max_blocking_time_ms`
|
||||
- `:node_batch_threshold`
|
||||
- `:blur_downscale_threshold`
|
||||
|
||||
Returns `{:context :handle}` or nil when `getContext` returns nil.
|
||||
On failure, releases acquired resources before rethrowing. On success,
|
||||
ownership transfers to the caller."
|
||||
[canvas {:keys [module context-id css-width css-height dpr flags browser params]}]
|
||||
(let [gl (unchecked-get module "GL")
|
||||
context (.getContext ^js canvas context-id default-context-options)]
|
||||
(when-not (nil? context)
|
||||
(let [handle (volatile! nil)]
|
||||
(try
|
||||
(vreset! handle (.registerContext ^js gl context #js {"majorVersion" 2}))
|
||||
(.makeContextCurrent ^js gl @handle)
|
||||
|
||||
;; 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 module "_init" css-width css-height)
|
||||
(h/call module "_set_render_options" flags dpr)
|
||||
|
||||
;; Configurable parameters, fixed order to match the editor path.
|
||||
(when-let [value (:antialias_threshold params)]
|
||||
(h/call module "_set_antialias_threshold" value))
|
||||
(when-let [value (:viewport_interest_area_threshold params)]
|
||||
(h/call module "_set_viewport_interest_area_threshold" value))
|
||||
(when-let [value (:max_blocking_time_ms params)]
|
||||
(h/call module "_set_max_blocking_time_ms" value))
|
||||
(when-let [value (:node_batch_threshold params)]
|
||||
(h/call module "_set_node_batch_threshold" value))
|
||||
(when-let [value (:blur_downscale_threshold params)]
|
||||
(h/call module "_set_blur_downscale_threshold" value))
|
||||
|
||||
(h/call module "_set_browser" browser)
|
||||
{:context context
|
||||
:handle @handle}
|
||||
(catch :default cause
|
||||
(release-context! module context @handle)
|
||||
(throw cause)))))))
|
||||
|
||||
(defn get-webgl-context
|
||||
"Gets the WebGL context from the WASM module"
|
||||
[]
|
||||
@ -23,6 +153,10 @@
|
||||
(when current-ctx
|
||||
(.-GLctx ^js current-ctx)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Bitmap rendering
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn create-webgl-texture-from-image
|
||||
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
|
||||
[gl image-element]
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.render-wasm.serialization-test
|
||||
"Routing tests for `serialize-shape!` and `serialize-shapes-batch!`.
|
||||
"Routing tests for `serialize-shape` and `serialize-shapes-batch`.
|
||||
|
||||
A shared `tail-cases` table states, per shape, whether the svg-attrs/path
|
||||
tail must fire. The pure `needs-shape-tail?` predicate is tested against
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
(defn- with-ffi-stubs*
|
||||
"Stubs the single-arity FFI boundary fns (`flush-shapes-batch!`,
|
||||
`use-shape!`, `set-shape-svg-attrs`, `set-shape-path-content`).
|
||||
`use-shape`, `set-shape-svg-attrs`, `set-shape-path-content`).
|
||||
`set-shape-upload!` is deliberately left real: it is a trivial one-liner
|
||||
over the stubbed flush, so the single test exercises its actual delegation
|
||||
and default opts. A plain fn `set!` onto a multi-arity var breaks under
|
||||
@ -121,7 +121,7 @@
|
||||
:svg-attrs (fn [attrs] (swap! events conj [:svg-attrs @current attrs]) nil)
|
||||
:path (fn [content] (swap! events conj [:path @current content]) nil)}
|
||||
result (with-ffi-stubs* stubs
|
||||
#(serialize-shape/serialize-shapes-batch! shapes opts))
|
||||
#(serialize-shape/serialize-shapes-batch shapes opts))
|
||||
flushed (first @flush-calls)]
|
||||
(t/is (= 1 (count @flush-calls)) "exactly one flush")
|
||||
(t/is (= opts (:opts flushed)) "flush carries passed opts")
|
||||
@ -140,7 +140,7 @@
|
||||
:svg-attrs (fn [attrs] (swap! svg-applied conj attrs) nil)
|
||||
:path (fn [content] (swap! path-applied conj content) nil)}]
|
||||
(let [expected (svg-derived/apply-svg-derived shape)
|
||||
result (with-ffi-stubs* stubs #(serialize-shape/serialize-shape! shape))]
|
||||
result (with-ffi-stubs* stubs #(serialize-shape/serialize-shape shape))]
|
||||
(t/is (= [{:shapes [expected] :opts {:include-layout? false}}] @flush-calls)
|
||||
(str "single upload carries derived shape without layout for " (:type shape)))
|
||||
(t/is (= expected result)
|
||||
@ -161,7 +161,7 @@
|
||||
:svg-attrs (fn [_] (t/is false "no svg-attrs write on empty batch") nil)
|
||||
:path (fn [_] (t/is false "no path write on empty batch") nil)}
|
||||
result (with-ffi-stubs* stubs
|
||||
#(serialize-shape/serialize-shapes-batch! [] opts))]
|
||||
#(serialize-shape/serialize-shapes-batch [] opts))]
|
||||
(t/is (= [] result) "returns empty prepared")
|
||||
(t/is (empty? @flush-calls) "no flush on empty batch")
|
||||
(t/is (empty? @selected) "no select on empty batch")))
|
||||
|
||||
105
frontend/test/frontend_tests/render_wasm/webgl_test.cljs
Normal file
105
frontend/test/frontend_tests/render_wasm/webgl_test.cljs
Normal file
@ -0,0 +1,105 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.render-wasm.webgl-test
|
||||
"Unit tests for `app.render-wasm.api.webgl` context init and release."
|
||||
(:require
|
||||
[app.render-wasm.api.webgl :as webgl]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(defn- fake-env
|
||||
"Builds `{canvas context module calls}` fakes.
|
||||
- `calls` records `:register`, `:delete`, `:clean` and `:lose` events.
|
||||
- Options:
|
||||
- `:throw-in` makes that module fn throw (`:_init`, `:_set_browser`, ...),
|
||||
- `:register-throws?` fails Emscripten registration (no handle exists),
|
||||
- `:nil-context?` makes `getContext` return nil (nothing is acquired)."
|
||||
[{:keys [throw-in register-throws? nil-context?]}]
|
||||
(let [calls (atom [])
|
||||
lose (fn [] (swap! calls conj [:lose]) nil)
|
||||
context #js {:getExtension (fn [name]
|
||||
(when (= name "WEBGL_lose_context")
|
||||
#js {:loseContext lose}))}
|
||||
gl #js {:registerContext (fn [_ _]
|
||||
(swap! calls conj [:register])
|
||||
(when register-throws?
|
||||
(throw (js/Error. "register failed")))
|
||||
7)
|
||||
:makeContextCurrent (fn [_] nil)
|
||||
:deleteContext (fn [handle]
|
||||
(swap! calls conj [:delete handle])
|
||||
nil)}
|
||||
thrower (fn [name]
|
||||
(fn [& _]
|
||||
(when (= name throw-in)
|
||||
(throw (js/Error. (str "fake " (pr-str name) " failure"))))
|
||||
nil))
|
||||
module #js {:GL gl
|
||||
:_init (thrower :_init)
|
||||
:_set_render_options (thrower :_set_render_options)
|
||||
:_set_browser (thrower :_set_browser)
|
||||
:_clean_up (fn [& _] (swap! calls conj [:clean]) nil)}
|
||||
canvas #js {:getContext (fn [_ _] (when-not nil-context? context))}]
|
||||
{:calls calls :canvas canvas :context context :module module}))
|
||||
|
||||
(defn- init-opts
|
||||
[module]
|
||||
{:module module
|
||||
:context-id "webgl2"
|
||||
:css-width 100
|
||||
:css-height 100
|
||||
:dpr 1
|
||||
:flags 0
|
||||
:browser 0
|
||||
:params {}})
|
||||
|
||||
(defn- init-failure
|
||||
"Runs `init-context` against `env`, returning the thrown cause."
|
||||
[{:keys [canvas module]}]
|
||||
(try
|
||||
(webgl/init-context canvas (init-opts module))
|
||||
nil
|
||||
(catch :default cause
|
||||
cause)))
|
||||
|
||||
(defn- effect-count
|
||||
[calls effect]
|
||||
(count (filter #(= effect (first %)) calls)))
|
||||
|
||||
(t/deftest init-failure-releases-the-acquired-context
|
||||
(let [{:keys [canvas module calls]} (fake-env {:throw-in :_set_browser})
|
||||
failure (init-failure {:canvas canvas :module module})]
|
||||
(t/is (some? failure) "the original failure propagates")
|
||||
(t/is (= "_set_browser" (:fn (ex-data failure)))
|
||||
"the cause keeps the failing fn")
|
||||
(t/is (= 1 (effect-count @calls :clean)) "renderer state is released")
|
||||
(t/is (some #{[:delete 7]} @calls) "the registered handle is deleted")
|
||||
(t/is (some #{[:lose]} @calls) "the browser context is released")))
|
||||
|
||||
(t/deftest init-success-returns-context-and-handle
|
||||
(let [{:keys [canvas context module calls]} (fake-env {})
|
||||
result (webgl/init-context canvas (init-opts module))]
|
||||
(t/is (= 7 (:handle result)))
|
||||
(t/is (identical? context (:context result)))
|
||||
(t/is (zero? (effect-count @calls :clean)) "no release on success")
|
||||
(t/is (zero? (effect-count @calls :delete)) "no release on success")
|
||||
(t/is (zero? (effect-count @calls :lose)) "no release on success")))
|
||||
|
||||
(t/deftest nil-context-returns-nil-without-releasing
|
||||
(let [{:keys [canvas module calls]} (fake-env {:nil-context? true})]
|
||||
(t/is (nil? (webgl/init-context canvas (init-opts module))))
|
||||
(t/is (empty? @calls) "nothing acquired, nothing released")))
|
||||
|
||||
(t/deftest registration-failure-releases-browser-context-only
|
||||
(let [{:keys [canvas module calls]} (fake-env {:register-throws? true})
|
||||
failure (init-failure {:canvas canvas :module module})]
|
||||
(t/is (some? failure) "the original failure propagates")
|
||||
(t/is (zero? (effect-count @calls :clean))
|
||||
"no renderer state exists without registration")
|
||||
(t/is (zero? (effect-count @calls :delete))
|
||||
"no handle exists to delete")
|
||||
(t/is (= 1 (effect-count @calls :lose))
|
||||
"the browser context is still released")))
|
||||
@ -90,6 +90,7 @@
|
||||
[frontend-tests.render-wasm.text-editor-apply-styles-test]
|
||||
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
||||
[frontend-tests.render-wasm.text-paste-test]
|
||||
[frontend-tests.render-wasm.webgl-test]
|
||||
[frontend-tests.router-test]
|
||||
[frontend-tests.svg-fills-test]
|
||||
[frontend-tests.svg-filters-test]
|
||||
@ -239,6 +240,7 @@
|
||||
'frontend-tests.render-wasm.text-editor-apply-styles-test
|
||||
'frontend-tests.render-wasm.text-editor-caret-color-test
|
||||
'frontend-tests.render-wasm.text-paste-test
|
||||
'frontend-tests.render-wasm.webgl-test
|
||||
'frontend-tests.router-test
|
||||
'frontend-tests.svg-fills-test
|
||||
'frontend-tests.svg-filters-test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user