Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
PhosphorAnimation::QtQuickClockManager Class Reference

Enforces "one QtQuickClock per QQuickWindow" within a single composition-root-owned manager instance. More...

#include <phosphor-animation/include/PhosphorAnimation/QtQuickClockManager.h>

Public Member Functions

 QtQuickClockManager ()
 
 ~QtQuickClockManager ()
 
IMotionClockclockFor (QQuickWindow *window)
 Return the clock for window, constructing it on first call.
 
void releaseClockFor (QQuickWindow *window)
 Drop the clock entry for window, if any.
 
int entryCount () const
 Current number of active entries.
 
void clearForTest ()
 Drop every entry.
 
 QtQuickClockManager (const QtQuickClockManager &)=delete
 
QtQuickClockManageroperator= (const QtQuickClockManager &)=delete
 

Static Public Member Functions

static void setDefaultManager (QtQuickClockManager *manager)
 Publish manager as the process-wide default for QML-side consumers (PhosphorAnimatedValueBase::resolveClock / PhosphorMotionAnimation) to look up.
 
static QtQuickClockManagerdefaultManager ()
 Read-only view of the manager pointer published by setDefaultManager.
 

Detailed Description

Enforces "one QtQuickClock per QQuickWindow" within a single composition-root-owned manager instance.

Phase 4 decision N. QtQuickClock's class doc (Phase 3) established the invariant: two clocks bound to the same QQuickWindow each connect independently to beforeRendering, double-counting signal dispatch cost without producing different readings. The manager reifies that invariant in code:

auto* clock = QtQuickClockManager::defaultManager()->clockFor(myWindow);

returns the same IMotionClock* for every call with the same QQuickWindow* for the lifetime of that window. When the window is destroyed, the manager's internal QPointer bookkeeping drops the entry on next access; the clock itself is owned by a unique_ptr in the manager and destroyed when the window goes away or the manager itself is destroyed (composition-root teardown).

Ownership: composition-root DI bridged through a QML service locator

The manager is created and owned by the composition root (daemon, editor, settings — each a separate process today). It is NOT a Meyers singleton — its lifetime is bounded by the composition root, not by the first call.

QML consumers can't be handed the manager via constructor injection (PhosphorAnimatedValueBase-derived types are created by the QML engine), so the composition root publishes its locally-owned manager via setDefaultManager(...) and defaultManager() reads it back — a process-wide service locator narrowly scoped to the QML bridge. Tests construct their own local manager per fixture and skip the static. Same pattern as PhosphorCurve::setDefaultRegistry.

Consumer shape

PhosphorMotionAnimation (Phase 4 sub-commit 4) and the PhosphorAnimatedValueBase family look up the manager via defaultManager() and call clockFor from the enclosing Item.window. Direct C++ consumers of AnimatedValue<T> from QML-adjacent code (a custom QQuickPaintedItem that drives its own animations) call this to obtain a clock without having to manage the one-per-window bookkeeping.

The manager does not expose itself to QML — it's a C++-only plumbing handle. QML authors never need to see it; they write PhosphorMotionAnimation and the animation subclass wires the clock behind their back.

Thread safety

defaultManager() is callable from any thread. clockFor() MUST be called on the thread that owns window (always the GUI thread for a live QQuickWindow); it asserts this contract in debug builds. The internal std::mutex guards the map mutation, but the QObject::connect(window, ...) call the method performs outside the lock dereferences window directly — letting a non-owning thread race a concurrent window destruction there would UAF inside Qt's connection machinery.

The returned IMotionClock* is the QtQuickClock instance; its own now() / requestFrame() thread-safety story (documented on QtQuickClock) applies to subsequent use.

Lifetime

The manager holds unique_ptr<QtQuickClock> values keyed on raw QQuickWindow*. Eager eviction wires a QQuickWindow::destroyed lambda at clockFor time; the lambda drops the entry synchronously so a recycled QQuickWindow* address never re-hits a stale clock.

The composition root owns the manager via unique_ptr (typically a member of the daemon / app object). The published default-handle pointer must be cleared (setDefaultManager(nullptr)) before the manager destructs so a successive composition (in tests, or a daemon reconfigure cycle) does not dangle.

Process-exit edge cases

Even with composition-root ownership, two hazards remain:

  1. Dangling destroyed lambda. Each Entry stores the QMetaObject::Connection for its window's destroyed signal. If a QQuickWindow somehow outlives this manager (e.g., a non-standard teardown where QApplication is destroyed AFTER the manager, or an embedded shell that leaks a window), the destroyed signal would dispatch the DirectConnection lambda into an already-destroyed this, UAF on m_mutex. The destructor disconnects every stored connection under the lock to close this path.
  2. Common main() shape — QApplication app(argc, argv); … ; return app.exec(); — makes QApplication a stack local. Its destructor runs BEFORE main()'s statics unwind, so every QQuickWindow is gone before any composition-root-owned manager destructs. Under that contract the hardening in (1) is a no-op. Embedded hosts that heap-allocate QApp or wire their own exit path are the teardowns that actually exercise the disconnect loop.

A previous revision used a lazy-eviction-only design (no eager destroyed hook) on the theory that stale entries consume negligible memory and the Phase-3 reapAnimationsForClock hook handles the animation side. That design was superseded once address-reuse (Qt recycling a raw QQuickWindow* for a fresh window) was identified as a ghost-entry hazard — the eager eviction closes it.

Constructor & Destructor Documentation

◆ QtQuickClockManager() [1/2]

PhosphorAnimation::QtQuickClockManager::QtQuickClockManager ( )

◆ ~QtQuickClockManager()

PhosphorAnimation::QtQuickClockManager::~QtQuickClockManager ( )

◆ QtQuickClockManager() [2/2]

PhosphorAnimation::QtQuickClockManager::QtQuickClockManager ( const QtQuickClockManager )
delete

Member Function Documentation

◆ clearForTest()

void PhosphorAnimation::QtQuickClockManager::clearForTest ( )

Drop every entry.

Intended for unit-test teardown — production code must not call this (animations mid-flight on the dropped clocks would UAF on their next advance).

◆ clockFor()

IMotionClock * PhosphorAnimation::QtQuickClockManager::clockFor ( QQuickWindow *  window)

Return the clock for window, constructing it on first call.

Returns nullptr if window is nullptr. Subsequent calls with the same window return the same pointer.

Must be called on the thread that owns window (always the GUI thread for a live QQuickWindow); the call installs a destroyed hook on window which dereferences the pointer outside the manager's lock. Asserted in debug builds.

Stale-window handling: the manager tracks entries via QPointer<QQuickWindow>. A call after the window was destroyed detects the null-QPointer and evicts the entry before returning nullptr (the caller should not be routing animations to a destroyed window — this just stops the manager from returning a clock whose requestFrame() would target freed state).

◆ defaultManager()

static QtQuickClockManager * PhosphorAnimation::QtQuickClockManager::defaultManager ( )
static

Read-only view of the manager pointer published by setDefaultManager.

Returns nullptr when no composition root has published yet — QML callsites then return a null clock and fall back to library-default fixed-duration animation.

◆ entryCount()

int PhosphorAnimation::QtQuickClockManager::entryCount ( ) const

Current number of active entries.

Does not evict stale ones (opposed to clockFor which does). Lets tests assert the "one clock per window" contract without triggering eviction as a side effect.

◆ operator=()

QtQuickClockManager & PhosphorAnimation::QtQuickClockManager::operator= ( const QtQuickClockManager )
delete

◆ releaseClockFor()

void PhosphorAnimation::QtQuickClockManager::releaseClockFor ( QQuickWindow *  window)

Drop the clock entry for window, if any.

Called by the destructor-on-window-signal wiring (deferred — see class doc) and by tests that want to exercise construction + teardown. Firing reapAnimationsForClock on every controller that captured the dropped clock is the caller's responsibility — the manager has no visibility into which controllers captured what.

◆ setDefaultManager()

static void PhosphorAnimation::QtQuickClockManager::setDefaultManager ( QtQuickClockManager manager)
static

Publish manager as the process-wide default for QML-side consumers (PhosphorAnimatedValueBase::resolveClock / PhosphorMotionAnimation) to look up.

Called once by each composition root after constructing its own manager; pass nullptr on teardown to drop the handle before the manager destructs.


The documentation for this class was generated from the following file: