Miguel de Benito Delgado e4709287dd ✨ Complete renderer benchmark command catalogue
- 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
2026-11-05 11:18:16 +01:00

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)))))))))))