mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
🚧 WIP Step 18 Camera ops
This commit is contained in:
parent
9cb53adb44
commit
0eeba84af6
@ -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!)))
|
||||
|
||||
96
frontend/test/benches/render_wasm/scenes/camera.cljs
Normal file
96
frontend/test/benches/render_wasm/scenes/camera.cljs
Normal 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!))))
|
||||
Loading…
x
Reference in New Issue
Block a user