mirror of
https://github.com/penpot/penpot.git
synced 2026-11-06 10:00:38 +00:00
- Improve readme - Migrate scene cases to measured commands and remove obsolete descriptors. - Preserve exact camera targets, including very small positive scales. AI-assisted-by: gpt-6.1-sol
425 lines
19 KiB
Clojure
425 lines
19 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 benches.render-wasm.runtime.protocol
|
|
"Rendering until completion and timing camera movement.
|
|
|
|
A frame type describes the progress of one renderer call. A render drain is
|
|
a sequence of calls that ends when the renderer returns Full. Each call
|
|
produces a slice: its timestamp, flags, frame type and duration. Partial and
|
|
ViewportReady schedule another call on the next animation frame. Full marks
|
|
renderer submission of the completed request. GPU execution and display
|
|
proceed asynchronously.
|
|
|
|
A view contains zoom `:scale` and offsets `:x` and `:y`. A cached preview
|
|
draws from existing tiles, the fixed regions of the scene that the renderer
|
|
caches. A camera session starts an interaction, applies views with cached
|
|
previews, waits for the requested settling time, ends the interaction and
|
|
renders until Full. Each preview waits for an animation frame. Explicit
|
|
pauses can extend the active interval between camera start and settling.
|
|
Camera finalization starts immediately before ending the interaction and
|
|
ends at the final renderer return.
|
|
|
|
## Hooks and errors
|
|
|
|
Hooks are functions that supply clock, scheduling and renderer operations:
|
|
|
|
- `:now` returns a clock reading in milliseconds.
|
|
- `:frame` resolves a Promise with the next animation frame's timestamp.
|
|
- `:sleep` accepts a requested wait in milliseconds and returns its Promise.
|
|
- `:check` is a guard. Returning authorizes work; throwing rejects it. It can
|
|
report cancellation, an expired deadline, loss of the WebGL2 graphics
|
|
context or replacement of the active renderer owner. An owner is the
|
|
execution authorized to use the renderer.
|
|
- `:render` accepts a timestamp and flags and returns a frame type.
|
|
- `:render-from-cache` draws one cached preview.
|
|
- `:set-view` accepts a view containing scale and both offsets.
|
|
- `:set-view-start` and `:set-view-end` start and end the interaction.
|
|
|
|
Guards run before work, after awaited frames and waits, and around render
|
|
calls. Guard and renderer errors reject the returned Promises. Errors keep
|
|
available traces of render calls and pauses under `:partial`.
|
|
|
|
Frame types come from `app.common.render-wasm.wasm`. Partial and ViewportReady
|
|
continue rendering, Full completes it, and other values reject with
|
|
`::unexpected-frame-type`. Each subsequent call receives the preceding frame
|
|
type as its flags. SyncTiles asks the renderer to update which tiles the
|
|
current view requires.
|
|
View restoration and camera finalization use that flag for their first call
|
|
and start immediately. A drain can also wait for an animation frame before
|
|
its first call. Later calls wait for animation frames.
|
|
|
|
## Render calls and measured durations
|
|
|
|
A trace records individual calls and waits. Durations use milliseconds and
|
|
preserve raw numeric values, including zero. A drain origin is the clock
|
|
reading from which its completion durations start.
|
|
|
|
- `:slices` contains one entry per renderer call:
|
|
- `:timestamp`: the animation frame's timestamp or the current clock
|
|
reading for an immediate call.
|
|
- `:flags`: the flags passed to the renderer.
|
|
- `:frame-type`: the returned frame type.
|
|
- `:duration-ms`: elapsed time during the call.
|
|
- `:cached-slices` contains one entry per cached preview:
|
|
- `:timestamp`: the animation frame's timestamp for the view update.
|
|
- `:duration-ms`: elapsed time during the cached render call.
|
|
- `:view-and-preview-ms`: elapsed time for the view update and preview.
|
|
- `:viewport-ready-ms` measures from the drain origin to the first call that
|
|
returns ViewportReady or Full. It equals `:full-ms` when the first such
|
|
call returns Full.
|
|
- `:full-ms` measures from the drain origin to the call that returns Full.
|
|
- `:end-ms` is the clock reading immediately after that final call returns.
|
|
- `:interact-ms` measures from immediately before camera start through the
|
|
final renderer return.
|
|
- `:active-ms` measures from camera start through the last cached preview and
|
|
its guard check, or through an explicit pause that follows the preview.
|
|
- `:authored-sleeps` contains requested and observed durations of explicit
|
|
pauses during the active interval.
|
|
- `:settling-requested-ms` is the requested settling wait.
|
|
- `:settling-actual-ms` measures from the end of the active interval to the
|
|
start of camera finalization.
|
|
- `:set-view-end-ms` measures the call that ends the interaction.
|
|
- `:time-to-viewport-ready-ms` measures from camera finalization to the first
|
|
ViewportReady or Full result. `:time-to-full-ms` ends at Full. Both include
|
|
the call that ends the interaction.
|
|
- `:last-input-to-full-ms` measures from immediately before the last view
|
|
update to the final renderer return. The initial input time equals camera
|
|
start; each view update replaces it."
|
|
(:require-macros
|
|
[benches.render-wasm.runtime.protocol :refer [resolve-or-reject resolve-or-reject-with]])
|
|
(:require
|
|
[app.common.render-wasm.wasm :as wasm]
|
|
[benches.render-wasm.errors :as errors]))
|
|
|
|
|
|
(defn- partial-error
|
|
"Returns an error with completed traces under `:partial`.
|
|
Trace fields already present in the error take precedence over `evidence`."
|
|
[cause evidence]
|
|
(errors/with-context cause (errors/message-of cause "renderer protocol failed")
|
|
{:partial evidence}))
|
|
|
|
(defn- next-render-timestamp
|
|
"Resolves to the current clock reading for an immediate render, or to the
|
|
next animation frame's timestamp."
|
|
[{:keys [now frame]} immediate?]
|
|
(if immediate?
|
|
(js/Promise.resolve (now))
|
|
(frame)))
|
|
|
|
|
|
(defn- render-slice
|
|
"Returns the updated drain state, frame type and endpoint for one render call.
|
|
The state includes the completed call in its `:slices` trace."
|
|
[{:keys [now check render]}
|
|
{:keys [flags] :as state}
|
|
timestamp]
|
|
(check)
|
|
(let [started-ms (now)
|
|
frame-type (render timestamp flags)
|
|
ended-ms (now)
|
|
slice {:timestamp timestamp
|
|
:flags flags
|
|
:frame-type frame-type
|
|
:duration-ms (- ended-ms started-ms)}]
|
|
(try
|
|
(check)
|
|
(catch :default cause
|
|
(throw (partial-error cause {:slices (conj (:slices state) slice)}))))
|
|
{:state (update state :slices conj slice)
|
|
:frame-type frame-type
|
|
:ended-ms ended-ms}))
|
|
|
|
|
|
(defn- valid-frame-type?
|
|
"Recognizes the progressive and final renderer frame types."
|
|
[frame-type]
|
|
(or (= frame-type wasm/FRAME_TYPE_PARTIAL)
|
|
(= frame-type wasm/FRAME_TYPE_VIEWPORT_READY)
|
|
(= frame-type wasm/FRAME_TYPE_FULL)))
|
|
|
|
|
|
(defn- viewport-ready-time
|
|
"Returns the elapsed time to the first ViewportReady or Full result."
|
|
[current-ready-ms frame-type ended-ms origin-ms]
|
|
(or current-ready-ms
|
|
(when (or (= frame-type wasm/FRAME_TYPE_VIEWPORT_READY)
|
|
(= frame-type wasm/FRAME_TYPE_FULL))
|
|
(- ended-ms origin-ms))))
|
|
|
|
|
|
(defn- advance-drain-state
|
|
"Updates the continuation flags and completion time for a returned frame type.
|
|
An unexpected type throws an error containing the completed render calls."
|
|
[{:keys [viewport-ready-ms] :as state}
|
|
frame-type
|
|
ended-ms
|
|
origin-ms]
|
|
(when-not (valid-frame-type? frame-type)
|
|
(throw (ex-info (str "unexpected frame type: " (pr-str frame-type))
|
|
{:type ::unexpected-frame-type
|
|
:frame-type frame-type
|
|
:partial (select-keys state [:slices :viewport-ready-ms])})))
|
|
(assoc state
|
|
:flags frame-type
|
|
:viewport-ready-ms
|
|
(viewport-ready-time viewport-ready-ms
|
|
frame-type
|
|
ended-ms
|
|
origin-ms)))
|
|
|
|
(defn- drain-complete?
|
|
"Returns whether the renderer reported Full completion."
|
|
[frame-type]
|
|
(= frame-type wasm/FRAME_TYPE_FULL))
|
|
|
|
(defn- drain-result
|
|
"Returns traces of render calls, the final clock reading and completion durations
|
|
measured from the drain origin."
|
|
[{:keys [slices viewport-ready-ms]}
|
|
ended-ms
|
|
origin-ms]
|
|
{:slices slices
|
|
:end-ms ended-ms
|
|
:viewport-ready-ms viewport-ready-ms
|
|
:full-ms (- ended-ms origin-ms)})
|
|
|
|
|
|
(defn drain
|
|
"Renders until a call returns Full and resolves to its traces and durations.
|
|
|
|
Options:
|
|
- `:flags` supplies the first render call's flags.
|
|
- `:origin` supplies the starting clock reading for completion durations.
|
|
- `:immediate` starts the first call immediately when true. Otherwise, the
|
|
first call waits for an animation frame. Subsequent calls wait for frames.
|
|
|
|
The result contains `:slices`, the traces of render calls; `:end-ms`, the final
|
|
renderer clock reading; and `:viewport-ready-ms` and `:full-ms`, durations
|
|
measured from `:origin`. Partial and ViewportReady continue with the returned
|
|
frame type as the next flags. A guard failure or unexpected frame type
|
|
rejects the Promise with completed traces in the error's `:partial` field."
|
|
[{:keys [check] :as hooks}
|
|
{:keys [flags origin immediate]}]
|
|
(resolve-or-reject
|
|
(check)
|
|
(letfn [(step [state immediate?]
|
|
(-> (next-render-timestamp hooks immediate?)
|
|
(.then ;; recurse or drain
|
|
(fn [timestamp]
|
|
(let [{:keys [frame-type ended-ms] :as rendered}
|
|
(render-slice hooks state timestamp)
|
|
next-state
|
|
(advance-drain-state (:state rendered)
|
|
frame-type
|
|
ended-ms
|
|
origin)]
|
|
|
|
(if (drain-complete? frame-type)
|
|
(drain-result next-state ended-ms origin)
|
|
(step next-state false)))))
|
|
(.catch (fn [cause]
|
|
(throw (partial-error cause (select-keys state [:slices :viewport-ready-ms])))))))]
|
|
(step {:flags flags
|
|
:slices []
|
|
:viewport-ready-ms nil}
|
|
immediate))))
|
|
|
|
(defn restore
|
|
"Restores `view` and renders its tiles until Full.
|
|
The first call uses SyncTiles and starts immediately. Completion durations
|
|
begin after restoring the view and ending the interaction. Returns a Promise
|
|
with recorded render calls, completion durations and the final clock reading
|
|
`:end-ms`."
|
|
[{:keys [now check set-view set-view-start set-view-end] :as hooks} view]
|
|
(resolve-or-reject
|
|
(check)
|
|
(set-view-start)
|
|
(set-view view)
|
|
(set-view-end)
|
|
(drain hooks {:flags wasm/RENDER_FLAG_SYNC_TILES
|
|
:origin (now)
|
|
:immediate true})))
|
|
|
|
(defn start-camera!
|
|
"Starts a camera session from the context's current `:view` and `:hooks`.
|
|
Resolves to the session with its start time and requested settling wait.
|
|
The initial active interval covers the operation that starts the camera.
|
|
`:settle-ms` must give a finite, nonnegative duration in milliseconds."
|
|
[{:keys [hooks view]} {:keys [settle-ms]}]
|
|
(resolve-or-reject
|
|
(when-not (and (number? settle-ms)
|
|
(js/Number.isFinite settle-ms)
|
|
(<= 0 settle-ms))
|
|
(throw (ex-info "settle-ms must be a nonnegative finite number"
|
|
{:type ::invalid-settle-ms :settle-ms settle-ms})))
|
|
(let [{:keys [now check set-view-start]} hooks]
|
|
(check)
|
|
(let [started-ms (now)]
|
|
(set-view-start)
|
|
{:hooks hooks
|
|
:view view
|
|
:settle-ms settle-ms
|
|
:started-ms started-ms
|
|
:last-input-ms started-ms
|
|
:active-end-ms (now)
|
|
:cached-slices []
|
|
:authored-sleeps []}))))
|
|
|
|
(defn preview-view!
|
|
"Updates the view and draws one cached preview on the next animation frame.
|
|
Resolves to the session with the new view and measured preview durations.
|
|
Errors retain completed previews and pauses under `:partial`."
|
|
[session-p view]
|
|
(-> (js/Promise.resolve session-p)
|
|
(.then (fn [{:keys [hooks] :as session}]
|
|
(let [{:keys [now frame check render-from-cache set-view]} hooks
|
|
evidence (select-keys session [:cached-slices :authored-sleeps])]
|
|
(resolve-or-reject-with cause
|
|
(partial-error cause evidence)
|
|
(check)
|
|
(-> (frame)
|
|
(.then (fn [timestamp]
|
|
(check)
|
|
(let [input-ms (now)
|
|
call-start (now)]
|
|
(set-view view)
|
|
(let [cache-start (now)]
|
|
(render-from-cache)
|
|
(let [cache-end (now)
|
|
slices (conj (:cached-slices session)
|
|
{:timestamp timestamp
|
|
:duration-ms (- cache-end cache-start)
|
|
:view-and-preview-ms (- cache-end call-start)})]
|
|
(try
|
|
(check)
|
|
(catch :default cause
|
|
(throw (partial-error cause (assoc evidence :cached-slices slices)))))
|
|
(assoc session :view view :last-input-ms input-ms
|
|
:active-end-ms (now) :cached-slices slices))))))
|
|
(.catch (fn [cause] (throw (partial-error cause evidence)))))))))))
|
|
|
|
(defn- interpolate
|
|
"Interpolates $v(t) = (1-t)a + tb$ between finite endpoints.
|
|
|
|
Args:
|
|
a: starting value.
|
|
b: target value.
|
|
t: interpolation fraction in [0, 1].
|
|
|
|
Returns:
|
|
The interpolated value, preserving positive endpoints near zero."
|
|
[a b t]
|
|
(if (< t 0.5)
|
|
(+ a (* t (- b a)))
|
|
(+ b (* (- 1 t) (- a b)))))
|
|
|
|
(defn animate-view!
|
|
"Moves to `to` through `steps` linearly interpolated views in sequence.
|
|
Each update waits for an animation frame and draws one cached preview.
|
|
Resolves to the session with its final view. Steps must be a positive integer;
|
|
errors retain completed previews and pauses under `:partial`."
|
|
[session-p {:keys [to steps]}]
|
|
(-> (js/Promise.resolve session-p)
|
|
(.then (fn [{from :view :as session}]
|
|
(when-not (and (integer? steps) (pos? steps))
|
|
(throw (ex-info "camera steps must be a positive integer"
|
|
{:type ::invalid-steps :steps steps
|
|
:partial (select-keys session [:cached-slices :authored-sleeps])})))
|
|
(reduce (fn [session-p i]
|
|
(let [t (/ i steps)
|
|
view (if (= i steps)
|
|
to
|
|
{:scale (interpolate (:scale from) (:scale to) t)
|
|
:x (interpolate (:x from) (:x to) t)
|
|
:y (interpolate (:y from) (:y to) t)})]
|
|
(preview-view! session-p
|
|
view)))
|
|
(js/Promise.resolve session)
|
|
(range 1 (inc steps)))))))
|
|
|
|
(defn sleep!
|
|
"Pauses the camera session and records the requested and observed wait.
|
|
The wait must be finite and nonnegative. The pause extends the active
|
|
interval. Resolves to the updated session;
|
|
errors retain completed previews and pauses under `:partial`."
|
|
[session-p requested-ms]
|
|
(-> (js/Promise.resolve session-p)
|
|
(.then (fn [{:keys [hooks] :as session}]
|
|
(let [{:keys [now sleep check]} hooks
|
|
evidence (select-keys session [:cached-slices :authored-sleeps])]
|
|
(resolve-or-reject-with cause
|
|
(partial-error cause evidence)
|
|
(when-not (and (number? requested-ms)
|
|
(js/Number.isFinite requested-ms)
|
|
(<= 0 requested-ms))
|
|
(throw (ex-info "sleep duration must be a nonnegative finite number"
|
|
{:type ::invalid-sleep :requested-ms requested-ms})))
|
|
(check)
|
|
(let [started-ms (now)]
|
|
(-> (sleep requested-ms)
|
|
(.then (fn [_]
|
|
(check)
|
|
(let [ended-ms (now)]
|
|
(-> session
|
|
(assoc :active-end-ms ended-ms)
|
|
(update :authored-sleeps conj
|
|
{:requested-ms requested-ms
|
|
:actual-ms (- ended-ms started-ms)})))))
|
|
(.catch (fn [cause] (throw (partial-error cause evidence))))))))))))
|
|
|
|
(defn- finish-interaction
|
|
"Ends the interaction and resolves to camera durations and recorded render calls.
|
|
Errors retain completed previews and pauses under `:partial`."
|
|
[{:keys [now check set-view-end] :as hooks}
|
|
{:keys [settle-ms started-ms cached-slices last-input-ms active-end-ms authored-sleeps]}]
|
|
(check)
|
|
|
|
(let [finalization-start (now)
|
|
t0 (now)]
|
|
(set-view-end)
|
|
|
|
(let [set-view-end-ms (- (now) t0)]
|
|
(-> (drain hooks
|
|
{:flags wasm/RENDER_FLAG_SYNC_TILES
|
|
:origin finalization-start
|
|
:immediate true})
|
|
(.then
|
|
(fn [{:keys [slices end-ms viewport-ready-ms full-ms]}]
|
|
{:cached-slices cached-slices
|
|
:slices slices
|
|
:end-ms end-ms
|
|
:interact-ms (- end-ms started-ms)
|
|
:active-ms (- active-end-ms started-ms)
|
|
:authored-sleeps authored-sleeps
|
|
:settling-requested-ms settle-ms
|
|
:settling-actual-ms (- finalization-start active-end-ms)
|
|
:set-view-end-ms set-view-end-ms
|
|
:time-to-viewport-ready-ms viewport-ready-ms
|
|
:time-to-full-ms full-ms
|
|
:last-input-to-full-ms (- end-ms last-input-ms)}))
|
|
(.catch (fn [cause]
|
|
(throw (partial-error cause {:cached-slices cached-slices
|
|
:authored-sleeps authored-sleeps}))))))))
|
|
|
|
|
|
(defn finish-camera!
|
|
"Waits for settling, ends the interaction and renders until Full.
|
|
Resolves to cached previews, traces of final render calls, pauses and measured
|
|
camera durations. `:end-ms` is the clock reading at the final renderer return.
|
|
Errors retain completed previews and pauses under `:partial`."
|
|
[session-p]
|
|
(-> (js/Promise.resolve session-p)
|
|
(.then (fn [{:keys [hooks settle-ms] :as session}]
|
|
(let [evidence (select-keys session [:cached-slices :authored-sleeps])]
|
|
(resolve-or-reject-with cause
|
|
(partial-error cause evidence)
|
|
((:check hooks))
|
|
(-> ((:sleep hooks) settle-ms)
|
|
(.then (fn [_] (finish-interaction hooks session)))
|
|
(.catch (fn [cause] (throw (partial-error cause evidence)))))))))))
|