🚧 WIP Step 18 Camera ops

This commit is contained in:
Miguel de Benito Delgado 2026-10-02 12:00:27 +02:00
parent 9cb53adb44
commit 0eeba84af6
2 changed files with 219 additions and 80 deletions

View File

@ -9,18 +9,17 @@
The WASM renderer finishes progressively in tile budgets: one `_render`
call returns a frame type and the caller keeps calling until `FRAME_TYPE_FULL`.
This namespace implements a driver loop as three functions over an injected
`hooks` map. Callers can use the same timing and continuation rules for
This namespace implements a driver loop and camera sessions over an injected
`hooks` map. Callers use the same timing and continuation rules for
rendering and interaction:
- `drain` drives `_render` to `FRAME_TYPE_FULL`, collecting one slice per
call.
- `restore` puts the camera back to the case's initial view, then drains.
Callers run it outside timing.
- `interact` replays a deterministic gesture of small camera moves with
cheap cached previews, waits out the declared settle, then drains to
`FRAME_TYPE_FULL`. The caller's interaction timer includes the camera
start, gesture, settle wait, camera end and final drain.
- `start-camera!`, `preview-view!`, `animate-view!`, `sleep!` and
`finish-camera!` form one Promise-aware camera session. `interact` is a
thin adapter for callers with a predeclared frame vector.
`hooks` supplies clock, scheduling and renderer operations. The browser
supplies real capabilities; tests supply fakes.
@ -62,16 +61,19 @@
- `:cached-slices` one map per gesture frame:
- `:timestamp` rAF stamp of the input
- `:duration-ms`: the timed `_render-from-cache` call.
- `:view-and-preview-ms`: the view update and cached-preview call span.
Cached previews have their own shape: no flags or frame type.
- `:viewport-ready-ms` first Full-or-ViewportReady completion measured
from the drain origin. An immediate Full supplies both boundaries, so
they are equal there.
- `:full-ms` Full completion measured from the drain origin.
- `:interact-ms` from before `_set_view_start` through final Full.
- `:active-ms` from before `_set_view_start` to after the last cached
preview and its guard check.
preview and its guard check, or an authored sleep after that preview.
- `:authored-sleeps` requested and actual durations of optional pauses.
- `:settling-requested-ms` the declared settle wait passed in.
- `:settling-actual-ms` from the end of the last cached preview to
finalization start, recorded separately from the declared value.
- `:settling-actual-ms` from the end of the active span to finalization
start, recorded separately from the declared value.
- `:set-view-end-ms` wall time of the `_set_view_end` call itself.
Finalization starts before `_set_view_end`, so the final drain times
include it; the call time is still reported on its own.
@ -223,57 +225,103 @@
(catch :default cause
(js/Promise.reject cause))))
(defn- replay-frame
[{:keys [now frame check render-from-cache set-view]}
state
view
last-frame?]
(-> (frame)
(.then
(fn [timestamp]
(check)
(let [input-ms (now)]
(set-view view)
(let [t0 (now)
_ (render-from-cache)
duration-ms (- (now) t0)]
(check)
(cond-> (-> state
(assoc :last-input-ms input-ms)
(update :cached-slices conj
{:timestamp timestamp
:duration-ms duration-ms}))
last-frame?
(assoc :active-end-ms (now)))))))))
(defn start-camera!
"Starts a measured camera session after the caller's untimed preparation.
`rtx` supplies `:hooks` and the current full `:view`."
[{:keys [hooks view]} {:keys [settle-ms]}]
(try
(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)
(js/Promise.resolve
{: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 []})))
(catch :default cause
(js/Promise.reject cause))))
(defn preview-view!
"Awaits a session or session promise, then submits one cached preview after
exactly one rAF. The call span includes `_set_view` and cache rendering."
[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]
(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)]
(check)
(assoc session
:view view
:last-input-ms input-ms
:active-end-ms (now)
:cached-slices
(conj (:cached-slices session)
{:timestamp timestamp
:duration-ms (- cache-end cache-start)
:view-and-preview-ms (- cache-end call-start)})))))))))))))
(defn- replay-gesture
[{:keys [now] :as hooks} frames started-ms]
(if (empty? frames)
(js/Promise.resolve
{:cached-slices []
:last-input-ms started-ms
:active-end-ms (now)})
(let [last-index (dec (count frames))]
(reduce-kv
(fn [state-p index view]
(.then state-p
#(replay-frame hooks
%
view
(= index last-index))))
(js/Promise.resolve
{:cached-slices []
:last-input-ms started-ms})
frames))))
(defn animate-view!
"Submits exactly `steps` serial, linearly interpolated full views."
[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})))
(reduce (fn [session-p i]
(let [t (/ i steps)]
(preview-view! session-p
{:scale (+ (:scale from) (* t (- (:scale to) (:scale from))))
:x (+ (:x from) (* t (- (:x to) (:x from))))
:y (+ (:y from) (* t (- (:y to) (:y from))))})))
(js/Promise.resolve session)
(range 1 (inc steps)))))))
(defn sleep!
"Awaits an authored pause inside the active span and records its actual time."
[session-p requested-ms]
(-> (js/Promise.resolve session-p)
(.then (fn [{:keys [hooks] :as session}]
(let [{:keys [now sleep check]} hooks]
(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)}))))))))))))
(defn- finish-interaction
[{:keys [now check set-view-end] :as hooks}
settle-ms
started-ms
{:keys [cached-slices last-input-ms active-end-ms]}]
{:keys [settle-ms started-ms cached-slices last-input-ms active-end-ms authored-sleeps]}]
(check)
(let [finalization-start (now)
@ -289,7 +337,9 @@
(fn [{:keys [slices viewport-ready-ms full-ms]}]
{:cached-slices cached-slices
:slices slices
:interact-ms (- (now) 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
@ -298,16 +348,15 @@
:last-input-to-full-ms (- (now) last-input-ms)}))))))
(defn- settle-and-finish
[{:keys [sleep] :as hooks}
settle-ms
started-ms
gesture-state]
(-> (sleep settle-ms)
(.then #(finish-interaction hooks
settle-ms
started-ms
gesture-state))))
(defn finish-camera!
"Awaits a session, settles, ends the camera and drains through Full."
[session-p]
(-> (js/Promise.resolve session-p)
(.then (fn [{:keys [hooks settle-ms] :as session}]
((:check hooks))
(-> ((:sleep hooks) settle-ms)
(.then (fn [_]
(finish-interaction hooks session))))))))
(defn interact
@ -317,11 +366,10 @@
The flow is:
interact
├─ set-view-start
├─ replay-gesture
│ └─ replay-frame × N
├─ settle
└─ finish-interaction
├─ start-camera!
├─ preview-view! × N
└─ finish-camera!
├─ settle
├─ set-view-end
└─ drain
@ -330,15 +378,10 @@
`settle-ms` elapses, `_set_view_end` is timed on its own, and a SyncTiles
immediate drain runs with its origin at finalization start, so the final
times include `_set_view_end` while reporting it separately."
[{:keys [now check set-view-start] :as hooks}
[hooks
{:keys [frames settle-ms]}]
(try
(let [started-ms (do (check) (now))]
(set-view-start)
(-> (replay-gesture hooks (vec frames) started-ms)
(.then #(settle-and-finish hooks
settle-ms
started-ms
%))))
(catch :default cause
(js/Promise.reject cause))))
(-> (reduce preview-view!
(start-camera! {:hooks hooks :view (first frames)}
{:settle-ms settle-ms})
frames)
(finish-camera!)))

View File

@ -0,0 +1,96 @@
;; 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.scenes.camera
"Standard load, pan and zoom bodies for renderer benchmark cases.
## Concepts
A scene is a recipe for shapes. A case pairs a scene with one measured
action. Its `:run!` body receives `rtx`, a browser runtime map. The browser
builds and uploads the scene snapshot before calling a body.
`rtx` contains:
- `:case`: plain case data, including its `:operation` settings.
- `:scene`: the prepared shape snapshot.
- `:module`: the WASM renderer handle.
- `:view`: the starting `{:scale :x :y}` camera view. The fields are zoom
scale and two camera offsets.
- `:hooks`: clock, scheduling and renderer functions.
'Full' (`FRAME_TYPE_FULL`) is the renderer frame type that ends a progressive
render (it does not mean the frame is visible on screen, though!).
A cached preview updates a view and calls `_render_from_cache` once, without
draining to `Full`.
A camera session spans camera start, previews, settling and the final `Full`
drain. A measurement is a map of render slices and elapsed times.
## Work and timing
`load!` drains the first render to `Full` after upload. `Full` marks renderer
completion. Pan and zoom read settings from `[:case :operation]`. `pan!` adds
`:dx` and `:dy` to the starting view. `zoom!` multiplies its `:scale` by
`:factor`. Both submit a number `:steps` of interpolated views. Each view
waits for the browser's next animation frame and makes one cached preview.
They then request a `:settle-ms` pause, in milliseconds, and drain the final
view to `Full`.
Each body returns a promise of a measurement map. For reused cases,
the browser restores the starting view outside interaction timing. Pan and
zoom calculate their target before starting that timer.
## Related code
Scene declarations such as `scenes/rects.cljs` call these functions from
their `:run!` bodies. `scenes/core.cljs` registers those bodies.
`browser.cljs` supplies `rtx` and prepares reused cases. `protocol.cljs`
owns the render drain and camera session; its `Metrics reported` section
defines the measurement fields. Node reads case declarations without running
their bodies. This namespace loads there because renderer effects come
through the browser-supplied `:hooks`."
(:require
[benches.render-wasm.protocol :as protocol]))
(defn load!
"Drains the uploaded scene until the renderer reports Full completion.
`rtx` supplies the clock and renderer hooks. Returns a promise of first-render
slices and completion times."
[rtx]
(let [hooks (:hooks rtx)]
(protocol/drain hooks {:flags 0
:origin ((:now hooks))
:immediate false})))
(defn pan!
"Moves the starting `{:scale :x :y}` view by the case's `:dx` and `:dy`.
Submits `:steps` views, each with one cached renderer preview after an
animation frame. Requests a `:settle-ms` pause, then drains until the renderer
reports Full completion.
Returns a promise of interaction measurements."
[rtx]
(let [view (:view rtx)
{:keys [steps dx dy settle-ms]} (get-in rtx [:case :operation])
target (-> view
(update :x + dx)
(update :y + dy))]
(-> (protocol/start-camera! rtx {:settle-ms settle-ms})
(protocol/animate-view! {:to target :steps steps})
(protocol/finish-camera!))))
(defn zoom!
"Multiplies the starting `{:scale :x :y}` view's scale by `:factor`.
Submits `:steps` views, each with one cached renderer preview after an
animation frame. Requests a `:settle-ms` pause, then drains until the renderer
reports Full completion.
Returns a promise of interaction measurements."
[rtx]
(let [view (:view rtx)
{:keys [steps factor settle-ms]} (get-in rtx [:case :operation])
target (update view :scale * factor)]
(-> (protocol/start-camera! rtx {:settle-ms settle-ms})
(protocol/animate-view! {:to target :steps steps})
(protocol/finish-camera!))))