Elena Torró 8124ecf148
⚡ Improve canvas snapshot and nested drop shadows calculation (#11766)
* ⚡ Capture canvas snapshot only when the renderer is idle

* ⚡ Skip descendant silhouettes for frames with nested drop shadows

* 🔧 Add a profiling build mode for render-wasm
2026-09-18 09:21:52 +02:00

183 lines
8.4 KiB
Clojure

;; 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 app.render-wasm.api.webgl
"WebGL utilities for pixel capture and rendering"
(:require
[app.common.logging :as log]
[app.common.render-wasm.wasm :as wasm]
[promesa.core :as p]))
(defn get-webgl-context
"Gets the WebGL context from the WASM module"
[]
(when (wasm/live?)
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
(when gl-obj
;; Get the current WebGL context from Emscripten
;; The GL object has a currentContext property that contains the context handle
(let [current-ctx (.-currentContext ^js gl-obj)]
(when current-ctx
(.-GLctx ^js current-ctx)))))))
(defn create-webgl-texture-from-image
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
[gl image-element]
(let [texture (.createTexture ^js gl)]
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
texture))
;; FIXME: temporary function until we are able to keep the same <canvas> across pages.
(defn- draw-imagedata-to-webgl
"Draws ImageData to a WebGL2 context by creating a texture"
[gl image-data]
(let [width (.-width ^js image-data)
height (.-height ^js image-data)
texture (.createTexture ^js gl)]
;; Bind texture and set parameters
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-data)
;; Set up viewport
(.viewport ^js gl 0 0 width height)
;; Vertex & Fragment shaders
;; Since we are only calling this function once (on page switch), we don't need
;; to cache the compiled shaders somewhere else (cannot be reused in a differen context).
(let [vertex-shader-source "#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}"
fragment-shader-source "#version 300 es
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
fragColor = texture(u_texture, v_texCoord);
}"
vertex-shader (.createShader ^js gl (.-VERTEX_SHADER ^js gl))
fragment-shader (.createShader ^js gl (.-FRAGMENT_SHADER ^js gl))
program (.createProgram ^js gl)]
(.shaderSource ^js gl vertex-shader vertex-shader-source)
(.compileShader ^js gl vertex-shader)
(when-not (.getShaderParameter ^js gl vertex-shader (.-COMPILE_STATUS ^js gl))
(log/error :hint "Vertex shader compilation failed"
:log (.getShaderInfoLog ^js gl vertex-shader)))
(.shaderSource ^js gl fragment-shader fragment-shader-source)
(.compileShader ^js gl fragment-shader)
(when-not (.getShaderParameter ^js gl fragment-shader (.-COMPILE_STATUS ^js gl))
(log/error :hint "Fragment shader compilation failed"
:log (.getShaderInfoLog ^js gl fragment-shader)))
(.attachShader ^js gl program vertex-shader)
(.attachShader ^js gl program fragment-shader)
(.linkProgram ^js gl program)
(if (.getProgramParameter ^js gl program (.-LINK_STATUS ^js gl))
(do
(.useProgram ^js gl program)
;; Create full-screen quad vertices (normalized device coordinates)
(let [position-location (.getAttribLocation ^js gl program "a_position")
texcoord-location (.getAttribLocation ^js gl program "a_texCoord")
position-buffer (.createBuffer ^js gl)
texcoord-buffer (.createBuffer ^js gl)
positions #js [-1.0 -1.0 1.0 -1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0]
texcoords #js [0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0 1.0]]
;; Set up position buffer
(.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) position-buffer)
(.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. positions) (.-STATIC_DRAW ^js gl))
(.enableVertexAttribArray ^js gl position-location)
(.vertexAttribPointer ^js gl position-location 2 (.-FLOAT ^js gl) false 0 0)
;; Set up texcoord buffer
(.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) texcoord-buffer)
(.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. texcoords) (.-STATIC_DRAW ^js gl))
(.enableVertexAttribArray ^js gl texcoord-location)
(.vertexAttribPointer ^js gl texcoord-location 2 (.-FLOAT ^js gl) false 0 0)
;; Set texture uniform
(.activeTexture ^js gl (.-TEXTURE0 ^js gl))
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(let [texture-location (.getUniformLocation ^js gl program "u_texture")]
(.uniform1i ^js gl texture-location 0))
;; draw
(.drawArrays ^js gl (.-TRIANGLES ^js gl) 0 6)
;; cleanup
(.deleteBuffer ^js gl position-buffer)
(.deleteBuffer ^js gl texcoord-buffer)
(.deleteShader ^js gl vertex-shader)
(.deleteShader ^js gl fragment-shader)
(.deleteProgram ^js gl program)))
(log/error :hint "Program linking failed"
:log (.getProgramInfoLog ^js gl program)))
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
(.deleteTexture ^js gl texture))))
(defn capture-canvas-snapshot
"Captures the current viewport canvas as an `ImageBitmap` and stores it in
`wasm/canvas-snapshot`. It avoids `canvas.toBlob`'s PNG encoding, but the
readback itself is not free: on Firefox, `createImageBitmap` on a WebGL canvas
takes a synchronous back-buffer snapshot over IPC before the promise is even
created, which blocks the main thread until the queued GL commands drain.
Callers must only invoke it when the renderer is idle.
Returns a promise resolving to the ImageBitmap (or nil)."
[]
(if-let [^js canvas wasm/canvas]
(-> (js/createImageBitmap canvas)
(p/then (fn [^js bitmap]
(set! wasm/canvas-snapshot bitmap)
bitmap))
(p/catch (fn [_] nil)))
(p/resolved nil)))
(defn draw-thumbnail-to-canvas
"Loads an image from `uri` and draws it stretched to fill the WebGL canvas.
Returns a promise that resolves to true if drawn, false otherwise."
[uri]
(if (and uri wasm/canvas wasm/gl-context)
(p/create
(fn [resolve _reject]
(let [img (js/Image.)]
(set! (.-crossOrigin img) "anonymous")
(set! (.-onload img)
(fn []
(if wasm/gl-context
(let [gl wasm/gl-context
width (.-width wasm/canvas)
height (.-height wasm/canvas)
;; Draw image to an offscreen 2D canvas, scaled to fill
canvas2d (js/OffscreenCanvas. width height)
ctx2d (.getContext canvas2d "2d")]
(.drawImage ctx2d img 0 0 width height)
(let [image-data (.getImageData ctx2d 0 0 width height)]
(draw-imagedata-to-webgl gl image-data))
(resolve true))
(resolve false))))
(set! (.-onerror img)
(fn [_]
(resolve false)))
(set! (.-src img) uri))))
(p/resolved false)))