Code:
/ Dotnetfx_Vista_SP2 / Dotnetfx_Vista_SP2 / 8.0.50727.4016 / DEVDIV / depot / DevDiv / releases / Orcas / QFE / wpf / src / Core / CSharp / System / Windows / Media / Animation / Clock.cs / 1 / Clock.cs
//------------------------------------------------------------------------------ // Microsoft Windows Client Platform // Copyright (c) Microsoft Corporation, 2003 // // File: Clock.cs //----------------------------------------------------------------------------- #if DEBUG #define TRACE #endif // DEBUG using MS.Internal; using MS.Utility; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Media.Composition; using System.Windows.Markup; using System.Windows.Threading; using SR=MS.Internal.PresentationCore.SR; using SRID=MS.Internal.PresentationCore.SRID; namespace System.Windows.Media.Animation { ////// Maintains run-time timing state for timed objects. /// ////// A Clock object maintains the run-time state for a timed object /// according to the description specified in a Timeline object. It also /// provides methods for timing control, such as event scheduling and /// VCR-like functionality. Clock objects are arranged in trees /// that match the structure of the Timeline objects they are created from. /// public class Clock : DispatcherObject { // // Constructors // #region Constructors ////// Creates a Clock object. /// /// /// The Timeline to use as a template. /// ////// The returned Clock doesn't have any children. /// protected internal Clock(Timeline timeline) { #if DEBUG lock (_debugLockObject) { _debugIdentity = ++_nextIdentity; WeakReference weakRef = new WeakReference(this); _objectTable[_debugIdentity] = weakRef; } #endif // DEBUG Debug.Assert(timeline != null); // // Store a frozen copy of the timeline // _timeline = (Timeline)timeline.GetCurrentValueAsFrozen(); // GetCurrentValueAsFrozen will make a clone of the Timeline if it's // not frozen and will return the Timeline if it is frozen. // The clone will never have event handlers, while the // frozen original may. This means we need to copy // the event handlers from the original timeline onto the clock // to be consistent. // // Copy the event handlers from the original timeline into the clock // _eventHandlersStore = timeline.InternalEventHandlersStore; // // FXCop fix. Do not call overridables in constructors // UpdateNeedsTicksWhenActive(); // Set the NeedsTicksWhenActive only if we have someone listening // to an event. SetFlag(ClockFlags.NeedsTicksWhenActive, _eventHandlersStore != null); // // Cache values that won't change as the clock ticks // // Non-root clocks have an unchanging begin time specified by their timelines. // A root clock will update _beginTime as necessary. _beginTime = _timeline.BeginTime; // Cache duration, getting Timeline.Duration and recalculating duration // each Tick was eating perf, resolve the duration if possible. _resolvedDuration = _timeline.Duration; if (_resolvedDuration == Duration.Automatic) { // Forever is the default for an automatic duration. We can't // try to resolve the duration yet because the tree // may not be fully built, in which case ClockGroups won't // have their children yet. _resolvedDuration = Duration.Forever; } else { HasResolvedDuration = true; } _currentDuration = _resolvedDuration; // Cache speed ratio, for roots this value may be updated if the interactive // speed ratio changes, but for non-roots this value will remain constant // throughout the lifetime of the clock. _appliedSpeedRatio = _timeline.SpeedRatio; // // Initialize current state // _currentClockState = ClockState.Stopped; if (_beginTime.HasValue) { // We need a tick to bring our state up to date _nextTickNeededTime = TimeSpan.Zero; } // All other data members initialized to zero by default } #endregion // Constructors // // Public Properties // #region Public Properties internal bool CanGrow { get { return GetFlag(ClockFlags.CanGrow); } } internal bool CanSlip { get { return GetFlag(ClockFlags.CanSlip); } } ////// Returns an ClockController which can be used to perform interactive /// operations on this Clock. If interactive operations are not allowed, /// this property returns null; this is the case for Clocks that /// aren't children of the root Clock. /// public ClockController Controller { get { Debug.Assert(!IsTimeManager); // Unless our parent is the root clock and we're controllable, // return null if (IsRoot && HasControllableRoot) { return new ClockController(this); } else { return null; } } } ////// The current repeat iteration. The first period has a value of one. /// ////// If the clock is not active, the value of this property is only valid if /// the fill attribute specifies that the timing attributes should be /// extended. Otherwise, the property returns -1. /// public Int32? CurrentIteration { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _currentIteration; } } ////// Gets the current rate at which time is progressing in the clock, /// compared to the real-world wall clock. If the clock is stopped, /// this method returns null. /// ///public double? CurrentGlobalSpeed { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _currentGlobalSpeed; } } /// /// The current progress of time for this clock. /// ////// If the clock is active, the progress is always a value between 0 and 1, /// inclusive. Otherwise, the progress depends on the value of the /// public double? CurrentProgress { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _currentProgress; } } ///attribute. /// If the clock is inactive and the fill attribute is not in effect, this /// property returns null. /// /// Gets a value indicating whether the Clock’s current time is inside the Active period /// (meaning properties may change frame to frame), inside the Fill period, or Stopped. /// ////// You can tell whether you’re in FillBegin or FillEnd by the value of CurrentProgress /// (0 for FillBegin, 1 for FillEnd). /// public ClockState CurrentState { get { // VerifyAccess(); return _currentClockState; } } ////// The current position of the clock, relative to the starting time. Setting /// this property to a new value has the effect of seeking the clock to a /// new point in time. Both forward and backward seeks are allowed. Setting this /// property has no effect if the clock is not active. However, seeking while the /// clock is paused works as expected. /// public TimeSpan? CurrentTime { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _currentTime; } } ////// /// public bool HasControllableRoot { get { Debug.Assert(!IsTimeManager); return GetFlag(ClockFlags.HasControllableRoot); } } ////// True if the timeline is currently paused, false otherwise. /// ////// This property returns true either if this timeline has been paused, or if an /// ancestor of this timeline has been paused. /// public bool IsPaused { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return IsInteractivelyPaused; } } ////// Returns the natural duration of this Clock, which is defined /// by the Timeline from which it is created. /// ///public Duration NaturalDuration { get { return _timeline.GetNaturalDuration(this); } } /// /// The Clock that sets the parent time for this Clock. /// public Clock Parent { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); // If our parent is the root clock, force a return value of null if (IsRoot) { return null; } else { return _parent; } } } ////// Gets the Timeline object that holds the description controlling the /// behavior of this clock. /// ////// The Timeline object that holds the description controlling the /// behavior of this clock. /// public Timeline Timeline { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _timeline; } } #endregion // Public Properties // // Public Events // #region Public Events ////// Raised by the timeline when it has completed. /// public event EventHandler Completed { add { AddEventHandler(Timeline.CompletedKey, value); } remove { RemoveEventHandler(Timeline.CompletedKey, value); } } ////// Raised by the Clock whenever its current speed changes. /// This event mirrors the CurrentGlobalSpeedInvalidated event on Timeline /// public event EventHandler CurrentGlobalSpeedInvalidated { add { // VerifyAccess(); AddEventHandler(Timeline.CurrentGlobalSpeedInvalidatedKey, value); } remove { // VerifyAccess(); RemoveEventHandler(Timeline.CurrentGlobalSpeedInvalidatedKey, value); } } ////// Raised by the Clock whenever its current state changes. /// This event mirrors the CurrentStateInvalidated event on Timeline /// public event EventHandler CurrentStateInvalidated { add { // VerifyAccess(); AddEventHandler(Timeline.CurrentStateInvalidatedKey, value); } remove { // VerifyAccess(); RemoveEventHandler(Timeline.CurrentStateInvalidatedKey, value); } } ////// Raised by the Clock whenever its current time changes. /// This event mirrors the CurrentTimeInvalidated event on Timeline /// public event EventHandler CurrentTimeInvalidated { add { // VerifyAccess(); AddEventHandler(Timeline.CurrentTimeInvalidatedKey, value); } remove { // VerifyAccess(); RemoveEventHandler(Timeline.CurrentTimeInvalidatedKey, value); } } ////// Raised by the timeline when its removal has been requested by the user. /// public event EventHandler RemoveRequested { add { AddEventHandler(Timeline.RemoveRequestedKey, value); } remove { RemoveEventHandler(Timeline.RemoveRequestedKey, value); } } #endregion // Public Events // // Protected Methods // #region Protected Methods ////// Notify a clock that we've moved in a discontinuous way /// protected virtual void DiscontinuousTimeMovement() { // Base class does nothing } ////// Returns true if the Clock has its own external source for time, which may /// require synchronization with the timing system. Media is one example of this. /// protected virtual bool GetCanSlip() { return false; } ////// /// protected virtual TimeSpan GetCurrentTimeCore() { Debug.Assert(!IsTimeManager); return _currentTime.HasValue ? _currentTime.Value : TimeSpan.Zero; } ////// Notify a clock that we've changed the speed /// protected virtual void SpeedChanged() { // Base class does nothing } ////// Notify a clock that we've stopped /// protected virtual void Stopped() { // Base class does nothing } #endregion // Protected Methods // // Protected Properties // #region Protected Properties ////// The current global time in TimeSpan units, established by the time manager. /// protected TimeSpan CurrentGlobalTime { get { if (_timeManager == null) { return TimeSpan.Zero; } else if (IsTimeManager) { return _timeManager.InternalCurrentGlobalTime; } else { Clock current = this; while (!current.IsRoot) // Traverse up the tree to the root node { current = current._parent; } if (current.HasDesiredFrameRate) { return current._rootData.CurrentAdjustedGlobalTime; } else { return _timeManager.InternalCurrentGlobalTime; } } } } #endregion // Protected Properties // // Internal Methods // #region Internal Methods internal virtual void AddNullPointToCurrentIntervals() { } internal static Clock AllocateClock( Timeline timeline, bool hasControllableRoot) { Clock clock = timeline.AllocateClock(); // Assert that we weren't given an existing clock Debug.Assert(!clock.IsTimeManager); ClockGroup clockGroup = clock as ClockGroup; if ( clock._parent != null || ( clockGroup != null && clockGroup.InternalChildren != null )) { // The derived class is trying to fool us -- we require a new, // fresh, unassociated clock here throw new InvalidOperationException( SR.Get( SRID.Timing_CreateClockMustReturnNewClock, timeline.GetType().Name)); } clock.SetFlag(ClockFlags.HasControllableRoot, hasControllableRoot); return clock; } internal virtual void BuildClockSubTreeFromTimeline( Timeline timeline, bool hasControllableRoot) { SetFlag(ClockFlags.CanSlip, GetCanSlip()); // Set the CanSlip flag // Here we preview the clock's own slip-ability, hence ClockGroups should return false // at this stage, because their children are not yet added by the time of this call. if (CanSlip && (IsRoot || _timeline.BeginTime.HasValue)) { ResolveDuration(); // A [....] clock with duration of zero or no begin time has no effect, so do skip it if (!_resolvedDuration.HasTimeSpan || _resolvedDuration.TimeSpan > TimeSpan.Zero) { // Verify that we only use SlipBehavior in supported scenarios if ((_timeline.AutoReverse == true) || (_timeline.AccelerationRatio > 0) || (_timeline.DecelerationRatio > 0)) { throw new NotSupportedException(SR.Get(SRID.Timing_CanSlipOnlyOnSimpleTimelines)); } _syncData = new SyncData(this); // CanSlip clocks keep themselves synced HasDescendantsWithUnresolvedDuration = !HasResolvedDuration; // Keep track of when our duration is resolved Clock current = _parent; // Traverse up the parent chain and verify that no unsupported behavior is specified while (current != null) { Debug.Assert(!current.IsTimeManager); // We should not yet be connected to the TimeManager if (current._timeline.AutoReverse || current._timeline.AccelerationRatio > 0 || current._timeline.DecelerationRatio > 0) { throw new System.InvalidOperationException(SR.Get(SRID.Timing_SlipBehavior_SyncOnlyWithSimpleParents)); } current.SetFlag(ClockFlags.CanGrow, true); // Propagate the slippage tracking up the tree if (!HasResolvedDuration) // Let the parents know that we have not yet unresolved duration { current.HasDescendantsWithUnresolvedDuration = true; } current._currentIterationBeginTime = current._beginTime; current = current._parent; } } } } internal static Clock BuildClockTreeFromTimeline( Timeline rootTimeline, bool hasControllableRoot) { Clock rootClock = AllocateClock(rootTimeline, hasControllableRoot); // Set this flag so that the subsequent method can rely on it. rootClock.IsRoot = true; rootClock._rootData = new RootData(); // Create a RootData to hold root specific information. // The root clock was given a reference to a frozen copy of the // timing tree. We pass this copy down BuildClockSubTreeFromTimeline // so that each child clock will use that tree rather // than create a new one. rootClock.BuildClockSubTreeFromTimeline(rootClock.Timeline, hasControllableRoot); rootClock.AddToTimeManager(); return rootClock; } internal virtual void ClearCurrentIntervalsToNull() { } // Perform Stage 1 of clipping next tick time: clip by parent internal void ClipNextTickByParent() { // Clip by parent's NTNT if needed. We don't want to clip // if the parent is the TimeManager's root clock. if (!IsTimeManager && !_parent.IsTimeManager && (!InternalNextTickNeededTime.HasValue || (_parent.InternalNextTickNeededTime.HasValue && _parent.InternalNextTickNeededTime.Value < InternalNextTickNeededTime.Value))) { InternalNextTickNeededTime = _parent.InternalNextTickNeededTime; } } internal virtual void ComputeCurrentIntervals(TimeIntervalCollection parentIntervalCollection, TimeSpan beginTime, TimeSpan? endTime, Duration fillDuration, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { } internal virtual void ComputeCurrentFillInterval(TimeIntervalCollection parentIntervalCollection, TimeSpan beginTime, TimeSpan endTime, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { } internal void ComputeLocalState() { Debug.Assert(!IsTimeManager); // Cache previous state values ClockState lastClockState = _currentClockState; TimeSpan? lastCurrentTime = _currentTime; double? lastCurrentGlobalSpeed = _currentGlobalSpeed; double? lastCurrentProgress = _currentProgress; Int32? lastCurrentIteration = _currentIteration; // Reset the PauseStateChangedDuringTick for this tick PauseStateChangedDuringTick = false; ComputeLocalStateHelper(true, false); // Perform the local state calculations with early bail out if (lastClockState != _currentClockState) { // It can happen that we change state without detecting it when // a parent is auto-reversing and we are ticking exactly at the // reverse point, so raise the events. RaiseCurrentStateInvalidated(); RaiseCurrentGlobalSpeedInvalidated(); RaiseCurrentTimeInvalidated(); } // if (_currentGlobalSpeed != lastCurrentGlobalSpeed) { RaiseCurrentGlobalSpeedInvalidated(); } if (HasDiscontinuousTimeMovementOccured) { DiscontinuousTimeMovement(); HasDiscontinuousTimeMovementOccured = false; } } ////// Return the current duration from a specific clock /// ////// A Duration quantity representing the current iteration's estimated duration. /// internal virtual Duration CurrentDuration { get { return Duration.Automatic; } } ////// Internal helper. Schedules an interactive begin at the next tick. /// An interactive begin is literally a seek to 0. It is completely distinct /// from the BeginTime specified on a timeline, which is managed by /// _pendingBeginOffset /// internal void InternalBegin() { InternalSeek(TimeSpan.Zero); } ////// Internal helper for moving to new API. Gets the speed multiplier for the Clock's speed. /// internal double InternalGetSpeedRatio() { return _rootData.InteractiveSpeedRatio; } ////// Internal helper for moving to new API. Pauses the timeline for this timeline and its children. /// internal void InternalPause() { // VerifyAccess(); Debug.Assert(!IsTimeManager); // INVARIANT: we enforce 4 possible valid states: // 1) !Paused (running) // 2) !Paused, Pause pending // 3) Paused // 4) Paused, Resume pending Debug.Assert(!(IsInteractivelyPaused && PendingInteractivePause)); Debug.Assert(!(!IsInteractivelyPaused && PendingInteractiveResume)); if (PendingInteractiveResume) // Cancel existing resume request if made { PendingInteractiveResume = false; } else if (!IsInteractivelyPaused) // If we don't have a pending resume AND we aren't paused already, schedule a pause // This is an ELSE clause because if we had a Resume pending, we MUST already be paused { PendingInteractivePause = true; } NotifyNewEarliestFutureActivity(); } ////// Schedules a Remove operation to happen at the next tick. /// ////// This method schedules the Clock and its subtree to be stopped and the RemoveRequested /// event to be fired on the subtree at the next tick. /// internal void InternalRemove() { PendingInteractiveRemove = true; InternalStop(); } ////// Internal helper for moving to new API. Allows a timeline's timeline to progress again after a call to Pause. /// internal void InternalResume() { // VerifyAccess(); Debug.Assert(!IsTimeManager); // INVARIANT: we enforce 4 possible valid states: // 1) !Paused (running) // 2) !Paused, Pause pending // 3) Paused // 4) Paused, sd Resume pending Debug.Assert( !(IsInteractivelyPaused && PendingInteractivePause)); Debug.Assert( !(!IsInteractivelyPaused && PendingInteractiveResume)); if (PendingInteractivePause) // Cancel existing pause request if made { PendingInteractivePause = false; } else if (IsInteractivelyPaused) // If we don't have a pending pause AND we are currently paused, schedule a resume // This is an ELSE clause because if we had a Pause pending, we MUST already be unpaused { PendingInteractiveResume = true; } NotifyNewEarliestFutureActivity(); } ////// Internal helper for moving to new API. Seeks a timeline's timeline to a new position. /// /// /// The destination to seek to, relative to the clock's BeginTime. If this is past the /// active period execute the FillBehavior. /// internal void InternalSeek(TimeSpan destination) { // VerifyAccess(); Debug.Assert(IsRoot); IsInteractivelyStopped = false; PendingInteractiveStop = false; // Cancel preceding stop; ResetNodesWithSlip(); // Reset [....] tracking _rootData.PendingSeekDestination = destination; RootBeginPending = false; // cancel a previous begin call NotifyNewEarliestFutureActivity(); } ////// The only things that can change for this are the begin time of this /// timeline /// /// /// The destination to seek to, relative to the clock's BeginTime. If this is past the /// active perioed execute the FillBehavior. /// internal void InternalSeekAlignedToLastTick(TimeSpan destination) { Debug.Assert(IsRoot); // This is a no-op with a null TimeManager or when all durations have not yet been resolved if (_timeManager == null || HasDescendantsWithUnresolvedDuration) { return; } // Adjust _beginTime such that our current time equals the Seek position // that was requested _beginTime = CurrentGlobalTime - DivideTimeSpan(destination, _appliedSpeedRatio); if (CanGrow) { _currentIteration = null; // This node is not visited by ResetSlipOnSubtree _currentIterationBeginTime = _beginTime; ResetSlipOnSubtree(); UpdateSyncBeginTime(); } IsInteractivelyStopped = false; // We have unset disabled status PendingInteractiveStop = false; RootBeginPending = false; // Cancel a pending begin ResetNodesWithSlip(); // Reset [....] tracking _timeManager.InternalCurrentIntervals = TimeIntervalCollection.Empty; PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { // We are processing a Seek immediately. We don't need a TIC yet // since we are not computing events, and we don't want to // process pending stuff either. subtree.Current.ComputeLocalStateHelper(false, true); // Compute the state of the node if (HasDiscontinuousTimeMovementOccured) { DiscontinuousTimeMovement(); HasDiscontinuousTimeMovementOccured = false; } subtree.Current.ClipNextTickByParent(); // Perform NextTick clipping, stage 1 // Make a note to visit for stage 2, only for ClockGroups subtree.Current.NeedsPostfixTraversal = (subtree.Current is ClockGroup); } _parent.ComputeTreeStateRoot(); // Re-clip the next tick estimates by children // Fire the events indicating that we've invalidated this whole subtree subtree.Reset(); while (subtree.MoveNext()) { // CurrentTimeInvalidated should be fired first to give AnimationStorage a chance // to update the value. We directly set the flags, then fire RaiseAccumulatedEvents // to avoid involving the TimeManager for this local subtree operation. subtree.Current.CurrentTimeInvalidatedEventRaised = true; subtree.Current.CurrentStateInvalidatedEventRaised = true; subtree.Current.CurrentGlobalSpeedInvalidatedEventRaised = true; subtree.Current.RaiseAccumulatedEvents(); } } ////// Internal helper for moving to new API. Sets a speed multiplier for the Clock's speed. /// /// /// The ratio by which to multiply the Clock's speed. /// internal void InternalSetSpeedRatio(double ratio) { Debug.Assert(IsRoot); _rootData.PendingSpeedRatio = ratio; } ////// Internal helper. Schedules an end for some specified time in the future. /// internal void InternalSkipToFill() { Debug.Assert(IsRoot); TimeSpan? effectiveDuration; effectiveDuration = ComputeEffectiveDuration(); // Throw an exception if the active period extends forever. if (effectiveDuration == null) { // Can't seek to the end if the simple duration is not resolved throw new InvalidOperationException(SR.Get(SRID.Timing_SkipToFillDestinationIndefinite)); } // Offset to the end; override preceding seek requests IsInteractivelyStopped = false; PendingInteractiveStop = false; ResetNodesWithSlip(); // Reset [....] tracking RootBeginPending = false; _rootData.PendingSeekDestination = effectiveDuration.Value; // Seek to the end time NotifyNewEarliestFutureActivity(); } ////// Internal helper for moving to new API. Removes a timeline from its active or fill period. /// Timeline can be restarted with an interactive Begin call. /// internal void InternalStop() { Debug.Assert(IsRoot); PendingInteractiveStop = true; // Cancel all non-persistent interactive requests _rootData.PendingSeekDestination = null; RootBeginPending = false; ResetNodesWithSlip(); // Reset [....] tracking NotifyNewEarliestFutureActivity(); } ////// Raises the events that occured since the last tick and reset their state. /// internal void RaiseAccumulatedEvents() { try // We are calling user-defined delegates, if they throw we must ensure that we leave the Clock in a valid state { // CurrentTimeInvalidated should fire first. This is because AnimationStorage hooks itself // up to this event in order to invalidate whichever DependencyProperty this clock may be // animating. User code in any of these callbacks may query the value of that DP - if they // do so before AnimationStorage has a chance to invalidate they will get the wrong value. if (CurrentTimeInvalidatedEventRaised) { FireCurrentTimeInvalidatedEvent(); } if (CurrentGlobalSpeedInvalidatedEventRaised) { FireCurrentGlobalSpeedInvalidatedEvent(); // Tell the Clocks that they have changed Speed SpeedChanged(); } if (CurrentStateInvalidatedEventRaised) { FireCurrentStateInvalidatedEvent(); // Since the state has been invalidated this means that // we've got a discontinuous time movemement. Tell the clock if (!CurrentGlobalSpeedInvalidatedEventRaised) { DiscontinuousTimeMovement(); } } if (CompletedEventRaised) { FireCompletedEvent(); } if (RemoveRequestedEventRaised) { FireRemoveRequestedEvent(); } } finally // Reset the flags to make the state consistent, even if the user has thrown { CurrentTimeInvalidatedEventRaised = false; CurrentGlobalSpeedInvalidatedEventRaised = false; CurrentStateInvalidatedEventRaised = false; CompletedEventRaised = false; RemoveRequestedEventRaised = false; IsInEventQueue = false; } } ////// Raises the Completed event. /// ////// We only need to raise this event once per tick. If we've already /// raised it in this tick, do nothing. /// internal void RaiseCompleted() { Debug.Assert(!IsTimeManager); CompletedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } ////// Raises the CurrentGlobalSpeedInvalidated event. /// ////// We only need to raise this event once per tick. If we've already /// raised it in this tick, do nothing. /// internal void RaiseCurrentGlobalSpeedInvalidated() { // ROOT Debug.Assert(!IsTimeManager); CurrentGlobalSpeedInvalidatedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } ////// Raises the CurrentStateInvalidated event. /// internal void RaiseCurrentStateInvalidated() { Debug.Assert(!IsTimeManager); if (_currentClockState == ClockState.Stopped) // If our state changed to stopped { Stopped(); } CurrentStateInvalidatedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } ////// Raises the CurrentTimeInvalidated event. This enqueues the event for later dispatch /// if we are in a tick operation. /// internal void RaiseCurrentTimeInvalidated() { Debug.Assert(!IsTimeManager); CurrentTimeInvalidatedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } ////// Raises the RemoveRequested event. /// ////// We only need to raise this event once per tick. If we've already /// raised it in this tick, do nothing. /// internal void RaiseRemoveRequested() { Debug.Assert(!IsTimeManager); RemoveRequestedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } // Reset all currently cached state internal void ResetCachedStateToStopped() { _currentGlobalSpeed = null; _currentIteration = null; IsBackwardsProgressingGlobal = false; _currentProgress = null; _currentTime = null; _currentClockState = ClockState.Stopped; } // Reset IsInSyncPeriod for this node and all children if any (ClockGroup handles this). // We do this whenever a discontinuous interactive action (seek/begin/stop) is performed. internal virtual void ResetNodesWithSlip() { if (_syncData != null) { _syncData.IsInSyncPeriod = false; // Reset [....] tracking } } ////// Check if our descendants have resolved their duration, and resets the HasDescendantsWithUnresolvedDuration /// flag from true to false once that happens. /// ///Returns true when this node or one of its descendants have unresolved duration. internal virtual void UpdateDescendantsWithUnresolvedDuration() { if (HasResolvedDuration) { HasDescendantsWithUnresolvedDuration = false; } } #endregion // Internal Methods // // Internal Properties // #region Internal Properties ////// Specifies the depth of this timeline in the timing tree. If the timeline does not have /// a parent, its depth value is zero. /// internal int Depth { get { // ROOT Debug.Assert(!IsTimeManager); return _depth; } } ////// Returns the last time this timeline will become non-active if it is not /// clipped by the parent container first. Null represents infinity. /// internal Duration EndOfActivePeriod { get { Debug.Assert(!IsTimeManager); if (!HasResolvedDuration) { return Duration.Automatic; } // Computed expiration time with respect to repeat behavior and natural duration; TimeSpan? expirationTime; ComputeExpirationTime(out expirationTime); // if (expirationTime.HasValue) { return expirationTime.Value; } else { return Duration.Forever; } } } ////// Gets the first child of this timeline. /// ////// Since a Clock doesn't have children we will always return null /// internal virtual Clock FirstChild { get { return null; } } ////// Internal unverified access to the CurrentState /// ////// Only ClockGroup should set the value /// internal ClockState InternalCurrentClockState { get { return _currentClockState; } set { _currentClockState = value; } } ////// Internal unverified access to the CurrentGlobalSpeed /// ////// Only ClockGroup should set the value /// internal double? InternalCurrentGlobalSpeed { get { return _currentGlobalSpeed; } set { _currentGlobalSpeed = value; } } ////// Internal unverified access to the CurrentIteration /// ////// Only ClockGroup should set the value /// internal Int32? InternalCurrentIteration { get { return _currentIteration; } set { _currentIteration = value; } } ////// Internal unverified access to the CurrentProgress /// ////// Only ClockGroup should set the value /// internal double? InternalCurrentProgress { get { return _currentProgress; } set { _currentProgress = value; } } ////// The next GlobalTime that this clock may need a tick /// internal TimeSpan? InternalNextTickNeededTime { get { return _nextTickNeededTime; } set { _nextTickNeededTime = value; } } ////// Unchecked internal access to the parent of this Clock. /// internal ClockGroup InternalParent { get { Debug.Assert(!IsTimeManager); return _parent; } } ////// Gets the current Duration for internal callers. This property will /// never return Duration.Automatic. If the _resolvedDuration of the Clock /// has not yet been resolved, this property will return Duration.Forever. /// Therefore, it's possible that the value of this property will change /// one time during the Clock's lifetime. It's not predictable when that /// change will occur, it's up to the custom Clock author. /// internal Duration ResolvedDuration { get { ResolveDuration(); Debug.Assert(_resolvedDuration != Duration.Automatic, "_resolvedDuration should never be set to Automatic."); return _resolvedDuration; } } ////// Gets the right sibling of this timeline. /// ////// The right sibling of this timeline if it's not the last in its parent's /// collection; otherwise, null. /// internal Clock NextSibling { get { Debug.Assert(!IsTimeManager); Debug.Assert(_parent != null && !_parent.IsTimeManager); ListparentChildren = _parent.InternalChildren; if (_childIndex == parentChildren.Count - 1) { return null; } else { return parentChildren[_childIndex + 1]; } } } /// /// Gets a cached weak reference to this clock. /// internal WeakReference WeakReference { get { WeakReference reference = _weakReference; if (reference == null) { reference = new WeakReference(this); _weakReference = reference; } return reference; } } ////// Get the desired framerate of this clock /// internal int? DesiredFrameRate { get { int? returnValue = null; if (HasDesiredFrameRate) { returnValue = _rootData.DesiredFrameRate; } return returnValue; } } // // Internal access to some of the flags // #region Internal Flag Accessors internal bool CompletedEventRaised { get { return GetFlag(ClockFlags.CompletedEventRaised); } set { SetFlag(ClockFlags.CompletedEventRaised, value); } } internal bool CurrentGlobalSpeedInvalidatedEventRaised { get { return GetFlag(ClockFlags.CurrentGlobalSpeedInvalidatedEventRaised); } set { SetFlag(ClockFlags.CurrentGlobalSpeedInvalidatedEventRaised, value); } } internal bool CurrentStateInvalidatedEventRaised { get { return GetFlag(ClockFlags.CurrentStateInvalidatedEventRaised); } set { SetFlag(ClockFlags.CurrentStateInvalidatedEventRaised, value); } } internal bool CurrentTimeInvalidatedEventRaised { get { return GetFlag(ClockFlags.CurrentTimeInvalidatedEventRaised); } set { SetFlag(ClockFlags.CurrentTimeInvalidatedEventRaised, value); } } private bool HasDesiredFrameRate { get { return GetFlag(ClockFlags.HasDesiredFrameRate); } set { SetFlag(ClockFlags.HasDesiredFrameRate, value); } } internal bool HasResolvedDuration { get { return GetFlag(ClockFlags.HasResolvedDuration); } set { SetFlag(ClockFlags.HasResolvedDuration, value); } } internal bool IsBackwardsProgressingGlobal { get { return GetFlag(ClockFlags.IsBackwardsProgressingGlobal); } set { SetFlag(ClockFlags.IsBackwardsProgressingGlobal, value); } } internal bool IsInEventQueue { get { return GetFlag(ClockFlags.IsInEventQueue); } set { SetFlag(ClockFlags.IsInEventQueue, value); } } ////// Unchecked internal access to the paused state of the clock. /// ///internal bool IsInteractivelyPaused { get { return GetFlag(ClockFlags.IsInteractivelyPaused); } set { SetFlag(ClockFlags.IsInteractivelyPaused, value); } } internal bool IsInteractivelyStopped { get { return GetFlag(ClockFlags.IsInteractivelyStopped); } set { SetFlag(ClockFlags.IsInteractivelyStopped, value); } } internal bool IsRoot { get { return GetFlag(ClockFlags.IsRoot); } set { SetFlag(ClockFlags.IsRoot, value); } } internal bool IsTimeManager { get { return GetFlag(ClockFlags.IsTimeManager); } set { SetFlag(ClockFlags.IsTimeManager, value); } } /// /// Returns true if the Clock traversed during the first tick pass. /// ///internal bool NeedsPostfixTraversal { get { return GetFlag(ClockFlags.NeedsPostfixTraversal); } set { SetFlag(ClockFlags.NeedsPostfixTraversal, value); } } internal virtual bool NeedsTicksWhenActive { get { return GetFlag(ClockFlags.NeedsTicksWhenActive); } set { SetFlag(ClockFlags.NeedsTicksWhenActive, value); } } internal bool PauseStateChangedDuringTick { get { return GetFlag(ClockFlags.PauseStateChangedDuringTick); } set { SetFlag(ClockFlags.PauseStateChangedDuringTick, value); } } internal bool PendingInteractivePause { get { return GetFlag(ClockFlags.PendingInteractivePause); } set { SetFlag(ClockFlags.PendingInteractivePause, value); } } internal bool PendingInteractiveRemove { get { return GetFlag(ClockFlags.PendingInteractiveRemove); } set { SetFlag(ClockFlags.PendingInteractiveRemove, value); } } internal bool PendingInteractiveResume { get { return GetFlag(ClockFlags.PendingInteractiveResume); } set { SetFlag(ClockFlags.PendingInteractiveResume, value); } } internal bool PendingInteractiveStop { get { return GetFlag(ClockFlags.PendingInteractiveStop); } set { SetFlag(ClockFlags.PendingInteractiveStop, value); } } internal bool RemoveRequestedEventRaised { get { return GetFlag(ClockFlags.RemoveRequestedEventRaised); } set { SetFlag(ClockFlags.RemoveRequestedEventRaised, value); } } private bool HasDiscontinuousTimeMovementOccured { get { return GetFlag(ClockFlags.HasDiscontinuousTimeMovementOccured); } set { SetFlag(ClockFlags.HasDiscontinuousTimeMovementOccured, value); } } internal bool HasDescendantsWithUnresolvedDuration { get { return GetFlag(ClockFlags.HasDescendantsWithUnresolvedDuration); } set { SetFlag(ClockFlags.HasDescendantsWithUnresolvedDuration, value); } } private bool HasSeekOccuredAfterLastTick { get { return GetFlag(ClockFlags.HasSeekOccuredAfterLastTick); } set { SetFlag(ClockFlags.HasSeekOccuredAfterLastTick, value); } } #endregion // Internal Flag Accessors #endregion // Internal Properties // // Private Methods // #region Private Methods // // Local State Computation Helpers // #region Local State Computation Helpers // // Seek, Begin and Pause are internally implemented by adjusting the begin time. // For example, when paused, each tick moves the begin time forward so that // overall the clock hasn't moved. // // Note that _beginTime is an offset from the parent's begin. Since // these are root clocks, the parent is the TimeManager, and we // must add in the CurrentGlobalTime private void AdjustBeginTime() { Debug.Assert(IsRoot); // root clocks only; non-roots have constant begin time Debug.Assert(_rootData != null); // Process the effects of Seek, Begin, and Pause; delay request if not all durations are resolved in this subtree. if (_rootData.PendingSeekDestination.HasValue && !HasDescendantsWithUnresolvedDuration) { Debug.Assert(!RootBeginPending); // we can have either a begin or a seek, not both // Adjust the begin time such that our current time equals PendingSeekDestination _beginTime = CurrentGlobalTime - DivideTimeSpan(_rootData.PendingSeekDestination.Value, _appliedSpeedRatio); if (CanGrow) // One of our descendants has set this flag on us { _currentIterationBeginTime = _beginTime; // We relied on a combination of _currentIterationBeginTime and _currentIteration for our state _currentIteration = null; // Therefore, we should reset both to reset our position ResetSlipOnSubtree(); } UpdateSyncBeginTime(); _rootData.PendingSeekDestination = null; // We have seeked, so raise all events signifying that no assumptions can be made about our state PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.RaiseCurrentStateInvalidated(); subtree.Current.RaiseCurrentTimeInvalidated(); subtree.Current.RaiseCurrentGlobalSpeedInvalidated(); } } else if (RootBeginPending) { // RootBeginPending is set when a root is parented to a tree (in AddToRoot()). // It allows us to interpret Timeline.BeginTime as an offset from the current // time and thus schedule a begin in the future. _beginTime = CurrentGlobalTime + _timeline.BeginTime; if (CanGrow) // One of our descendants has set this flag on us { _currentIterationBeginTime = _beginTime; // We should be just starting our first iteration now } UpdateSyncBeginTime(); RootBeginPending = false; } else if ((IsInteractivelyPaused || _rootData.InteractiveSpeedRatio == 0) && (_syncData == null || !_syncData.IsInSyncPeriod)) // We were paused at the last tick, so move _beginTime by the delta from last tick to this one // Only perform this iff we are *continuously* moving, e.g. if we haven't seeked between ticks. // [....] NOTE: If we are syncing, then the [....] code should be the one to make this adjustment // by using the Media's current time (which should already be paused). { if (_beginTime.HasValue) { // Adjust for the speed of this timelineClock _beginTime += _timeManager.LastTickDelta; UpdateSyncBeginTime(); if (_currentIterationBeginTime.HasValue) // One of our descendants has set this flag on us { _currentIterationBeginTime += _timeManager.LastTickDelta; } } } // Adjust for changes to the speed ratio if (_rootData.PendingSpeedRatio.HasValue) { double pendingSpeedRatio = _rootData.PendingSpeedRatio.Value * _timeline.SpeedRatio; // If the calculated speed ratio is 0, we reset it to 1. I believe this is // because we don't want to support pausing by setting speed ratio. Instead // they should call pause. if (pendingSpeedRatio == 0) { pendingSpeedRatio = 1; } Debug.Assert(_beginTime.HasValue); // Below code uses the above assumption that beginTime has a value TimeSpan previewParentTime = CurrentGlobalTime; if (_currentIterationBeginTime.HasValue) { // Adjusting SpeedRatio is not a discontiuous event, we don't want to reset slip after doing this _currentIterationBeginTime = previewParentTime - MultiplyTimeSpan(previewParentTime - _currentIterationBeginTime.Value, _appliedSpeedRatio / pendingSpeedRatio); } else { _beginTime = previewParentTime - MultiplyTimeSpan(previewParentTime - _beginTime.Value, _appliedSpeedRatio / pendingSpeedRatio); } RaiseCurrentGlobalSpeedInvalidated(); // _appliedSpeedRatio represents the speed ratio we're actually using // for this Clock. _appliedSpeedRatio = pendingSpeedRatio; // _rootData.InteractiveSpeedRatio represents the actual user set interactive // speed ratio value even though we may override it if it's 0. _rootData.InteractiveSpeedRatio = _rootData.PendingSpeedRatio.Value; // Clear out the new pending speed ratio since we've finished applying it. _rootData.PendingSpeedRatio = null; UpdateSyncBeginTime(); } return; } // Apply the effects of having DFR set on a root clock internal void ApplyDesiredFrameRateToGlobalTime() { if (HasDesiredFrameRate) { _rootData.LastAdjustedGlobalTime = _rootData.CurrentAdjustedGlobalTime; _rootData.CurrentAdjustedGlobalTime = GetCurrentDesiredFrameTime(_timeManager.InternalCurrentGlobalTime); } } // Apply the effects of having DFR set on a root clock internal void ApplyDesiredFrameRateToNextTick() { Debug.Assert(IsRoot); if (HasDesiredFrameRate && InternalNextTickNeededTime.HasValue) { // If we have a desired frame rate, it should greater than // zero (the default for the field.) Debug.Assert(_rootData.DesiredFrameRate > 0); // We "round" our next tick needed time up to the next frame TimeSpan nextDesiredTick = InternalNextTickNeededTime == TimeSpan.Zero ? _rootData.CurrentAdjustedGlobalTime : InternalNextTickNeededTime.Value; InternalNextTickNeededTime = GetNextDesiredFrameTime(nextDesiredTick); } } // Determines the current iteration and uncorrected linear time accounting for _timeline.AutoReverse // At the end of this function call, the following state attributes are initialized: // CurrentIteration private bool ComputeCurrentIteration(TimeSpan parentTime, double parentSpeed, TimeSpan? expirationTime, out TimeSpan localProgress) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_currentClockState != ClockState.Stopped); Debug.Assert(_currentDuration != Duration.Automatic, "_currentDuration should never be Automatic."); Debug.Assert(_beginTime.HasValue); Debug.Assert(parentTime >= _beginTime.Value); // We are active or in postfill RepeatBehavior repeatBehavior = _timeline.RepeatBehavior; // Apply speed and offset, convert down to TimeSpan TimeSpan beginTimeForOffsetComputation = _currentIterationBeginTime.HasValue ? _currentIterationBeginTime.Value : _beginTime.Value; TimeSpan offsetFromBegin = MultiplyTimeSpan(parentTime - beginTimeForOffsetComputation, _appliedSpeedRatio); // This may be set redundantly in one case, but simplifies code IsBackwardsProgressingGlobal = _parent.IsBackwardsProgressingGlobal; if (_currentDuration.HasTimeSpan) // For finite duration, use modulo arithmetic to compute current iteration { if (_currentDuration.TimeSpan == TimeSpan.Zero) // We must be post-filling if we have gotten here { Debug.Assert(_currentClockState != ClockState.Active); // Assign localProgress to avoid compiler error localProgress = TimeSpan.Zero; // CurrentTime will always be zero. _currentTime = TimeSpan.Zero; Double currentProgress; if (repeatBehavior.HasCount) { Double repeatCount = repeatBehavior.Count; if (repeatCount <= 1.0) { currentProgress = repeatCount; _currentIteration = 1; } else { Double wholePart = (Double)((Int32)repeatCount); if (repeatCount == wholePart) { currentProgress = 1.0; _currentIteration = (Int32)repeatCount; } else { currentProgress = repeatCount - wholePart; _currentIteration = (Int32)(repeatCount + 1.0d); } } } else { // RepeatBehavior.HasTimeSpan cases: // I guess we could repeat a 0 Duration inside of a 0 or // greater TimeSpan an infinite number of times. But that's // not really helpful to the user. // RepeatBehavior.Forever cases: // The situation here is that we have done an infinite amount // of work in zero time, so exact answers are hard to determine: // The "correct" current iteration may be Double.PositiveInfinity, // however returning this just makes our API too tricky to work with. // There is no "correct" current progress. // In both cases we'll say we repeated one whole iteration exactly // once to make things easy for the user. _currentIteration = 1; currentProgress = 1.0; } // Adjust progress for AutoReverse. if (_timeline.AutoReverse) { if (currentProgress == 1.0) { currentProgress = 0.0; } else if (currentProgress < 0.5) { currentProgress *= 2.0; } else { currentProgress = 1.0 - ((currentProgress - 0.5) * 2.0); } } _currentProgress = currentProgress; return true; } else // CurrentDuration.TimeSpan != TimeSpan.Zero { if (_currentClockState == ClockState.Filling && repeatBehavior.HasCount && !_currentIterationBeginTime.HasValue) { // // This block definitely needs a long comment. // // Basically, roundoff errors in the computation of offsetFromBegin // can cause us to calculate localProgress incorrectly. // Normally this doesn't matter, since we'll be off by a // miniscule amount. However, if we have a Filling Clock that // is exactly on a boundary, offsetFromBegin % duration should be 0. // We check for this special case below. If we have any rounding // errors in this situation, the modulus will not be 0 and we'll // Fill at the wrong place (for example, we may think the Clock // is Filling at the very beginning of its second iteration when // it should be Filling at the very end of its first). // // The precision error is due to dividing, then multiplying by, // appliedSpeedRatio when computing offsetFromBegin(to see this trace // back the computation of parentTime, expirationTime, and // effectiveDuration). The specific codepath that does // this is only executed when we have a Filling clock with // RepeatBehavior.HasCount. In this special case we can avoid // the precision error by calculating offsetFromBegin directly. // TimeSpan optimizedOffsetFromBegin; double scalingFactor = repeatBehavior.Count; if (_timeline.AutoReverse) { scalingFactor *= 2; } optimizedOffsetFromBegin = MultiplyTimeSpan(_resolvedDuration.TimeSpan, scalingFactor); Debug_VerifyOffsetFromBegin(offsetFromBegin.Ticks, optimizedOffsetFromBegin.Ticks); offsetFromBegin = optimizedOffsetFromBegin; } int newIteration; if (_currentIterationBeginTime.HasValue) { ComputeCurrentIterationWithGrow(parentTime, expirationTime, out localProgress, out newIteration); } else // Regular scenario -- no Grow behavior { localProgress = TimeSpan.FromTicks(offsetFromBegin.Ticks % _currentDuration.TimeSpan.Ticks); newIteration = (int)(offsetFromBegin.Ticks / _resolvedDuration.TimeSpan.Ticks); // Iteration count starting from 0 } // Iteration boundary cases depend on which direction the parent progresses and if we are Filling if ((localProgress == TimeSpan.Zero) && (newIteration > 0) // Detect a boundary case past the first zero (begin point) && (_currentClockState == ClockState.Filling || _parent.IsBackwardsProgressingGlobal)) { // Special post-fill case: // We hit 0 progress because of wraparound in modulo arithmetic. However, for post-fill we don't // want to wrap around to zero; we compensate for that here. The only legal way to hit zero // post-fill is at the ends of autoreversed segments, which are handled by logic further below. // Note that parentTime is clamped to expirationTime in post-fill situations, so even if the // actual parentTime is larger, it would still get clamped and then potentially wrapped to 0. // We are at 100% progress of previous iteration, instead of 0% progress of next one // Back up to previous iteration localProgress = _currentDuration.TimeSpan; newIteration--; } // Invert the localProgress for odd (AutoReversed) paths if (_timeline.AutoReverse) { if ((newIteration & 1) == 1) // We are on a reversing segment { if (localProgress == TimeSpan.Zero) { // We're exactly at an AutoReverse inflection point. Any active children // of this clock will be filling for this point only. The next tick // time needs to be 0 so that they can go back to active; filling clocks // aren't ordinarily ticked. InternalNextTickNeededTime = TimeSpan.Zero; } localProgress = _currentDuration.TimeSpan - localProgress; IsBackwardsProgressingGlobal = !IsBackwardsProgressingGlobal; parentSpeed = -parentSpeed; // Negate parent speed here for tick logic, since we negated localProgress } newIteration = newIteration / 2; // Definition of iteration with AutoReverse is a front and back segment, divide by 2 } _currentIteration = 1 + newIteration; // Officially, iterations are numbered from 1 // This is where we predict tick logic for approaching an iteration boundary // We only need to do this if NTWA == false because otherwise, we already have NTNT = zero if (_currentClockState == ClockState.Active && parentSpeed != 0 && !NeedsTicksWhenActive) { TimeSpan timeUntilNextBoundary; if (localProgress == TimeSpan.Zero) // We are currently exactly at a boundary { timeUntilNextBoundary = DivideTimeSpan(_currentDuration.TimeSpan, Math.Abs(parentSpeed)); } else if (parentSpeed > 0) // We are approaching the next iteration boundary (end or decel zone) { TimeSpan decelBegin = MultiplyTimeSpan(_currentDuration.TimeSpan, 1.0 - _timeline.DecelerationRatio); timeUntilNextBoundary = DivideTimeSpan(decelBegin - localProgress, parentSpeed); } else // parentSpeed < 0, we are approaching the previous iteration boundary { TimeSpan accelEnd = MultiplyTimeSpan(_currentDuration.TimeSpan, _timeline.AccelerationRatio); timeUntilNextBoundary = DivideTimeSpan(accelEnd - localProgress, parentSpeed); } TimeSpan proposedNextTickTime = CurrentGlobalTime + timeUntilNextBoundary; if (!InternalNextTickNeededTime.HasValue || proposedNextTickTime < InternalNextTickNeededTime.Value) { InternalNextTickNeededTime = proposedNextTickTime; } } } } else // CurrentDuration is Forever { Debug.Assert(_currentDuration == Duration.Forever, "_currentDuration has an invalid enum value."); Debug.Assert(_currentClockState == ClockState.Active || (_currentClockState == ClockState.Filling && expirationTime.HasValue && parentTime >= expirationTime)); localProgress = offsetFromBegin; _currentIteration = 1; // We have infinite duration, so iteration is 1 } return false; // We aren't done computing state yet } // This should only be called for nodes which have RepeatBehavior and have ancestors which can slip; // It gets called after we move from our current iteration to a new one private void ComputeCurrentIterationWithGrow(TimeSpan parentTime, TimeSpan? expirationTime, out TimeSpan localProgress, out int newIteration) { Debug.Assert(this is ClockGroup, "ComputeCurrentIterationWithGrow should only run on ClockGroups."); Debug.Assert(CanGrow, "ComputeCurrentIterationWithGrow should only run on clocks with CanGrow."); Debug.Assert(_currentIterationBeginTime.HasValue, "ComputeCurrentIterationWithGrow should only be called when _currentIterationBeginTime has a value."); Debug.Assert(_resolvedDuration.HasTimeSpan, "ComputeCurrentIterationWithGrow should only be called when _resolvedDuration has a value."); // We must have a computed duration Debug.Assert(_currentDuration.HasTimeSpan, "ComputeCurrentIterationWithGrow should only be called when _currentDuration has a value."); TimeSpan offsetFromBegin = MultiplyTimeSpan(parentTime - _currentIterationBeginTime.Value, _appliedSpeedRatio); int iterationIncrement; if (offsetFromBegin < _currentDuration.TimeSpan) // We fall within the same iteration as during last tick { localProgress = offsetFromBegin; iterationIncrement = 0; } else // offsetFromBegin is larger than _currentDuration, so we have moved at least one iteration up { // This iteration variable is actually 0-based, but if we got into this IF block, we are past 0th iteration long offsetOnLaterIterations = (offsetFromBegin - _currentDuration.TimeSpan).Ticks; localProgress = TimeSpan.FromTicks(offsetOnLaterIterations % _resolvedDuration.TimeSpan.Ticks); iterationIncrement = 1 + (int)(offsetOnLaterIterations / _resolvedDuration.TimeSpan.Ticks); // Now, adjust to a new current iteration: // Use the current and resolved values of Duration to compute the beginTime for the latest iteration // We know that we at least have passed the current iteration, so add _currentIteration; // If we also passed subsequent iterations, then assume they have perfect durations (_resolvedDuration) each. _currentIterationBeginTime += _currentDuration.TimeSpan + MultiplyTimeSpan(_resolvedDuration.TimeSpan, iterationIncrement - 1); // If we hit the Filling state, we could fall exactly on the iteration finish boundary. // In this case, step backwards one iteration so that we are one iteration away from the finish if (_currentClockState == ClockState.Filling && expirationTime.HasValue && _currentIterationBeginTime >= expirationTime) { if (iterationIncrement > 1) // We last added a resolvedDuration, subtract it back out { _currentIterationBeginTime -= _resolvedDuration.TimeSpan; } else // iterationIncrement == 1, we only added a currentDuration, subtract it back out { _currentIterationBeginTime -= _currentDuration.TimeSpan; } } else // We have not encountered a false finish due to entering Fill state { // Reset all children's slip time here; NOTE that this will change our effective duration, // but this will not become important until the next tick, when it will be recomputed anyway. ResetSlipOnSubtree(); } } newIteration = _currentIteration.HasValue ? iterationIncrement + (_currentIteration.Value - 1) : iterationIncrement; } /// /// Determine if we are active, filling, or off /// parentTime is clamped if it is inside the postfill zone /// We have to handle reversed parent differently because we have closed-open intervals in global time /// /// Our computed expiration time, null if infinite. /// Our parent time. /// Our parent speed. /// Whether we are called from within a tick. ///private bool ComputeCurrentState(TimeSpan? expirationTime, ref TimeSpan parentTime, double parentSpeed, bool isInTick) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_beginTime.HasValue); FillBehavior fillBehavior = _timeline.FillBehavior; if (parentTime < _beginTime) // Including special backward progressing case { ResetCachedStateToStopped(); return true; // Nothing more to compute here } else if ( expirationTime.HasValue && parentTime >= expirationTime) // We are in postfill zone { RaiseCompletedForRoot(isInTick); if (fillBehavior == FillBehavior.HoldEnd) #if IMPLEMENTED // Uncomment when we enable new FillBehaviors || fillBehavior == FillBehavior.HoldBeginAndEnd) #endif { ResetCachedStateToFilling(); parentTime = expirationTime.Value; // Clamp parent time to expiration time // We still don't know our current time or progress at this point } else { ResetCachedStateToStopped(); return true; // We are off, nothing more to compute } } else // Else we are inside the active interval and thus active { _currentClockState = ClockState.Active; } // This is where we short-circuit Next Tick Needed logic while we are active if (parentSpeed != 0 && _currentClockState == ClockState.Active && NeedsTicksWhenActive) { InternalNextTickNeededTime = TimeSpan.Zero; // We need ticks immediately } return false; // There is more state to compute } // If we reach this function, we are active private bool ComputeCurrentSpeed(double localSpeed) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_currentClockState == ClockState.Active); // Must be active at this point if (IsInteractivelyPaused) { _currentGlobalSpeed = 0; } else { localSpeed *= _appliedSpeedRatio; if (IsBackwardsProgressingGlobal) // Negate speed if we are on a backwards arc of an autoreversing timeline { localSpeed = -localSpeed; } // Get global speed by multiplying by parent global speed _currentGlobalSpeed = localSpeed * _parent._currentGlobalSpeed; } return false; // There may be more state to compute yet } // Determines the time and local speed with accel+decel // At the end of this function call, the following state attributes are initialized: // CurrentProgress // CurrentTime private bool ComputeCurrentTime(TimeSpan localProgress, out double localSpeed) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_currentClockState != ClockState.Stopped); Debug.Assert(_currentDuration != Duration.Automatic, "_currentDuration should never be Automatic."); if (_currentDuration.HasTimeSpan) // Finite duration, need to apply accel/decel { Debug.Assert(_currentDuration.TimeSpan > TimeSpan.Zero, "ComputeCurrentTime was entered with _currentDuration <= 0"); double userAcceleration = _timeline.AccelerationRatio; double userDeceleration = _timeline.DecelerationRatio; double transitionTime = userAcceleration + userDeceleration; // The following assert is enforced when the Acceleration or Deceleration are set. Debug.Assert(transitionTime <= 1, "The values of the accel and decel attributes incorrectly add to more than 1.0"); Debug.Assert(transitionTime >= 0, "The values of the accel and decel attributes incorrectly add to less than 0.0"); double durationInTicks = (double)_currentDuration.TimeSpan.Ticks; double t = ((double)localProgress.Ticks) / durationInTicks; // For tracking progress if (transitionTime == 0) // Case of no accel/decel { localSpeed = 1; _currentTime = localProgress; } else { double maxRate = 2 / (2 - transitionTime); if (t < userAcceleration) { // Acceleration phase localSpeed = maxRate * t / userAcceleration; t = maxRate * t * t / (2 * userAcceleration); // Fix for bug 118853: Animations with Deceleration cause the Timing system to // keep ticking while idle. Only reset NextTickNeededTime when we are // Active. When we (or our parent) is Filling, there is no non-linear // unpredictability to our behavior that requires us to reset NextTickNeededTime. if (_currentClockState == ClockState.Active && _parent._currentClockState == ClockState.Active) { // We are in a non-linear segment, cannot linearly predict anything InternalNextTickNeededTime = TimeSpan.Zero; } } else if (t <= (1 - userDeceleration)) { // Run-rate phase localSpeed = maxRate; t = maxRate * (t - userAcceleration / 2); } else { // Deceleration phase double tc = 1 - t; // t's complement from 1 localSpeed = maxRate * tc / userDeceleration; t = 1 - maxRate * tc * tc / (2 * userDeceleration); // Fix for bug 118853: Animations with Deceleration cause the Timing system to // keep ticking while idle. Only reset NextTickNeededTime when we are // Active. When we (or our parent) is Filling, there is no non-linear // unpredictability to our behavior that requires us to reset NextTickNeededTime. if (_currentClockState == ClockState.Active && _parent._currentClockState == ClockState.Active) { // We are in a non-linear segment, cannot linearly predict anything InternalNextTickNeededTime = TimeSpan.Zero; } } _currentTime = TimeSpan.FromTicks((long)((t * durationInTicks) + 0.5)); } _currentProgress = t; } else // CurrentDuration is Forever { Debug.Assert(_currentDuration == Duration.Forever, "_currentDuration has an invalid enum value."); _currentTime = localProgress; _currentProgress = 0; localSpeed = 1; } return (_currentClockState != ClockState.Active); // Proceed to calculate global speed if we are active } // Compute the duration private void ResolveDuration() { Debug.Assert(!IsTimeManager); if (!HasResolvedDuration) { Duration duration = NaturalDuration; if (duration != Duration.Automatic) { _resolvedDuration = duration; _currentDuration = duration; // If CurrentDuration is different, we update it later in this method HasResolvedDuration = true; } else { Debug.Assert(_resolvedDuration == Duration.Forever, "_resolvedDuration should be Forever when NaturalDuration is Automatic."); } } if (CanGrow) { _currentDuration = CurrentDuration; if (_currentDuration == Duration.Automatic) { _currentDuration = Duration.Forever; // We treat Automatic as unresolved current duration } } // We have descendants (such as Media) which don't know their duration yet. Note that this won't prevent us // from resolving our own Duration when it is explicitly set on the ParallelTimeline; therefore, we keep // a separate flag for the entire subtree. if (HasDescendantsWithUnresolvedDuration) { UpdateDescendantsWithUnresolvedDuration(); // See if this is still the case } } // This returns the effective duration of a clock. The effective duration is basically the // length of the clock's active period, taking into account speed ratio, repeat, and autoreverse. // Null is used to represent an infinite effective duration. private TimeSpan? ComputeEffectiveDuration() { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped || IsRoot); ResolveDuration(); Debug.Assert(_resolvedDuration != Duration.Automatic, "_resolvedDuration should never be Automatic."); Debug.Assert(_currentDuration != Duration.Automatic, "_currentDuration should never be Automatic."); TimeSpan? effectiveDuration; RepeatBehavior repeatBehavior = _timeline.RepeatBehavior; if (_currentDuration.HasTimeSpan && _currentDuration.TimeSpan == TimeSpan.Zero) { // Zero-duration case ignores any repeat behavior effectiveDuration = TimeSpan.Zero; } else if (repeatBehavior.HasCount) { if (repeatBehavior.Count == 0) // This clause avoids multiplying an infinite duration by zero { effectiveDuration = TimeSpan.Zero; } else if (_currentDuration == Duration.Forever) { effectiveDuration = null; // We use Null to represent infinite duration } else if (!CanGrow) // Case of finite duration { Debug.Assert(_currentDuration.HasTimeSpan, "_currentDuration is invalid, neither Forever nor a TimeSpan."); Debug.Assert(_currentDuration == _resolvedDuration, "For clocks which cannot grow, _currentDuration must equal _resolvedDuration."); double scalingFactor = repeatBehavior.Count / _appliedSpeedRatio; if (_timeline.AutoReverse) { scalingFactor *= 2; } effectiveDuration = MultiplyTimeSpan(_currentDuration.TimeSpan, scalingFactor); } else // Finite duration, CanGrow: _currentDuration may be different from _resolvedDuration { Debug.Assert(_resolvedDuration.HasTimeSpan, "_resolvedDuration is invalid, neither Forever nor a TimeSpan."); Debug.Assert(_currentDuration.HasTimeSpan, "_currentDuration is invalid, neither Forever nor a TimeSpan."); TimeSpan previousIterationDuration = TimeSpan.Zero; double presentAndFutureIterations = repeatBehavior.Count; double presentAndFutureDuration; // Note: this variable is not scaled by speedRatio, we scale it further down: // If we have growth, we have to take prior iterations into account as a FIXED time span if (CanGrow && _currentIterationBeginTime.HasValue && _currentIteration.HasValue) { Debug.Assert(_beginTime.HasValue); // _currentIterationBeginTime.HasValue implies _beginTime.HasValue presentAndFutureIterations -= (_currentIteration.Value - 1); previousIterationDuration = _currentIterationBeginTime.Value - _beginTime.Value; } if (presentAndFutureIterations <= 1) // This means we are on our last iteration { presentAndFutureDuration = ((double)_currentDuration.TimeSpan.Ticks) * presentAndFutureIterations; } else // presentAndFutureIterations > 1, we are not at the last iteration, so count _currentDuration a full one time { presentAndFutureDuration = ((double)_currentDuration.TimeSpan.Ticks) // Current iteration; below is the future iteration length + ((double)_resolvedDuration.TimeSpan.Ticks) * (presentAndFutureIterations - 1); } if (_timeline.AutoReverse) { presentAndFutureDuration *= 2; // Double the remaining duration with AutoReverse } effectiveDuration = TimeSpan.FromTicks((long)(presentAndFutureDuration / _appliedSpeedRatio + 0.5)) + previousIterationDuration; } } else if (repeatBehavior.HasDuration) { effectiveDuration = repeatBehavior.Duration; } else // Repeat behavior is Forever { Debug.Assert(repeatBehavior == RepeatBehavior.Forever); // Only other valid enum value effectiveDuration = null; } return effectiveDuration; } // Run new eventing logic on root nodes // Note that Completed events are fired from a different place (currently, ComputeCurrentState) private void ComputeEvents(TimeSpan? expirationTime, TimeIntervalCollection parentIntervalCollection) { // We clear CurrentIntervals here in case that we don't reinitialize it in the method. // Unless there is something to project, we assume the null state ClearCurrentIntervalsToNull(); if (_beginTime.HasValue // If we changed to a paused state during this tick then we still // changed state and the events should be computed. // If we were paused and we resumed during this tick, even though // we are now active, no progress has been made during this tick. && !(IsInteractivelyPaused ^ PauseStateChangedDuringTick)) { Duration postFillDuration; // This is Zero when we have no fill zone if (expirationTime.HasValue) { postFillDuration = Duration.Forever; } else { postFillDuration = TimeSpan.Zero; // There is no reachable postfill zone because the active period is infinite } // if (!expirationTime.HasValue // If activePeriod extends forever, || expirationTime >= _beginTime) // OR if activePeriod extends to or beyond _beginTime, { // Check for CurrentTimeInvalidated TimeIntervalCollection activePeriod; if (expirationTime.HasValue) { if (expirationTime == _beginTime) { activePeriod = TimeIntervalCollection.Empty; } else { activePeriod = TimeIntervalCollection.CreateClosedOpenInterval(_beginTime.Value, expirationTime.Value); } } else // expirationTime is infinity { activePeriod = TimeIntervalCollection.CreateInfiniteClosedInterval(_beginTime.Value); } // If we have an intersection between parent domain times and the interval over which we // change, our time was invalidated if (parentIntervalCollection.Intersects(activePeriod)) { ComputeIntervalsWithParentIntersection( parentIntervalCollection, activePeriod, expirationTime, postFillDuration); } else if (postFillDuration != TimeSpan.Zero && // Our active period is finite and we have fill behavior _timeline.FillBehavior == FillBehavior.HoldEnd) // Check for state changing between Filling and Stopped { ComputeIntervalsWithHoldEnd( parentIntervalCollection, expirationTime); } } } // It is important to launch this method at the end of ComputeEvents, because it checks // whether the StateInvalidated event had been raised earlier in this method. if (PendingInteractiveRemove) { RaiseRemoveRequestedForRoot(); RaiseCompletedForRoot(true); // At this time, we also want to raise the Completed event; // this code always runs during a tick. PendingInteractiveRemove = false; } } // Find end time that is defined by our repeat behavior. Null is used to represent // an infinite expiration time. private bool ComputeExpirationTime(out TimeSpan? expirationTime) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped || IsRoot); TimeSpan? effectiveDuration; // if (!_beginTime.HasValue) { Debug.Assert(!_currentIterationBeginTime.HasValue, "_currentIterationBeginTime should not have a value when _beginTime has no value."); expirationTime = null; return true; } Debug.Assert(_beginTime.HasValue); effectiveDuration = ComputeEffectiveDuration(); if (effectiveDuration.HasValue) { expirationTime = _beginTime + effectiveDuration; // Precaution against slipping at the last frame of media: don't permit the clock to finish this tick yet if (_syncData != null && _syncData.IsInSyncPeriod && !_syncData.SyncClockHasReachedEffectiveDuration) { expirationTime += TimeSpan.FromMilliseconds(50); // This compensation is roughly one frame of video } } else { expirationTime = null; // infinite expiration time } return false; // More state to compute } // Compute values for root children that reflect interactivity // We may modify SpeedRatio if the interactive SpeedRatio was changed private bool ComputeInteractiveValues() { bool exitEarly = false; Debug.Assert(IsRoot); // Check for a pending stop first. This allows us to exit early. if (PendingInteractiveStop) { PendingInteractiveStop = false; IsInteractivelyStopped = true; // If we are disabled, no other interactive state is kept _beginTime = null; _currentIterationBeginTime = null; if (CanGrow) { ResetSlipOnSubtree(); } // Process the state for the whole subtree; the events will later // be replaced with invalidation-style events PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) // { Clock current = subtree.Current; if (current._currentClockState != ClockState.Stopped) { current.ResetCachedStateToStopped(); current.RaiseCurrentStateInvalidated(); current.RaiseCurrentTimeInvalidated(); current.RaiseCurrentGlobalSpeedInvalidated(); } else { subtree.SkipSubtree(); } } } if (IsInteractivelyStopped) { // If we are disabled, no other interactive state is kept Debug.Assert(_beginTime == null); Debug.Assert(_currentClockState == ClockState.Stopped); ResetCachedStateToStopped(); InternalNextTickNeededTime = null; // Can't return here: still need to process pending pause and resume // which can be set independent of the current state of the clock. exitEarly = true; } else { // Clocks that are currently paused or have a pending seek, begin, or change to the // speed ratio need to adjust their begin time. AdjustBeginTime(); } // // If we were about to pause or resume, set flags accordingly. // This must be done after adjusting the begin time so that we don't // appear to pause one tick early. // if (PendingInteractivePause) { Debug.Assert(!IsInteractivelyPaused); // Enforce invariant: cannot be pausePending when already paused Debug.Assert(!PendingInteractiveResume); // Enforce invariant: cannot be both pause and resumePending PendingInteractivePause = false; RaiseCurrentGlobalSpeedInvalidated(); // Update paused state for the entire subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.IsInteractivelyPaused = true; subtree.Current.PauseStateChangedDuringTick = true; } } if (PendingInteractiveResume) { Debug.Assert(IsInteractivelyPaused); Debug.Assert(!PendingInteractivePause); // We will no longer have to do paused begin time adjustment PendingInteractiveResume = false; // During pause, our speed was zero. Unless we are filling, invalidate the speed. if (_currentClockState != ClockState.Filling) { RaiseCurrentGlobalSpeedInvalidated(); } // Update paused state for the entire subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.IsInteractivelyPaused = false; subtree.Current.PauseStateChangedDuringTick = true; } } return exitEarly; } private void ComputeIntervalsWithHoldEnd( TimeIntervalCollection parentIntervalCollection, TimeSpan? endOfActivePeriod) { Debug.Assert(endOfActivePeriod.HasValue); TimeIntervalCollection fillPeriod = TimeIntervalCollection.CreateInfiniteClosedInterval(endOfActivePeriod.Value); if (parentIntervalCollection.Intersects(fillPeriod)) // We enter or leave Fill period { TimeSpan relativeBeginTime = _currentIterationBeginTime.HasValue ? _currentIterationBeginTime.Value : _beginTime.Value; ComputeCurrentFillInterval(parentIntervalCollection, relativeBeginTime, endOfActivePeriod.Value, _currentDuration, _appliedSpeedRatio, _timeline.AccelerationRatio, _timeline.DecelerationRatio, _timeline.AutoReverse); if (parentIntervalCollection.IntersectsInverseOf(fillPeriod)) // ... and we don't intersect the Active period, so we must go in or out of the Stopped period. { RaiseCurrentStateInvalidated(); RaiseCurrentTimeInvalidated(); RaiseCurrentGlobalSpeedInvalidated(); AddNullPointToCurrentIntervals(); // Count the stopped state by projecting the null point. } } } private void ComputeIntervalsWithParentIntersection( TimeIntervalCollection parentIntervalCollection, TimeIntervalCollection activePeriod, TimeSpan? endOfActivePeriod, Duration postFillDuration) { // Make sure that our periodic function is aligned to the boundary of the current iteration, regardless of prior slip TimeSpan relativeBeginTime = _currentIterationBeginTime.HasValue ? _currentIterationBeginTime.Value : _beginTime.Value; RaiseCurrentTimeInvalidated(); // Check for state changing between Active and the union of (Filling, Stopped) if (parentIntervalCollection.IntersectsInverseOf(activePeriod)) { RaiseCurrentStateInvalidated(); RaiseCurrentGlobalSpeedInvalidated(); } else if (parentIntervalCollection.IntersectsPeriodicCollection( relativeBeginTime, _currentDuration, _appliedSpeedRatio, _timeline.AccelerationRatio, _timeline.DecelerationRatio, _timeline.AutoReverse)) // Else we were always inside the active period, check for non-linear speed invalidations { RaiseCurrentGlobalSpeedInvalidated(); } // Else our speed has not changed, but our iteration may have been invalidated else if (parentIntervalCollection.IntersectsMultiplePeriods( relativeBeginTime, _currentDuration, _appliedSpeedRatio)) { HasDiscontinuousTimeMovementOccured = true; if (_syncData != null) { _syncData.SyncClockDiscontinuousEvent = true; // Notify the syncing node of discontinuity } } // Compute our output intervals ComputeCurrentIntervals(parentIntervalCollection, relativeBeginTime, endOfActivePeriod, postFillDuration, _currentDuration, _appliedSpeedRatio, _timeline.AccelerationRatio, _timeline.DecelerationRatio, _timeline.AutoReverse); } /// /// GillesK: performTickOperations means that we process the pending events /// private void ComputeLocalStateHelper(bool performTickOperations, bool seekedAlignedToLastTick) { Debug.Assert(!IsTimeManager); TimeSpan? parentTime; // Computed parent-local time TimeSpan? expirationTime; // Computed expiration time with respect to repeat behavior and resolved duration; TimeSpan localProgress; // Computed time inside simple duration TimeSpan parentTimeValue; double? parentSpeed; // Parent's CurrentGlobalSpeed TimeIntervalCollection parentIntervalCollection; double localSpeed; bool returnDelayed = false; // // In this function, 'true' return values allow us to exit early // We first compute parent parameters; with SlipBehavior, we may modify our parentIntervalCollection if (ComputeParentParameters(out parentTime, out parentSpeed, out parentIntervalCollection, seekedAlignedToLastTick)) { returnDelayed = true; } // Now take potential SlipBehavior into account: if (_syncData != null && _syncData.IsInSyncPeriod && _parent.CurrentState != ClockState.Stopped) // We are already in a slip zone { Debug.Assert(parentTime.HasValue); // If parent isn't stopped, it must have valid time and speed Debug.Assert(parentSpeed.HasValue); // ComputeSyncSlip(ref parentIntervalCollection, parentTime.Value, parentSpeed.Value); } ResolveDuration(); // We only calculate the Interactive values when we are processing the pending events if (performTickOperations && IsRoot) { // Special case for root-children, which may have interactivity if (ComputeInteractiveValues()) { returnDelayed = true; } } // Check whether we are entering a [....] period. This includes cases when we have // ticked before the beginning, then past the end of a [....] period; we still have to // move back to the exact beginning of the tick period. We handle cases where // we seek (HasSeekOccuredAfterLastTick) in a special way, by not synchronizing with the beginning. // Also, if the parent has been paused prior to this tick, we cannot enter the [....] zone, so skip the call. if (_syncData != null && !_syncData.IsInSyncPeriod && _parent.CurrentState != ClockState.Stopped && (!parentIntervalCollection.IsEmptyOfRealPoints || HasSeekOccuredAfterLastTick)) { Debug.Assert(parentTime.HasValue); // Cannot be true unless parent is stopped // We use the parent's TIC as a way of determining its earliest non-null time ComputeSyncEnter(ref parentIntervalCollection, parentTime.Value); } if (ComputeExpirationTime(out expirationTime)) { returnDelayed = true; } // Run the eventing logic here if (performTickOperations) { ComputeEvents(expirationTime, parentIntervalCollection); } if (returnDelayed) // If we delayed returning until now, proceed to do so { return; } Debug.Assert(_beginTime.HasValue); Debug.Assert(parentTime.HasValue); parentTimeValue = parentTime.Value; // Determines the next time we need to tick if (ComputeNextTickNeededTime(expirationTime, parentTimeValue, parentSpeed.Value)) { return; } // Determines if we are active, filling, or off if (ComputeCurrentState(expirationTime, ref parentTimeValue, parentSpeed.Value, performTickOperations)) { return; } // Determines the current iteration if (ComputeCurrentIteration(parentTimeValue, parentSpeed.Value, expirationTime, out localProgress)) { return; } // Determines the current time and local speed with accel+decel if (ComputeCurrentTime(localProgress, out localSpeed)) { return; } // Determines the current speed if (ComputeCurrentSpeed(localSpeed)) { return; } } // // Determine the next needed tick time for approaching a StateInvalidated boundary // // Note that ComputeCurrentIteration and ComputeCurrentState also modify the // NextTickNeededTime and consequently must run after this method. // private bool ComputeNextTickNeededTime(TimeSpan? expirationTime, TimeSpan parentTime, double parentSpeed) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_beginTime.HasValue); if (parentSpeed == 0) { // InternalNextTickNeededTime = IsInteractivelyPaused ? TimeSpan.Zero : (TimeSpan?)null; } else { double invertedParentSpeed = 1.0 / parentSpeed; TimeSpan? timeUntilNextBoundary = null; // // Calculate the time in ms until begin or expiration time. // They are positive if we're heading towards one of these periods, negative if heading away. // This takes into account reversing clocks (so a clock heading back to begin will have // a positive timeUntilBegin). // // timeUntilNextBoundary will be the first of these three boundaries that we hit. // Negative values are obviously ignored. // TimeSpan timeUntilBegin = MultiplyTimeSpan(_beginTime.Value - parentTime, invertedParentSpeed); // // If the time until a boundary is 0 (i.e. we've ticked exactly on a boundary) // we'll ask for another tick immediately. // This is only relevant for reversing clocks, which, when on a boundary, are defined // to have the 'previous' state, not the 'next' state. Thus they need one more // tick for the state change to happen. // if (timeUntilBegin >= TimeSpan.Zero) { timeUntilNextBoundary = timeUntilBegin; } if (expirationTime.HasValue) { TimeSpan timeUntilExpiration = MultiplyTimeSpan(expirationTime.Value - parentTime, invertedParentSpeed); if (timeUntilExpiration >= TimeSpan.Zero && (!timeUntilNextBoundary.HasValue || timeUntilExpiration < timeUntilNextBoundary.Value)) { timeUntilNextBoundary = timeUntilExpiration; } } // // Set the next tick needed time depending on whether we're // headed towards a boundary. // if (timeUntilNextBoundary.HasValue) { // We are moving towards some ClockState boundary either begin or expiration InternalNextTickNeededTime = CurrentGlobalTime + timeUntilNextBoundary; } else { // We are not moving towards any boundary points InternalNextTickNeededTime = null; } } return false; } // Compute the parent's time; by the time we reach this method, we know that we // have a non-root parent. private bool ComputeParentParameters(out TimeSpan? parentTime, out double? parentSpeed, out TimeIntervalCollection parentIntervalCollection, bool seekedAlignedToLastTick) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped || IsRoot); if (IsRoot) // We are a root child, use time manager time { Debug.Assert(_rootData != null, "A root Clock must have the _rootData structure initialized."); HasSeekOccuredAfterLastTick = seekedAlignedToLastTick || (_rootData.PendingSeekDestination != null); // We may have a seek request pending // We don't have a TimeManager that is on, so we are off, nothing more to compute if (_timeManager == null || _timeManager.InternalIsStopped) { ResetCachedStateToStopped(); parentTime = null; // Assign parentTime to avoid compiler error parentSpeed = null; InternalNextTickNeededTime = TimeSpan.Zero; // When TimeManager wakes up, we will need an update parentIntervalCollection = TimeIntervalCollection.Empty; return true; } else // We have a valid global time; { parentSpeed = 1.0; // TimeManager defines the rate at which time moves, e.g. it moves at 1X speed parentIntervalCollection = _timeManager.InternalCurrentIntervals; if (HasDesiredFrameRate) { // Change the parent's interval collection to include all time intervals since the last time // we ticked this root node. Due to DFR, we may have skipped a number of "important" ticks. parentTime = _rootData.CurrentAdjustedGlobalTime; if (!parentIntervalCollection.IsEmptyOfRealPoints) { // parentIntervalCollection = parentIntervalCollection.SetBeginningOfConnectedInterval( _rootData.LastAdjustedGlobalTime); } } else { parentTime = _timeManager.InternalCurrentGlobalTime; } return false; } } else // We are a deeper node { HasSeekOccuredAfterLastTick = seekedAlignedToLastTick || _parent.HasSeekOccuredAfterLastTick; // We may have a seek request pending parentTime = _parent._currentTime; // This is Null if parent is off; we still init the 'out' parameter parentSpeed = _parent._currentGlobalSpeed; parentIntervalCollection = _parent.CurrentIntervals; // Find the parent's current time if (_parent._currentClockState != ClockState.Stopped) // We have a parent that is active or filling { return false; } else // Else parent is off, so we are off, nothing more to compute { // Before setting our state to Stopped make sure that we // fire the proper event if we change state. if (_currentClockState != ClockState.Stopped) { RaiseCurrentStateInvalidated(); RaiseCurrentGlobalSpeedInvalidated(); RaiseCurrentTimeInvalidated(); } ResetCachedStateToStopped(); InternalNextTickNeededTime = null; return true; } } } // Abbreviations for variables expressing time units: // PT: Parent time (e.g. our begin time is expressed in parent time coordinates) // LT: Local time (e.g. our duration is expressed in local time coordinates) // ST: [....] time -- this is the same as local time iff (_syncData.SyncClock == this) e.g. we are the [....] clock, // otherwise it is our child's time coordinates (this happens when we are a container with SlipBehavior.Slip). // SPT: The [....] clock's parent's time coordinates. When this IS the [....] clock, this is our parent coordinates. // otherwise, it is our local coordinates. private void ComputeSyncEnter(ref TimeIntervalCollection parentIntervalCollection, TimeSpan currentParentTimePT) { Debug.Assert(!IsTimeManager); Debug.Assert(_parent != null); Debug.Assert(_parent.CurrentState != ClockState.Stopped); // Parent is not stopped, so its TIC cannot be empty Debug.Assert(HasSeekOccuredAfterLastTick || (!parentIntervalCollection.IsEmptyOfRealPoints && parentIntervalCollection.FirstNodeTime <= currentParentTimePT)); // SyncData points to our child if we have SlipBehavior, for CanSlip nodes it points to the node itself Debug.Assert(_syncData.SyncClock == this || _syncData.SyncClock._parent == this); Debug.Assert(CanSlip || _timeline is ParallelTimeline && ((ParallelTimeline)_timeline).SlipBehavior == SlipBehavior.Slip); Debug.Assert(_syncData != null); Debug.Assert(!_syncData.IsInSyncPeriod); // Verify our limitations on slip functionality, but don't throw here for perf Debug.Assert(_timeline.AutoReverse == false); Debug.Assert(_timeline.AccelerationRatio == 0); Debug.Assert(_timeline.DecelerationRatio == 0); // With these limitations, we can easily preview our CurrentTime: if (_beginTime.HasValue && currentParentTimePT >= _beginTime.Value) { TimeSpan relativeBeginTimePT = _currentIterationBeginTime.HasValue ? _currentIterationBeginTime.Value : _beginTime.Value; TimeSpan previewCurrentOffsetPT = currentParentTimePT - relativeBeginTimePT; // This is our time offset (not yet scaled by speed) TimeSpan previewCurrentTimeLT = MultiplyTimeSpan(previewCurrentOffsetPT, _appliedSpeedRatio); // This is what our time would be // We can only enter [....] period if we are past the syncClock's begin time if (_syncData.SyncClock == this || previewCurrentTimeLT >= _syncData.SyncClockBeginTime) { // We have two very different scenarios: seek and non-seek enter if (HasSeekOccuredAfterLastTick) // We have seeked, see if we fell into a [....] period { // If we haven't returned yet, we are not past the end of the [....] period on the child // Also, we are not Stopped prior to BeginTime. TimeSpan? expirationTimePT; ComputeExpirationTime(out expirationTimePT); // This is to verify we did not seek past our active period duration if (!expirationTimePT.HasValue || currentParentTimePT < expirationTimePT.Value) { TimeSpan ourSyncTimeST = (_syncData.SyncClock == this) ? previewCurrentTimeLT : MultiplyTimeSpan(previewCurrentTimeLT - _syncData.SyncClockBeginTime, _syncData.SyncClockSpeedRatio); TimeSpan? syncClockEffectiveDurationST = _syncData.SyncClockEffectiveDuration; if (_syncData.SyncClock == this || !syncClockEffectiveDurationST.HasValue || ourSyncTimeST < syncClockEffectiveDurationST) { // If the [....] child has a specified duration Duration syncClockDuration = _syncData.SyncClockResolvedDuration; if (syncClockDuration.HasTimeSpan) { _syncData.PreviousSyncClockTime = TimeSpan.FromTicks(ourSyncTimeST.Ticks % syncClockDuration.TimeSpan.Ticks); _syncData.PreviousRepeatTime = ourSyncTimeST - _syncData.PreviousSyncClockTime; } else if (syncClockDuration == Duration.Forever) { _syncData.PreviousSyncClockTime = ourSyncTimeST; _syncData.PreviousRepeatTime = TimeSpan.Zero; } else { Debug.Assert(syncClockDuration == Duration.Automatic); // If we seek into an Automatic syncChild's duration, we may overseek it, so throw an exception throw new InvalidOperationException(SR.Get(SRID.Timing_SeekDestinationAmbiguousDueToSlip)); } // This is the heart of the HasSeekOccuredAfterLastTick codepath; we don't adjust our // time, but note to do so for the succeeding ticks. _syncData.IsInSyncPeriod = true; } } } else // Non-seek, regular case { TimeSpan? previousSyncParentTimeSPT = (_syncData.SyncClock == this) ? parentIntervalCollection.FirstNodeTime : _currentTime; if (!previousSyncParentTimeSPT.HasValue || _syncData.SyncClockDiscontinuousEvent || previousSyncParentTimeSPT.Value <= _syncData.SyncClockBeginTime) // Not seeking this tick, different criteria for entering [....] period. // We don't care if we overshot the beginTime, because we will seek backwards // to match the child's beginTime exactly. // NOTE: _currentTime is actually our time at last tick, since it wasn't yet updated. { // First, adjust our beginTime so that we match the syncClock's begin, accounting for SpeedRatio TimeSpan timeIntoSyncPeriodPT = previewCurrentOffsetPT; if (_syncData.SyncClock != this) // SyncClock is our child; account for SyncClock starting later than us { timeIntoSyncPeriodPT -= DivideTimeSpan(_syncData.SyncClockBeginTime, _appliedSpeedRatio); } // Offset our position to [....] with media begin if (_currentIterationBeginTime.HasValue) { _currentIterationBeginTime += timeIntoSyncPeriodPT; } else { _beginTime += timeIntoSyncPeriodPT; } // UpdateSyncBeginTime(); // This ensures that our _cutoffTime is correctly applied // Now, update the parent TIC to compensate for our slip parentIntervalCollection = parentIntervalCollection.SlipBeginningOfConnectedInterval(timeIntoSyncPeriodPT); _syncData.IsInSyncPeriod = true; _syncData.PreviousSyncClockTime = TimeSpan.Zero; _syncData.PreviousRepeatTime = TimeSpan.Zero; _syncData.SyncClockDiscontinuousEvent = false; } } } } } // Abbreviations for variables expressing time units: // PT: Parent time (e.g. our begin time is expressed in parent time coordinates) // LT: Local time (e.g. our duration is expressed in local time coordinates) // ST: [....] time -- this is the same as local time iff (_syncData.SyncClock == this) e.g. we are the [....] clock, // otherwise it is our child's time coordinates (this happens when we are a container with SlipBehavior.Slip). // SPT: The [....] clock's parent's time coordinates. When this IS the [....] clock, this is our parent coordinates. // otherwise, it is our local coordinates. private void ComputeSyncSlip(ref TimeIntervalCollection parentIntervalCollection, TimeSpan currentParentTimePT, double currentParentSpeed) { Debug.Assert(!IsTimeManager); Debug.Assert(_parent != null); Debug.Assert(_syncData != null); Debug.Assert(_syncData.IsInSyncPeriod); // SyncData points to our child if we have SlipBehavior, for CanSlip nodes it points to the node itself Debug.Assert(_syncData.SyncClock == this || _syncData.SyncClock._parent == this); // The overriding assumption for slip limitations is that the parent's intervals are // "connected", e.g. not broken into multiple intervals. This is always true for roots. Debug.Assert(!parentIntervalCollection.IsEmpty); // The parent isn't Stopped, so it must have a TIC // From now on, we assume that the parent's TIC begins from the parent's CurrentTime at // at the previous tick. Ensure that the parent is moving forward. Debug.Assert(parentIntervalCollection.IsEmptyOfRealPoints || parentIntervalCollection.FirstNodeTime <= currentParentTimePT); Debug.Assert(currentParentSpeed >= 0); // We now extract this information from the TIC. If we are paused, we have an empty TIC and assume parent time has not changed. TimeSpan previousParentTimePT = parentIntervalCollection.IsEmptyOfRealPoints ? currentParentTimePT : parentIntervalCollection.FirstNodeTime; TimeSpan parentElapsedTimePT = currentParentTimePT - previousParentTimePT; // Our elapsed time is assumed to be a simple linear scale of the parent's time, // as long as we are inside of the [....] period. TimeSpan ourProjectedElapsedTimeLT = MultiplyTimeSpan(parentElapsedTimePT, _appliedSpeedRatio); TimeSpan syncTimeST = _syncData.SyncClock.GetCurrentTimeCore(); TimeSpan syncElapsedTimeST = syncTimeST - _syncData.PreviousSyncClockTime; // Elapsed from last tick if (syncElapsedTimeST > TimeSpan.Zero) // Only store the last value if it is greater than // the old value. Note we can use either >= or > here. { // Check whether [....] has reached the end of our effective duration TimeSpan? effectiveDurationST = _syncData.SyncClockEffectiveDuration; Duration syncDuration = _syncData.SyncClockResolvedDuration; if (effectiveDurationST.HasValue && (_syncData.PreviousRepeatTime + syncTimeST >= effectiveDurationST.Value)) { _syncData.IsInSyncPeriod = false; // This is the last time we need to [....] _syncData.PreviousRepeatTime = TimeSpan.Zero; _syncData.SyncClockDiscontinuousEvent = false; // Make sure we don't reenter the [....] period } // Else check if we should wrap the simple duration due to repeats, and set previous times accordingly else if (syncDuration.HasTimeSpan && syncTimeST >= syncDuration.TimeSpan) { // If we have a single repetition, then we would be done here; // However, we may just have reached the end of an iteration on repeating media; // In this case, we still [....] this particular moment, but we should reset the // previous [....] clock time to zero, and increment the PreviousRepeatTime. // This tick, media should pick up a corresponding DiscontinuousMovement caused // by a repeat, and reset itself to zero as well. _syncData.PreviousSyncClockTime = TimeSpan.Zero; _syncData.PreviousRepeatTime += syncDuration.TimeSpan; } else // Don't need to wrap around { _syncData.PreviousSyncClockTime = syncTimeST; } } else // If the [....] timeline went backwards, pretend it just didn't move. { syncElapsedTimeST = TimeSpan.Zero; } // Convert elapsed time to local coordinates, not necessarily same as [....] clock coordinates TimeSpan syncElapsedTimeLT = (_syncData.SyncClock == this) ? syncElapsedTimeST : DivideTimeSpan(syncElapsedTimeST, _syncData.SyncClockSpeedRatio); // This is the actual slip formula: how much is the slipping clock lagging behind? TimeSpan parentTimeSlipPT = parentElapsedTimePT - DivideTimeSpan(syncElapsedTimeLT, _appliedSpeedRatio); // NOTE: The above line does the same as this: // parentTimeSlip = syncSlip / _appliedSpeedRatio // ...but it maintains greater accuracy and prevents a bug where parentTimeSlip ends up 1 tick greater // that it should be, thus becoming larger than parentElapsedTimePT and causing us to suddenly fall out // of our [....] period. // Unless the media is exactly perfect, we will have non-zero slip time; we assume that it isn't // perfect, and always adjust our time accordingly. if (_currentIterationBeginTime.HasValue) { _currentIterationBeginTime += parentTimeSlipPT; } else { _beginTime += parentTimeSlipPT; } // UpdateSyncBeginTime(); parentIntervalCollection = parentIntervalCollection.SlipBeginningOfConnectedInterval(parentTimeSlipPT); return; } private void ResetSlipOnSubtree() { // Reset all children's slip time here; NOTE that this will change our effective duration, // but this should not become important until the next tick, when it will be recomputed anyway. PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, false); // No iteration at this node while (subtree.MoveNext()) { Clock current = subtree.Current; Debug.Assert(!current.IsRoot, "Root nodes never should reset their Slip amounts with ResetSlipOnSubtree(), even when seeking."); if (current._syncData != null) { current._syncData.IsInSyncPeriod = false; current._syncData.SyncClockDiscontinuousEvent = true; } if (current.CanSlip) { current._beginTime = current._timeline.BeginTime; // _beginTime could have slipped with media nodes current._currentIteration = null; current.UpdateSyncBeginTime(); current.HasDiscontinuousTimeMovementOccured = true; } else if (current.CanGrow) // If it's a repeating container with slippable descendants... { current._currentIterationBeginTime = current._beginTime; // ...reset its current iteration as well current._currentDuration = current._resolvedDuration; // Revert currentDuration back to default size } else // Otherwise we know not to traverse any further { subtree.SkipSubtree(); } } } #endregion // Local State Computation Helpers #region Event Helpers ////// Adds a delegate to the list of event handlers on this object. /// /// /// A unique identifier for the event handler. Since Clock events /// mirror Timeline events the callers of this method will pass in /// keys from Timeline /// /// The delegate to add private void AddEventHandler(EventPrivateKey key, Delegate handler) { Debug.Assert(!IsTimeManager); if (_eventHandlersStore == null) { _eventHandlersStore = new EventHandlersStore(); } _eventHandlersStore.Add(key, handler); VerifyNeedsTicksWhenActive(); } ////// Immediately fire the Loaded Event /// private void FireCompletedEvent() { FireEvent(Timeline.CompletedKey); } ////// Immediately fire the GlobalSpeedInvalidated Event /// private void FireCurrentGlobalSpeedInvalidatedEvent() { FireEvent(Timeline.CurrentGlobalSpeedInvalidatedKey); } ////// Immediately fire the State Invalidated Event /// private void FireCurrentStateInvalidatedEvent() { FireEvent(Timeline.CurrentStateInvalidatedKey); } ////// Immediately fire the CurrentTimeInvalidated Event /// private void FireCurrentTimeInvalidatedEvent() { FireEvent(Timeline.CurrentTimeInvalidatedKey); } ////// Fires the given event /// /// The unique key representing the event to fire private void FireEvent(EventPrivateKey key) { if (_eventHandlersStore != null) { EventHandler handler = (EventHandler)_eventHandlersStore.Get(key); if (handler != null) { handler(this, null); } } } ////// Immediately fire the RemoveRequested Event /// private void FireRemoveRequestedEvent() { FireEvent(Timeline.RemoveRequestedKey); } // Find the last closest time that falls on the boundary of the Desired Frame Rate private TimeSpan GetCurrentDesiredFrameTime(TimeSpan time) { return GetDesiredFrameTime(time, +0); } // Find the closest time that falls on the boundary of the Desired Frame Rate, advancing [frameOffset] frames forward private TimeSpan GetDesiredFrameTime(TimeSpan time, int frameOffset) { Debug.Assert(_rootData.DesiredFrameRate > 0); Int64 desiredFrameRate = _rootData.DesiredFrameRate; Int64 desiredFrameNumber = (time.Ticks * desiredFrameRate) / s_TimeSpanTicksPerSecond + frameOffset; Int64 desiredFrameTick = (desiredFrameNumber * s_TimeSpanTicksPerSecond) / desiredFrameRate; return TimeSpan.FromTicks(desiredFrameTick); } // Find the next closest time that falls on the boundary of the Desired Frame Rate private TimeSpan GetNextDesiredFrameTime(TimeSpan time) { return GetDesiredFrameTime(time, +1); } ////// Removes a delegate from the list of event handlers on this object. /// /// /// A unique identifier for the event handler. Since Clock events /// mirror Timeline events the callers of this method will pass in /// keys from Timeline /// /// The delegate to remove private void RemoveEventHandler(EventPrivateKey key, Delegate handler) { Debug.Assert(!IsTimeManager); if (_eventHandlersStore != null) { _eventHandlersStore.Remove(key, handler); if (_eventHandlersStore.Count == 0) { _eventHandlersStore = null; } } UpdateNeedsTicksWhenActive(); } #endregion // Event Helpers ////// Adds this clock to the time manager. /// private void AddToTimeManager() { Debug.Assert(!IsTimeManager); Debug.Assert(_parent == null); Debug.Assert(_timeManager == null); TimeManager timeManager = MediaContext.From(Dispatcher).TimeManager; if (timeManager == null) { // The time manager has not been created or has been released // This occurs when we are shutting down. Simply return. return; } _parent = timeManager.TimeManagerClock; SetTimeManager(_parent._timeManager); Int32? desiredFrameRate = Timeline.GetDesiredFrameRate(_timeline); if (desiredFrameRate.HasValue) { HasDesiredFrameRate = true; _rootData.DesiredFrameRate = desiredFrameRate.Value; } // Store this clock in the root clock's child collection _parent.InternalRootChildren.Add(WeakReference); // Create a finalizer object that will clean up after we are gone _subtreeFinalizer = new SubtreeFinalizer(_timeManager); // Fix up depth values PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { Clock current = subtree.Current; current._depth = current._parent._depth + 1; } // Perform any necessary updates if (IsInTimingTree) { // Mark the tree as dirty _timeManager.SetDirty(); } TimeIntervalCollection currentIntervals = TimeIntervalCollection.CreatePoint(_timeManager.InternalCurrentGlobalTime); currentIntervals.AddNullPoint(); _timeManager.InternalCurrentIntervals = currentIntervals; // // Recompute the local state at this subtree // // Since _beginTime for a root clock takes the current time into account, // it needs to be set during a tick. The call to ComputeLocalState // here isn't on a tick boundary, so we don't want to begin the clock yet. _beginTime = null; _currentIterationBeginTime = null; subtree.Reset(); while (subtree.MoveNext()) { subtree.Current.ComputeLocalState(); // Compute the state of the node subtree.Current.ClipNextTickByParent(); // Perform NextTick clipping, stage 1 // Make a note to visit for stage 2, only for ClockGroups subtree.Current.NeedsPostfixTraversal = (subtree.Current is ClockGroup); } _parent.ComputeTreeStateRoot(); // Re-clip the next tick estimates by children // Adding is an implicit begin, so do that here. This is done after // ComputeLocalState so that the begin will be picked up on the // next tick. Note that if _timeline.BeginTime == null we won't // start the clock. if (_timeline.BeginTime != null) { RootBeginPending = true; } NotifyNewEarliestFutureActivity(); // Make sure we get ticks } ////// Helper for more elegant code dividing a TimeSpan by a double /// private TimeSpan DivideTimeSpan(TimeSpan timeSpan, double factor) { Debug.Assert(factor != 0); // Divide by zero return TimeSpan.FromTicks((long)(((double)timeSpan.Ticks) / factor + 0.5)); } ////// Gets the value of a flag specified by the ClockFlags enum /// private bool GetFlag(ClockFlags flagMask) { return (_flags & flagMask) == flagMask; } ////// Helper for more elegant code multiplying a TimeSpan by a double /// private static TimeSpan MultiplyTimeSpan(TimeSpan timeSpan, double factor) { return TimeSpan.FromTicks((long)(factor * (double)timeSpan.Ticks + 0.5)); } private void NotifyNewEarliestFutureActivity() { PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); // First reset the children while (subtree.MoveNext()) { subtree.Current.InternalNextTickNeededTime = TimeSpan.Zero; } Clock current = _parent; // Propagate the fact that we will need an update sooner up the chain while (current != null && current.InternalNextTickNeededTime != TimeSpan.Zero) { current.InternalNextTickNeededTime = TimeSpan.Zero; if (current.IsTimeManager) // We went all the way up to the root node, notify TimeManager { _timeManager.NotifyNewEarliestFutureActivity(); break; } current = current._parent; } if (_timeManager != null) { // If we get here from within a Tick, this will force MediaContext to perform another subsequent Tick // on the TimeManager. This will apply the requested interactive operations, so their results will // immediately become visible. _timeManager.SetDirty(); } } // State that must remain *constant* outside of the active period private void ResetCachedStateToFilling() { _currentGlobalSpeed = 0; IsBackwardsProgressingGlobal = false; _currentClockState = ClockState.Filling; } ////// Calls RaiseCompleted on this subtree when called on a root. /// /// Whether we are in a tick. private void RaiseCompletedForRoot(bool isInTick) { // Only perform this operation from root nodes. Also, to avoid constantly calling Completed after passing // expirationTime, check that the state is invalidated, so we only raise the event as we are finishing. if (IsRoot && (CurrentStateInvalidatedEventRaised || !isInTick)) { // If we are a root node, notify the entire subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.RaiseCompleted(); } } } private void RaiseRemoveRequestedForRoot() { Debug.Assert(IsRoot); // This should only be called on root-child clocks // If we are a root node, notify the entire subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.RaiseRemoveRequested(); } } ////// Sets a specified flag with the given value /// private void SetFlag(ClockFlags flagMask, bool value) { if (value) { _flags |= flagMask; } else { _flags &= ~(flagMask); } } ////// Sets the time manager for the subtree rooted at this timeline. /// /// /// The new time manager. /// private void SetTimeManager(TimeManager timeManager) { Debug.Assert(!IsTimeManager); // Optimize the no-op case away if (this._timeManager != timeManager) { // Set the new time manager for the whole subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current._timeManager = timeManager; } if (timeManager != null) { // If we are joining a new time manager, issue any deferred calls subtree.Reset(); while (subtree.MoveNext()) { Clock current = subtree.Current; // } } } } private void UpdateNeedsTicksWhenActive() { // Currently we'll set NeedsTicksWhenActive to true // if any of the three events are set on this clock. // if (_eventHandlersStore == null) { NeedsTicksWhenActive = false; } else { NeedsTicksWhenActive = true; } } // This wrapper is invoked anytime we invalidate the _beginTime private void UpdateSyncBeginTime() { if (_syncData != null) { _syncData.UpdateClockBeginTime(); } } private void VerifyNeedsTicksWhenActive() { if (!NeedsTicksWhenActive) // We may need to update the tree to know that we need ticks { NeedsTicksWhenActive = true; // Use this as a hint for NeedsTicksWhenActive NotifyNewEarliestFutureActivity(); } } #endregion // Private Methods // // Private Properties // #region Private Properties ////// True if we are in a running timing tree, false otherwise. /// private bool IsInTimingTree { get { Debug.Assert(!IsTimeManager); return (_timeManager != null) && (_timeManager.State != TimeState.Stopped); } } ////// This isn't an Interactive method: InternalBegin does /// not make use of this flag. It is set in AddToRoot() to /// notify a root clock that it must begin at the next tick. /// Until this is set_beginTime for a root clock will be null; /// AdjustBeginTime() sets _beginTime properly. /// private bool RootBeginPending { get { return GetFlag(ClockFlags.RootBeginPending); } set { SetFlag(ClockFlags.RootBeginPending, value); } } #endregion // Private Properties // // Nested Types // #region Nested Types [Flags] private enum ClockFlags : uint { IsTimeManager = 1 << 0, IsRoot = 1 << 1, IsBackwardsProgressingGlobal = 1 << 2, IsInteractivelyPaused = 1 << 3, IsInteractivelyStopped = 1 << 4, PendingInteractivePause = 1 << 5, PendingInteractiveResume = 1 << 6, PendingInteractiveStop = 1 << 7, PendingInteractiveRemove = 1 << 8, CanGrow = 1 << 9, CanSlip = 1 << 10, CurrentStateInvalidatedEventRaised = 1 << 11, CurrentTimeInvalidatedEventRaised = 1 << 12, CurrentGlobalSpeedInvalidatedEventRaised = 1 << 13, CompletedEventRaised = 1 << 14, RemoveRequestedEventRaised = 1 << 15, IsInEventQueue = 1 << 16, NeedsTicksWhenActive = 1 << 17, NeedsPostfixTraversal = 1 << 18, PauseStateChangedDuringTick = 1 << 19, RootBeginPending = 1 << 20, HasControllableRoot = 1 << 21, HasResolvedDuration = 1 << 22, HasDesiredFrameRate = 1 << 23, HasDiscontinuousTimeMovementOccured = 1 << 24, HasDescendantsWithUnresolvedDuration = 1 << 25, HasSeekOccuredAfterLastTick = 1 << 26, } ////// The result of a ResolveTimes method call. /// private enum ResolveCode { ////// Nothing changed in resolving new times. /// NoChanges, ////// Resolved times are different than before. /// NewTimes, ////// The children of this timeline need a full reset time resolution. This flag /// indicates that a partial resolution needs to be prunned at the current /// timeline in favor of a full resolution for its children. /// NeedNewChildResolve } ////// Implements a finalizer for a clock subtree. /// private class SubtreeFinalizer { ////// Creates a finalizer for a clock subtree. /// internal SubtreeFinalizer(TimeManager timeManager) { _timeManager = timeManager; } ////// Finalizes a clock subtree. /// ~SubtreeFinalizer() { #pragma warning disable 1634, 1691 #pragma warning suppress 6525 _timeManager.ScheduleClockCleanup(); } private TimeManager _timeManager; } ////// Holds [....]-related data when it is applicable /// internal class SyncData { internal SyncData(Clock syncClock) { Debug.Assert(syncClock != null); Debug.Assert(syncClock.GetCanSlip()); Debug.Assert(syncClock.IsRoot || syncClock._timeline.BeginTime.HasValue); // Only roots may later validate their _beginTime _syncClock = syncClock; UpdateClockBeginTime(); // This will update the remaining dependencies } // This method should be invoked anytime we invalidate the SyncClock's _beginTime only internal void UpdateClockBeginTime() { // _syncClockBeginTime = _syncClock._beginTime; _syncClockSpeedRatio = _syncClock._appliedSpeedRatio; _syncClockResolvedDuration = SyncClockResolvedDuration; // This is to detect media finding its true duration } internal Clock SyncClock { get { return _syncClock; } } internal Duration SyncClockResolvedDuration { get { // Duration can only change its value while it is Automatic if (!_syncClockResolvedDuration.HasTimeSpan) { _syncClockEffectiveDuration = _syncClock.ComputeEffectiveDuration(); // null == infinity _syncClockResolvedDuration = _syncClock._resolvedDuration; } return _syncClockResolvedDuration; } } internal bool SyncClockHasReachedEffectiveDuration { get { if (_syncClockEffectiveDuration.HasValue) // If the [....] clock has a finite duration { return (_previousRepeatTime + _syncClock.GetCurrentTimeCore() >= _syncClockEffectiveDuration.Value); } else { return false; } } } // NOTE: This value is in SyncNode coordinates, not its parent's coordinates internal TimeSpan? SyncClockEffectiveDuration { get { return _syncClockEffectiveDuration; } } internal double SyncClockSpeedRatio { get { return _syncClockSpeedRatio; } } internal bool IsInSyncPeriod { get { return _isInSyncPeriod; } set { _isInSyncPeriod = value; } } internal bool SyncClockDiscontinuousEvent { get { return _syncClockDiscontinuousEvent; } set { _syncClockDiscontinuousEvent = value; } } internal TimeSpan PreviousSyncClockTime { get { return _previousSyncClockTime; } set { _previousSyncClockTime = value; } } internal TimeSpan PreviousRepeatTime { get { return _previousRepeatTime; } set { _previousRepeatTime = value; } } internal TimeSpan SyncClockBeginTime { get { Debug.Assert(_syncClockBeginTime.HasValue); // This should never be queried on a root without beginTime return _syncClockBeginTime.Value; } } private Clock _syncClock; private double _syncClockSpeedRatio; private bool _isInSyncPeriod, _syncClockDiscontinuousEvent; private Duration _syncClockResolvedDuration = Duration.Automatic; // Duration -- *local* coordinates private TimeSpan? _syncClockEffectiveDuration; // This reflects RepeatBehavior (local coordinates) private TimeSpan? _syncClockBeginTime; private TimeSpan _previousSyncClockTime; private TimeSpan _previousRepeatTime; // How many complete iterations we already have done } ////// Holds data specific to root clocks. /// internal class RootData { internal RootData() { } internal TimeSpan CurrentAdjustedGlobalTime { get { return _currentAdjustedGlobalTime; } set { _currentAdjustedGlobalTime = value; } } internal Int32 DesiredFrameRate { get { return _desiredFrameRate; } set { _desiredFrameRate = value; } } internal Double InteractiveSpeedRatio { get { return _interactiveSpeedRatio; } set { _interactiveSpeedRatio = value; } } internal TimeSpan LastAdjustedGlobalTime { get { return _lastAdjustedGlobalTime; } set { _lastAdjustedGlobalTime = value; } } ////// The time to which we want to seek this timeline at the next tick /// internal TimeSpan? PendingSeekDestination { get { return _pendingSeekDestination; } set { _pendingSeekDestination = value; } } internal Double? PendingSpeedRatio { get { return _pendingSpeedRatio; } set { _pendingSpeedRatio = value; } } private Int32 _desiredFrameRate; private double _interactiveSpeedRatio = 1.0; private double? _pendingSpeedRatio; private TimeSpan _currentAdjustedGlobalTime; private TimeSpan _lastAdjustedGlobalTime; private TimeSpan? _pendingSeekDestination; } #endregion // Nested Types // // Debugging Instrumentation // #region Debugging instrumentation ////// Debug-only method to verify that the recomputed input time /// is close to the original. See ComputeCurrentIteration /// /// original input time /// input time without rounding errors [Conditional("DEBUG")] private void Debug_VerifyOffsetFromBegin(long inputTime, long optimizedInputTime) { long error = (long)Math.Max(_appliedSpeedRatio, 1.0); // Assert the computed inputTime is very close to the original. // _appliedSpeedRatio is the upper bound of the error (in Ticks) caused by the // calculation of inputTime. The reason is that we truncate (floor) during the // computation of EffectiveDuration, losing up to 0.99999.... off the number. // The computation of inputTime multiplies the truncated value by _appliedSpeedRatio, // so _appliedSpeedRatio is guaranteed to be close to but higher than the actual error. Debug.Assert(Math.Abs(optimizedInputTime - inputTime) <= error, "This optimized inputTime does not match the original - did the calculation of inputTime change?"); } #if DEBUG ////// Dumps the description of the subtree rooted at this clock. /// internal void Dump() { System.Text.StringBuilder builder = new System.Text.StringBuilder(); builder.Capacity = 1024; builder.Append("======================================================================\n"); builder.Append("Clocks rooted at Clock "); builder.Append(_debugIdentity); builder.Append('\n'); builder.Append("----------------------------------------------------------------------\n"); builder.Append("Flags LastBegin LastEnd NextBegin NextEnd Name\n"); builder.Append("----------------------------------------------------------------------\n"); if (IsTimeManager) { RootBuildInfoRecursive(builder); } else { BuildInfoRecursive(builder, 0); } builder.Append("----------------------------------------------------------------------\n"); Trace.Write(builder.ToString()); } ////// Dumps the description of all clocks in the known clock table. /// internal static void DumpAll() { System.Text.StringBuilder builder = new System.Text.StringBuilder(); int clockCount = 0; builder.Capacity = 1024; builder.Append("======================================================================\n"); builder.Append("Clocks in the GC heap\n"); builder.Append("----------------------------------------------------------------------\n"); builder.Append("Flags LastBegin LastEnd NextBegin NextEnd Name\n"); builder.Append("----------------------------------------------------------------------\n"); lock (_debugLockObject) { if (_objectTable.Count > 0) { // Output the clocks sorted by ID int[] idTable = new int[_objectTable.Count]; _objectTable.Keys.CopyTo(idTable, 0); Array.Sort(idTable); for (int index = 0; index < idTable.Length; index++) { WeakReference weakRef = (WeakReference)_objectTable[idTable[index]]; Clock clock = (Clock)weakRef.Target; if (clock != null) { clock.BuildInfo(builder, 0, true); clockCount++; } } } } if (clockCount == 0) { builder.Append("There are no Clocks in the GC heap.\n"); } builder.Append("----------------------------------------------------------------------\n"); Trace.Write(builder.ToString()); } ////// Dumps the description of the subtree rooted at this clock. /// /// /// A StringBuilder that accumulates the description text. /// /// /// The depth of recursion for this clock. /// // Normally a virtual method would be implemented for the dervied class // to handle the children property, but the asmmeta would be different // since this is only availabe in a debug build. For this reason we're // handling the children in the base class. internal void BuildInfoRecursive(System.Text.StringBuilder builder, int depth) { // Add the info for this clock BuildInfo(builder, depth, false); // Recurse into the children ClockGroup thisGroup = this as ClockGroup; if (thisGroup != null) { depth++; Listchildren = thisGroup.InternalChildren; if (children != null) { for (int childIndex = 0; childIndex < children.Count; childIndex++) { children[childIndex].BuildInfoRecursive(builder, depth); } } } } /// /// Dumps the description of the subtree rooted at this root clock. /// /// /// A StringBuilder that accumulates the description text. /// internal void RootBuildInfoRecursive(System.Text.StringBuilder builder) { // Add the info for this clock BuildInfo(builder, 0, false); // Recurse into the children. Don't use the enumerator because // that would remove dead references, which would be an undesirable // side-effect for a debug output method. Listchildren = ((ClockGroup) this).InternalRootChildren; for (int index = 0; index < children.Count; index++) { Clock child = (Clock)children[index].Target; if (child != null) { child.BuildInfoRecursive(builder, 1); } } } /// /// Dumps the description of this clock. /// /// /// A StringBuilder that accumulates the description text. /// /// /// The depth of recursion for this clock. /// /// /// True to dump the ID of the parent clock, false otherwise. /// internal void BuildInfo(System.Text.StringBuilder builder, int depth, bool includeParentID) { builder.Append(depth); builder.Append(GetType().Name); builder.Append(' '); builder.Append(_debugIdentity); builder.Append(' '); _timeline.BuildInfo(builder, 0, false); } ////// Finds a previously registered object. /// /// /// The ID of the object to look for /// ////// The object if found, null otherwise. /// internal static Clock Find(int id) { Clock clock = null; lock (_debugLockObject) { object handleReference = _objectTable[id]; if (handleReference != null) { WeakReference weakRef = (WeakReference)handleReference; clock = (Clock)weakRef.Target; if (clock == null) { // Object has been destroyed, so remove the weakRef. _objectTable.Remove(id); } } } return clock; } ////// Cleans up the known timeline clocks table by removing dead weak /// references. /// internal static void CleanKnownClocksTable() { lock (_debugLockObject) { Hashtable removeTable = new Hashtable(); // Identify dead references foreach (DictionaryEntry e in _objectTable) { WeakReference weakRef = (WeakReference) e.Value; if (weakRef.Target == null) { removeTable[e.Key] = weakRef; } } // Remove dead references foreach (DictionaryEntry e in removeTable) { _objectTable.Remove(e.Key); } } } #endif // DEBUG #endregion // Debugging instrumentation // // Data // #region Data private ClockFlags _flags; private int? _currentIteration; // Precalculated current iteration private double? _currentProgress; // Precalculated current progress value private double? _currentGlobalSpeed; // Precalculated current global speed value private TimeSpan? _currentTime; // Precalculated current global time private ClockState _currentClockState; // Precalculated current clock state private RootData _rootData = null; // Keeps track of root-related data for DesiredFrameRate internal SyncData _syncData = null; // Keeps track of [....]-related data for SlipBehavior // Stores the clock's begin time as an offset from the clock's // parent's begin time (i.e. a begin time of 2 sec means begin // 2 seconds afer the parent starts). For non-root clocks this // is always the same as its timeline's begin time. // For root clocks, the begin time is adjusted in response to seeks, // pauses, etc (see AdjustBeginTime()) // // This must be null when the clock is stopped. internal TimeSpan? _beginTime; // This is only used for repeating timelines which have CanSlip children/descendants; // In this case, we use this variable instead of _beginTime to compute the current state // of the clock (in conjunction with _currentIteration, so we know how many we have // already completed.) This makes us agnostic to slip/growth in our past iterations. private TimeSpan? _currentIterationBeginTime; // How soon this Clock needs another tick internal TimeSpan? _nextTickNeededTime = null; private WeakReference _weakReference; private SubtreeFinalizer _subtreeFinalizer; private EventHandlersStore _eventHandlersStore; ////// Cache Duration for perf reasons and also to accommodate one time /// only resolution of natural duration. /// If Timeline.Duration is Duration.Automatic, we will at first treat /// this as Duration.Forever, but will periodically ask what the resolved /// duration is. Once we receive an answer, that will be the duration /// used from then on, and we won't ask again. /// Otherwise, this will be set to Timeline.Duration when the Clock is /// created and won't change. /// internal Duration _resolvedDuration; ////// This is a cached estimated duration of the current iteration of this clock. /// For clocks which return false to CanGrow, this should always equal the /// _resolvedDuration. However, for clocks with CanGrow, this may change from /// tick to tick. Based on how far we are in the present iteration, and how /// on track our slipping children are, we make estimates of when we will finish /// the iteration, which are reflected by our estimated _currentDuration. /// internal Duration _currentDuration; ////// For a root, this is the Timeline's speed ratio multiplied by the /// interactive speed ratio. It's updated whenever the interactive speed /// ratio is updated. If the interactive speed ratio is 0, we'll use the /// Timeline's speed ratio, that is, treat interactive speed ratio as 1. /// This is why this is "applied" speed ratio. /// /// For a non-root, this is just a cache for the Timeline's speed ratio. /// private Double _appliedSpeedRatio; #region Linking data internal Timeline _timeline; internal TimeManager _timeManager; internal ClockGroup _parent; // Parents can only be ClockGroups since // they're the only ones having children internal int _childIndex; internal int _depth; static Int64 s_TimeSpanTicksPerSecond = TimeSpan.FromSeconds(1).Ticks; #endregion // Linking data #region Debug data #if DEBUG internal int _debugIdentity; internal static int _nextIdentity; internal static Hashtable _objectTable = new Hashtable(); internal static object _debugLockObject = new object(); #endif // DEBUG #endregion // Debug data #endregion // Data } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. //------------------------------------------------------------------------------ // Microsoft Windows Client Platform // Copyright (c) Microsoft Corporation, 2003 // // File: Clock.cs //----------------------------------------------------------------------------- #if DEBUG #define TRACE #endif // DEBUG using MS.Internal; using MS.Utility; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Media.Composition; using System.Windows.Markup; using System.Windows.Threading; using SR=MS.Internal.PresentationCore.SR; using SRID=MS.Internal.PresentationCore.SRID; namespace System.Windows.Media.Animation { ////// Maintains run-time timing state for timed objects. /// ////// A Clock object maintains the run-time state for a timed object /// according to the description specified in a Timeline object. It also /// provides methods for timing control, such as event scheduling and /// VCR-like functionality. Clock objects are arranged in trees /// that match the structure of the Timeline objects they are created from. /// public class Clock : DispatcherObject { // // Constructors // #region Constructors ////// Creates a Clock object. /// /// /// The Timeline to use as a template. /// ////// The returned Clock doesn't have any children. /// protected internal Clock(Timeline timeline) { #if DEBUG lock (_debugLockObject) { _debugIdentity = ++_nextIdentity; WeakReference weakRef = new WeakReference(this); _objectTable[_debugIdentity] = weakRef; } #endif // DEBUG Debug.Assert(timeline != null); // // Store a frozen copy of the timeline // _timeline = (Timeline)timeline.GetCurrentValueAsFrozen(); // GetCurrentValueAsFrozen will make a clone of the Timeline if it's // not frozen and will return the Timeline if it is frozen. // The clone will never have event handlers, while the // frozen original may. This means we need to copy // the event handlers from the original timeline onto the clock // to be consistent. // // Copy the event handlers from the original timeline into the clock // _eventHandlersStore = timeline.InternalEventHandlersStore; // // FXCop fix. Do not call overridables in constructors // UpdateNeedsTicksWhenActive(); // Set the NeedsTicksWhenActive only if we have someone listening // to an event. SetFlag(ClockFlags.NeedsTicksWhenActive, _eventHandlersStore != null); // // Cache values that won't change as the clock ticks // // Non-root clocks have an unchanging begin time specified by their timelines. // A root clock will update _beginTime as necessary. _beginTime = _timeline.BeginTime; // Cache duration, getting Timeline.Duration and recalculating duration // each Tick was eating perf, resolve the duration if possible. _resolvedDuration = _timeline.Duration; if (_resolvedDuration == Duration.Automatic) { // Forever is the default for an automatic duration. We can't // try to resolve the duration yet because the tree // may not be fully built, in which case ClockGroups won't // have their children yet. _resolvedDuration = Duration.Forever; } else { HasResolvedDuration = true; } _currentDuration = _resolvedDuration; // Cache speed ratio, for roots this value may be updated if the interactive // speed ratio changes, but for non-roots this value will remain constant // throughout the lifetime of the clock. _appliedSpeedRatio = _timeline.SpeedRatio; // // Initialize current state // _currentClockState = ClockState.Stopped; if (_beginTime.HasValue) { // We need a tick to bring our state up to date _nextTickNeededTime = TimeSpan.Zero; } // All other data members initialized to zero by default } #endregion // Constructors // // Public Properties // #region Public Properties internal bool CanGrow { get { return GetFlag(ClockFlags.CanGrow); } } internal bool CanSlip { get { return GetFlag(ClockFlags.CanSlip); } } ////// Returns an ClockController which can be used to perform interactive /// operations on this Clock. If interactive operations are not allowed, /// this property returns null; this is the case for Clocks that /// aren't children of the root Clock. /// public ClockController Controller { get { Debug.Assert(!IsTimeManager); // Unless our parent is the root clock and we're controllable, // return null if (IsRoot && HasControllableRoot) { return new ClockController(this); } else { return null; } } } ////// The current repeat iteration. The first period has a value of one. /// ////// If the clock is not active, the value of this property is only valid if /// the fill attribute specifies that the timing attributes should be /// extended. Otherwise, the property returns -1. /// public Int32? CurrentIteration { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _currentIteration; } } ////// Gets the current rate at which time is progressing in the clock, /// compared to the real-world wall clock. If the clock is stopped, /// this method returns null. /// ///public double? CurrentGlobalSpeed { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _currentGlobalSpeed; } } /// /// The current progress of time for this clock. /// ////// If the clock is active, the progress is always a value between 0 and 1, /// inclusive. Otherwise, the progress depends on the value of the /// public double? CurrentProgress { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _currentProgress; } } ///attribute. /// If the clock is inactive and the fill attribute is not in effect, this /// property returns null. /// /// Gets a value indicating whether the Clock’s current time is inside the Active period /// (meaning properties may change frame to frame), inside the Fill period, or Stopped. /// ////// You can tell whether you’re in FillBegin or FillEnd by the value of CurrentProgress /// (0 for FillBegin, 1 for FillEnd). /// public ClockState CurrentState { get { // VerifyAccess(); return _currentClockState; } } ////// The current position of the clock, relative to the starting time. Setting /// this property to a new value has the effect of seeking the clock to a /// new point in time. Both forward and backward seeks are allowed. Setting this /// property has no effect if the clock is not active. However, seeking while the /// clock is paused works as expected. /// public TimeSpan? CurrentTime { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _currentTime; } } ////// /// public bool HasControllableRoot { get { Debug.Assert(!IsTimeManager); return GetFlag(ClockFlags.HasControllableRoot); } } ////// True if the timeline is currently paused, false otherwise. /// ////// This property returns true either if this timeline has been paused, or if an /// ancestor of this timeline has been paused. /// public bool IsPaused { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return IsInteractivelyPaused; } } ////// Returns the natural duration of this Clock, which is defined /// by the Timeline from which it is created. /// ///public Duration NaturalDuration { get { return _timeline.GetNaturalDuration(this); } } /// /// The Clock that sets the parent time for this Clock. /// public Clock Parent { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); // If our parent is the root clock, force a return value of null if (IsRoot) { return null; } else { return _parent; } } } ////// Gets the Timeline object that holds the description controlling the /// behavior of this clock. /// ////// The Timeline object that holds the description controlling the /// behavior of this clock. /// public Timeline Timeline { get { // VerifyAccess(); Debug.Assert(!IsTimeManager); return _timeline; } } #endregion // Public Properties // // Public Events // #region Public Events ////// Raised by the timeline when it has completed. /// public event EventHandler Completed { add { AddEventHandler(Timeline.CompletedKey, value); } remove { RemoveEventHandler(Timeline.CompletedKey, value); } } ////// Raised by the Clock whenever its current speed changes. /// This event mirrors the CurrentGlobalSpeedInvalidated event on Timeline /// public event EventHandler CurrentGlobalSpeedInvalidated { add { // VerifyAccess(); AddEventHandler(Timeline.CurrentGlobalSpeedInvalidatedKey, value); } remove { // VerifyAccess(); RemoveEventHandler(Timeline.CurrentGlobalSpeedInvalidatedKey, value); } } ////// Raised by the Clock whenever its current state changes. /// This event mirrors the CurrentStateInvalidated event on Timeline /// public event EventHandler CurrentStateInvalidated { add { // VerifyAccess(); AddEventHandler(Timeline.CurrentStateInvalidatedKey, value); } remove { // VerifyAccess(); RemoveEventHandler(Timeline.CurrentStateInvalidatedKey, value); } } ////// Raised by the Clock whenever its current time changes. /// This event mirrors the CurrentTimeInvalidated event on Timeline /// public event EventHandler CurrentTimeInvalidated { add { // VerifyAccess(); AddEventHandler(Timeline.CurrentTimeInvalidatedKey, value); } remove { // VerifyAccess(); RemoveEventHandler(Timeline.CurrentTimeInvalidatedKey, value); } } ////// Raised by the timeline when its removal has been requested by the user. /// public event EventHandler RemoveRequested { add { AddEventHandler(Timeline.RemoveRequestedKey, value); } remove { RemoveEventHandler(Timeline.RemoveRequestedKey, value); } } #endregion // Public Events // // Protected Methods // #region Protected Methods ////// Notify a clock that we've moved in a discontinuous way /// protected virtual void DiscontinuousTimeMovement() { // Base class does nothing } ////// Returns true if the Clock has its own external source for time, which may /// require synchronization with the timing system. Media is one example of this. /// protected virtual bool GetCanSlip() { return false; } ////// /// protected virtual TimeSpan GetCurrentTimeCore() { Debug.Assert(!IsTimeManager); return _currentTime.HasValue ? _currentTime.Value : TimeSpan.Zero; } ////// Notify a clock that we've changed the speed /// protected virtual void SpeedChanged() { // Base class does nothing } ////// Notify a clock that we've stopped /// protected virtual void Stopped() { // Base class does nothing } #endregion // Protected Methods // // Protected Properties // #region Protected Properties ////// The current global time in TimeSpan units, established by the time manager. /// protected TimeSpan CurrentGlobalTime { get { if (_timeManager == null) { return TimeSpan.Zero; } else if (IsTimeManager) { return _timeManager.InternalCurrentGlobalTime; } else { Clock current = this; while (!current.IsRoot) // Traverse up the tree to the root node { current = current._parent; } if (current.HasDesiredFrameRate) { return current._rootData.CurrentAdjustedGlobalTime; } else { return _timeManager.InternalCurrentGlobalTime; } } } } #endregion // Protected Properties // // Internal Methods // #region Internal Methods internal virtual void AddNullPointToCurrentIntervals() { } internal static Clock AllocateClock( Timeline timeline, bool hasControllableRoot) { Clock clock = timeline.AllocateClock(); // Assert that we weren't given an existing clock Debug.Assert(!clock.IsTimeManager); ClockGroup clockGroup = clock as ClockGroup; if ( clock._parent != null || ( clockGroup != null && clockGroup.InternalChildren != null )) { // The derived class is trying to fool us -- we require a new, // fresh, unassociated clock here throw new InvalidOperationException( SR.Get( SRID.Timing_CreateClockMustReturnNewClock, timeline.GetType().Name)); } clock.SetFlag(ClockFlags.HasControllableRoot, hasControllableRoot); return clock; } internal virtual void BuildClockSubTreeFromTimeline( Timeline timeline, bool hasControllableRoot) { SetFlag(ClockFlags.CanSlip, GetCanSlip()); // Set the CanSlip flag // Here we preview the clock's own slip-ability, hence ClockGroups should return false // at this stage, because their children are not yet added by the time of this call. if (CanSlip && (IsRoot || _timeline.BeginTime.HasValue)) { ResolveDuration(); // A [....] clock with duration of zero or no begin time has no effect, so do skip it if (!_resolvedDuration.HasTimeSpan || _resolvedDuration.TimeSpan > TimeSpan.Zero) { // Verify that we only use SlipBehavior in supported scenarios if ((_timeline.AutoReverse == true) || (_timeline.AccelerationRatio > 0) || (_timeline.DecelerationRatio > 0)) { throw new NotSupportedException(SR.Get(SRID.Timing_CanSlipOnlyOnSimpleTimelines)); } _syncData = new SyncData(this); // CanSlip clocks keep themselves synced HasDescendantsWithUnresolvedDuration = !HasResolvedDuration; // Keep track of when our duration is resolved Clock current = _parent; // Traverse up the parent chain and verify that no unsupported behavior is specified while (current != null) { Debug.Assert(!current.IsTimeManager); // We should not yet be connected to the TimeManager if (current._timeline.AutoReverse || current._timeline.AccelerationRatio > 0 || current._timeline.DecelerationRatio > 0) { throw new System.InvalidOperationException(SR.Get(SRID.Timing_SlipBehavior_SyncOnlyWithSimpleParents)); } current.SetFlag(ClockFlags.CanGrow, true); // Propagate the slippage tracking up the tree if (!HasResolvedDuration) // Let the parents know that we have not yet unresolved duration { current.HasDescendantsWithUnresolvedDuration = true; } current._currentIterationBeginTime = current._beginTime; current = current._parent; } } } } internal static Clock BuildClockTreeFromTimeline( Timeline rootTimeline, bool hasControllableRoot) { Clock rootClock = AllocateClock(rootTimeline, hasControllableRoot); // Set this flag so that the subsequent method can rely on it. rootClock.IsRoot = true; rootClock._rootData = new RootData(); // Create a RootData to hold root specific information. // The root clock was given a reference to a frozen copy of the // timing tree. We pass this copy down BuildClockSubTreeFromTimeline // so that each child clock will use that tree rather // than create a new one. rootClock.BuildClockSubTreeFromTimeline(rootClock.Timeline, hasControllableRoot); rootClock.AddToTimeManager(); return rootClock; } internal virtual void ClearCurrentIntervalsToNull() { } // Perform Stage 1 of clipping next tick time: clip by parent internal void ClipNextTickByParent() { // Clip by parent's NTNT if needed. We don't want to clip // if the parent is the TimeManager's root clock. if (!IsTimeManager && !_parent.IsTimeManager && (!InternalNextTickNeededTime.HasValue || (_parent.InternalNextTickNeededTime.HasValue && _parent.InternalNextTickNeededTime.Value < InternalNextTickNeededTime.Value))) { InternalNextTickNeededTime = _parent.InternalNextTickNeededTime; } } internal virtual void ComputeCurrentIntervals(TimeIntervalCollection parentIntervalCollection, TimeSpan beginTime, TimeSpan? endTime, Duration fillDuration, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { } internal virtual void ComputeCurrentFillInterval(TimeIntervalCollection parentIntervalCollection, TimeSpan beginTime, TimeSpan endTime, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { } internal void ComputeLocalState() { Debug.Assert(!IsTimeManager); // Cache previous state values ClockState lastClockState = _currentClockState; TimeSpan? lastCurrentTime = _currentTime; double? lastCurrentGlobalSpeed = _currentGlobalSpeed; double? lastCurrentProgress = _currentProgress; Int32? lastCurrentIteration = _currentIteration; // Reset the PauseStateChangedDuringTick for this tick PauseStateChangedDuringTick = false; ComputeLocalStateHelper(true, false); // Perform the local state calculations with early bail out if (lastClockState != _currentClockState) { // It can happen that we change state without detecting it when // a parent is auto-reversing and we are ticking exactly at the // reverse point, so raise the events. RaiseCurrentStateInvalidated(); RaiseCurrentGlobalSpeedInvalidated(); RaiseCurrentTimeInvalidated(); } // if (_currentGlobalSpeed != lastCurrentGlobalSpeed) { RaiseCurrentGlobalSpeedInvalidated(); } if (HasDiscontinuousTimeMovementOccured) { DiscontinuousTimeMovement(); HasDiscontinuousTimeMovementOccured = false; } } ////// Return the current duration from a specific clock /// ////// A Duration quantity representing the current iteration's estimated duration. /// internal virtual Duration CurrentDuration { get { return Duration.Automatic; } } ////// Internal helper. Schedules an interactive begin at the next tick. /// An interactive begin is literally a seek to 0. It is completely distinct /// from the BeginTime specified on a timeline, which is managed by /// _pendingBeginOffset /// internal void InternalBegin() { InternalSeek(TimeSpan.Zero); } ////// Internal helper for moving to new API. Gets the speed multiplier for the Clock's speed. /// internal double InternalGetSpeedRatio() { return _rootData.InteractiveSpeedRatio; } ////// Internal helper for moving to new API. Pauses the timeline for this timeline and its children. /// internal void InternalPause() { // VerifyAccess(); Debug.Assert(!IsTimeManager); // INVARIANT: we enforce 4 possible valid states: // 1) !Paused (running) // 2) !Paused, Pause pending // 3) Paused // 4) Paused, Resume pending Debug.Assert(!(IsInteractivelyPaused && PendingInteractivePause)); Debug.Assert(!(!IsInteractivelyPaused && PendingInteractiveResume)); if (PendingInteractiveResume) // Cancel existing resume request if made { PendingInteractiveResume = false; } else if (!IsInteractivelyPaused) // If we don't have a pending resume AND we aren't paused already, schedule a pause // This is an ELSE clause because if we had a Resume pending, we MUST already be paused { PendingInteractivePause = true; } NotifyNewEarliestFutureActivity(); } ////// Schedules a Remove operation to happen at the next tick. /// ////// This method schedules the Clock and its subtree to be stopped and the RemoveRequested /// event to be fired on the subtree at the next tick. /// internal void InternalRemove() { PendingInteractiveRemove = true; InternalStop(); } ////// Internal helper for moving to new API. Allows a timeline's timeline to progress again after a call to Pause. /// internal void InternalResume() { // VerifyAccess(); Debug.Assert(!IsTimeManager); // INVARIANT: we enforce 4 possible valid states: // 1) !Paused (running) // 2) !Paused, Pause pending // 3) Paused // 4) Paused, sd Resume pending Debug.Assert( !(IsInteractivelyPaused && PendingInteractivePause)); Debug.Assert( !(!IsInteractivelyPaused && PendingInteractiveResume)); if (PendingInteractivePause) // Cancel existing pause request if made { PendingInteractivePause = false; } else if (IsInteractivelyPaused) // If we don't have a pending pause AND we are currently paused, schedule a resume // This is an ELSE clause because if we had a Pause pending, we MUST already be unpaused { PendingInteractiveResume = true; } NotifyNewEarliestFutureActivity(); } ////// Internal helper for moving to new API. Seeks a timeline's timeline to a new position. /// /// /// The destination to seek to, relative to the clock's BeginTime. If this is past the /// active period execute the FillBehavior. /// internal void InternalSeek(TimeSpan destination) { // VerifyAccess(); Debug.Assert(IsRoot); IsInteractivelyStopped = false; PendingInteractiveStop = false; // Cancel preceding stop; ResetNodesWithSlip(); // Reset [....] tracking _rootData.PendingSeekDestination = destination; RootBeginPending = false; // cancel a previous begin call NotifyNewEarliestFutureActivity(); } ////// The only things that can change for this are the begin time of this /// timeline /// /// /// The destination to seek to, relative to the clock's BeginTime. If this is past the /// active perioed execute the FillBehavior. /// internal void InternalSeekAlignedToLastTick(TimeSpan destination) { Debug.Assert(IsRoot); // This is a no-op with a null TimeManager or when all durations have not yet been resolved if (_timeManager == null || HasDescendantsWithUnresolvedDuration) { return; } // Adjust _beginTime such that our current time equals the Seek position // that was requested _beginTime = CurrentGlobalTime - DivideTimeSpan(destination, _appliedSpeedRatio); if (CanGrow) { _currentIteration = null; // This node is not visited by ResetSlipOnSubtree _currentIterationBeginTime = _beginTime; ResetSlipOnSubtree(); UpdateSyncBeginTime(); } IsInteractivelyStopped = false; // We have unset disabled status PendingInteractiveStop = false; RootBeginPending = false; // Cancel a pending begin ResetNodesWithSlip(); // Reset [....] tracking _timeManager.InternalCurrentIntervals = TimeIntervalCollection.Empty; PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { // We are processing a Seek immediately. We don't need a TIC yet // since we are not computing events, and we don't want to // process pending stuff either. subtree.Current.ComputeLocalStateHelper(false, true); // Compute the state of the node if (HasDiscontinuousTimeMovementOccured) { DiscontinuousTimeMovement(); HasDiscontinuousTimeMovementOccured = false; } subtree.Current.ClipNextTickByParent(); // Perform NextTick clipping, stage 1 // Make a note to visit for stage 2, only for ClockGroups subtree.Current.NeedsPostfixTraversal = (subtree.Current is ClockGroup); } _parent.ComputeTreeStateRoot(); // Re-clip the next tick estimates by children // Fire the events indicating that we've invalidated this whole subtree subtree.Reset(); while (subtree.MoveNext()) { // CurrentTimeInvalidated should be fired first to give AnimationStorage a chance // to update the value. We directly set the flags, then fire RaiseAccumulatedEvents // to avoid involving the TimeManager for this local subtree operation. subtree.Current.CurrentTimeInvalidatedEventRaised = true; subtree.Current.CurrentStateInvalidatedEventRaised = true; subtree.Current.CurrentGlobalSpeedInvalidatedEventRaised = true; subtree.Current.RaiseAccumulatedEvents(); } } ////// Internal helper for moving to new API. Sets a speed multiplier for the Clock's speed. /// /// /// The ratio by which to multiply the Clock's speed. /// internal void InternalSetSpeedRatio(double ratio) { Debug.Assert(IsRoot); _rootData.PendingSpeedRatio = ratio; } ////// Internal helper. Schedules an end for some specified time in the future. /// internal void InternalSkipToFill() { Debug.Assert(IsRoot); TimeSpan? effectiveDuration; effectiveDuration = ComputeEffectiveDuration(); // Throw an exception if the active period extends forever. if (effectiveDuration == null) { // Can't seek to the end if the simple duration is not resolved throw new InvalidOperationException(SR.Get(SRID.Timing_SkipToFillDestinationIndefinite)); } // Offset to the end; override preceding seek requests IsInteractivelyStopped = false; PendingInteractiveStop = false; ResetNodesWithSlip(); // Reset [....] tracking RootBeginPending = false; _rootData.PendingSeekDestination = effectiveDuration.Value; // Seek to the end time NotifyNewEarliestFutureActivity(); } ////// Internal helper for moving to new API. Removes a timeline from its active or fill period. /// Timeline can be restarted with an interactive Begin call. /// internal void InternalStop() { Debug.Assert(IsRoot); PendingInteractiveStop = true; // Cancel all non-persistent interactive requests _rootData.PendingSeekDestination = null; RootBeginPending = false; ResetNodesWithSlip(); // Reset [....] tracking NotifyNewEarliestFutureActivity(); } ////// Raises the events that occured since the last tick and reset their state. /// internal void RaiseAccumulatedEvents() { try // We are calling user-defined delegates, if they throw we must ensure that we leave the Clock in a valid state { // CurrentTimeInvalidated should fire first. This is because AnimationStorage hooks itself // up to this event in order to invalidate whichever DependencyProperty this clock may be // animating. User code in any of these callbacks may query the value of that DP - if they // do so before AnimationStorage has a chance to invalidate they will get the wrong value. if (CurrentTimeInvalidatedEventRaised) { FireCurrentTimeInvalidatedEvent(); } if (CurrentGlobalSpeedInvalidatedEventRaised) { FireCurrentGlobalSpeedInvalidatedEvent(); // Tell the Clocks that they have changed Speed SpeedChanged(); } if (CurrentStateInvalidatedEventRaised) { FireCurrentStateInvalidatedEvent(); // Since the state has been invalidated this means that // we've got a discontinuous time movemement. Tell the clock if (!CurrentGlobalSpeedInvalidatedEventRaised) { DiscontinuousTimeMovement(); } } if (CompletedEventRaised) { FireCompletedEvent(); } if (RemoveRequestedEventRaised) { FireRemoveRequestedEvent(); } } finally // Reset the flags to make the state consistent, even if the user has thrown { CurrentTimeInvalidatedEventRaised = false; CurrentGlobalSpeedInvalidatedEventRaised = false; CurrentStateInvalidatedEventRaised = false; CompletedEventRaised = false; RemoveRequestedEventRaised = false; IsInEventQueue = false; } } ////// Raises the Completed event. /// ////// We only need to raise this event once per tick. If we've already /// raised it in this tick, do nothing. /// internal void RaiseCompleted() { Debug.Assert(!IsTimeManager); CompletedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } ////// Raises the CurrentGlobalSpeedInvalidated event. /// ////// We only need to raise this event once per tick. If we've already /// raised it in this tick, do nothing. /// internal void RaiseCurrentGlobalSpeedInvalidated() { // ROOT Debug.Assert(!IsTimeManager); CurrentGlobalSpeedInvalidatedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } ////// Raises the CurrentStateInvalidated event. /// internal void RaiseCurrentStateInvalidated() { Debug.Assert(!IsTimeManager); if (_currentClockState == ClockState.Stopped) // If our state changed to stopped { Stopped(); } CurrentStateInvalidatedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } ////// Raises the CurrentTimeInvalidated event. This enqueues the event for later dispatch /// if we are in a tick operation. /// internal void RaiseCurrentTimeInvalidated() { Debug.Assert(!IsTimeManager); CurrentTimeInvalidatedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } ////// Raises the RemoveRequested event. /// ////// We only need to raise this event once per tick. If we've already /// raised it in this tick, do nothing. /// internal void RaiseRemoveRequested() { Debug.Assert(!IsTimeManager); RemoveRequestedEventRaised = true; if (!IsInEventQueue) { _timeManager.AddToEventQueue(this); IsInEventQueue = true; } } // Reset all currently cached state internal void ResetCachedStateToStopped() { _currentGlobalSpeed = null; _currentIteration = null; IsBackwardsProgressingGlobal = false; _currentProgress = null; _currentTime = null; _currentClockState = ClockState.Stopped; } // Reset IsInSyncPeriod for this node and all children if any (ClockGroup handles this). // We do this whenever a discontinuous interactive action (seek/begin/stop) is performed. internal virtual void ResetNodesWithSlip() { if (_syncData != null) { _syncData.IsInSyncPeriod = false; // Reset [....] tracking } } ////// Check if our descendants have resolved their duration, and resets the HasDescendantsWithUnresolvedDuration /// flag from true to false once that happens. /// ///Returns true when this node or one of its descendants have unresolved duration. internal virtual void UpdateDescendantsWithUnresolvedDuration() { if (HasResolvedDuration) { HasDescendantsWithUnresolvedDuration = false; } } #endregion // Internal Methods // // Internal Properties // #region Internal Properties ////// Specifies the depth of this timeline in the timing tree. If the timeline does not have /// a parent, its depth value is zero. /// internal int Depth { get { // ROOT Debug.Assert(!IsTimeManager); return _depth; } } ////// Returns the last time this timeline will become non-active if it is not /// clipped by the parent container first. Null represents infinity. /// internal Duration EndOfActivePeriod { get { Debug.Assert(!IsTimeManager); if (!HasResolvedDuration) { return Duration.Automatic; } // Computed expiration time with respect to repeat behavior and natural duration; TimeSpan? expirationTime; ComputeExpirationTime(out expirationTime); // if (expirationTime.HasValue) { return expirationTime.Value; } else { return Duration.Forever; } } } ////// Gets the first child of this timeline. /// ////// Since a Clock doesn't have children we will always return null /// internal virtual Clock FirstChild { get { return null; } } ////// Internal unverified access to the CurrentState /// ////// Only ClockGroup should set the value /// internal ClockState InternalCurrentClockState { get { return _currentClockState; } set { _currentClockState = value; } } ////// Internal unverified access to the CurrentGlobalSpeed /// ////// Only ClockGroup should set the value /// internal double? InternalCurrentGlobalSpeed { get { return _currentGlobalSpeed; } set { _currentGlobalSpeed = value; } } ////// Internal unverified access to the CurrentIteration /// ////// Only ClockGroup should set the value /// internal Int32? InternalCurrentIteration { get { return _currentIteration; } set { _currentIteration = value; } } ////// Internal unverified access to the CurrentProgress /// ////// Only ClockGroup should set the value /// internal double? InternalCurrentProgress { get { return _currentProgress; } set { _currentProgress = value; } } ////// The next GlobalTime that this clock may need a tick /// internal TimeSpan? InternalNextTickNeededTime { get { return _nextTickNeededTime; } set { _nextTickNeededTime = value; } } ////// Unchecked internal access to the parent of this Clock. /// internal ClockGroup InternalParent { get { Debug.Assert(!IsTimeManager); return _parent; } } ////// Gets the current Duration for internal callers. This property will /// never return Duration.Automatic. If the _resolvedDuration of the Clock /// has not yet been resolved, this property will return Duration.Forever. /// Therefore, it's possible that the value of this property will change /// one time during the Clock's lifetime. It's not predictable when that /// change will occur, it's up to the custom Clock author. /// internal Duration ResolvedDuration { get { ResolveDuration(); Debug.Assert(_resolvedDuration != Duration.Automatic, "_resolvedDuration should never be set to Automatic."); return _resolvedDuration; } } ////// Gets the right sibling of this timeline. /// ////// The right sibling of this timeline if it's not the last in its parent's /// collection; otherwise, null. /// internal Clock NextSibling { get { Debug.Assert(!IsTimeManager); Debug.Assert(_parent != null && !_parent.IsTimeManager); ListparentChildren = _parent.InternalChildren; if (_childIndex == parentChildren.Count - 1) { return null; } else { return parentChildren[_childIndex + 1]; } } } /// /// Gets a cached weak reference to this clock. /// internal WeakReference WeakReference { get { WeakReference reference = _weakReference; if (reference == null) { reference = new WeakReference(this); _weakReference = reference; } return reference; } } ////// Get the desired framerate of this clock /// internal int? DesiredFrameRate { get { int? returnValue = null; if (HasDesiredFrameRate) { returnValue = _rootData.DesiredFrameRate; } return returnValue; } } // // Internal access to some of the flags // #region Internal Flag Accessors internal bool CompletedEventRaised { get { return GetFlag(ClockFlags.CompletedEventRaised); } set { SetFlag(ClockFlags.CompletedEventRaised, value); } } internal bool CurrentGlobalSpeedInvalidatedEventRaised { get { return GetFlag(ClockFlags.CurrentGlobalSpeedInvalidatedEventRaised); } set { SetFlag(ClockFlags.CurrentGlobalSpeedInvalidatedEventRaised, value); } } internal bool CurrentStateInvalidatedEventRaised { get { return GetFlag(ClockFlags.CurrentStateInvalidatedEventRaised); } set { SetFlag(ClockFlags.CurrentStateInvalidatedEventRaised, value); } } internal bool CurrentTimeInvalidatedEventRaised { get { return GetFlag(ClockFlags.CurrentTimeInvalidatedEventRaised); } set { SetFlag(ClockFlags.CurrentTimeInvalidatedEventRaised, value); } } private bool HasDesiredFrameRate { get { return GetFlag(ClockFlags.HasDesiredFrameRate); } set { SetFlag(ClockFlags.HasDesiredFrameRate, value); } } internal bool HasResolvedDuration { get { return GetFlag(ClockFlags.HasResolvedDuration); } set { SetFlag(ClockFlags.HasResolvedDuration, value); } } internal bool IsBackwardsProgressingGlobal { get { return GetFlag(ClockFlags.IsBackwardsProgressingGlobal); } set { SetFlag(ClockFlags.IsBackwardsProgressingGlobal, value); } } internal bool IsInEventQueue { get { return GetFlag(ClockFlags.IsInEventQueue); } set { SetFlag(ClockFlags.IsInEventQueue, value); } } ////// Unchecked internal access to the paused state of the clock. /// ///internal bool IsInteractivelyPaused { get { return GetFlag(ClockFlags.IsInteractivelyPaused); } set { SetFlag(ClockFlags.IsInteractivelyPaused, value); } } internal bool IsInteractivelyStopped { get { return GetFlag(ClockFlags.IsInteractivelyStopped); } set { SetFlag(ClockFlags.IsInteractivelyStopped, value); } } internal bool IsRoot { get { return GetFlag(ClockFlags.IsRoot); } set { SetFlag(ClockFlags.IsRoot, value); } } internal bool IsTimeManager { get { return GetFlag(ClockFlags.IsTimeManager); } set { SetFlag(ClockFlags.IsTimeManager, value); } } /// /// Returns true if the Clock traversed during the first tick pass. /// ///internal bool NeedsPostfixTraversal { get { return GetFlag(ClockFlags.NeedsPostfixTraversal); } set { SetFlag(ClockFlags.NeedsPostfixTraversal, value); } } internal virtual bool NeedsTicksWhenActive { get { return GetFlag(ClockFlags.NeedsTicksWhenActive); } set { SetFlag(ClockFlags.NeedsTicksWhenActive, value); } } internal bool PauseStateChangedDuringTick { get { return GetFlag(ClockFlags.PauseStateChangedDuringTick); } set { SetFlag(ClockFlags.PauseStateChangedDuringTick, value); } } internal bool PendingInteractivePause { get { return GetFlag(ClockFlags.PendingInteractivePause); } set { SetFlag(ClockFlags.PendingInteractivePause, value); } } internal bool PendingInteractiveRemove { get { return GetFlag(ClockFlags.PendingInteractiveRemove); } set { SetFlag(ClockFlags.PendingInteractiveRemove, value); } } internal bool PendingInteractiveResume { get { return GetFlag(ClockFlags.PendingInteractiveResume); } set { SetFlag(ClockFlags.PendingInteractiveResume, value); } } internal bool PendingInteractiveStop { get { return GetFlag(ClockFlags.PendingInteractiveStop); } set { SetFlag(ClockFlags.PendingInteractiveStop, value); } } internal bool RemoveRequestedEventRaised { get { return GetFlag(ClockFlags.RemoveRequestedEventRaised); } set { SetFlag(ClockFlags.RemoveRequestedEventRaised, value); } } private bool HasDiscontinuousTimeMovementOccured { get { return GetFlag(ClockFlags.HasDiscontinuousTimeMovementOccured); } set { SetFlag(ClockFlags.HasDiscontinuousTimeMovementOccured, value); } } internal bool HasDescendantsWithUnresolvedDuration { get { return GetFlag(ClockFlags.HasDescendantsWithUnresolvedDuration); } set { SetFlag(ClockFlags.HasDescendantsWithUnresolvedDuration, value); } } private bool HasSeekOccuredAfterLastTick { get { return GetFlag(ClockFlags.HasSeekOccuredAfterLastTick); } set { SetFlag(ClockFlags.HasSeekOccuredAfterLastTick, value); } } #endregion // Internal Flag Accessors #endregion // Internal Properties // // Private Methods // #region Private Methods // // Local State Computation Helpers // #region Local State Computation Helpers // // Seek, Begin and Pause are internally implemented by adjusting the begin time. // For example, when paused, each tick moves the begin time forward so that // overall the clock hasn't moved. // // Note that _beginTime is an offset from the parent's begin. Since // these are root clocks, the parent is the TimeManager, and we // must add in the CurrentGlobalTime private void AdjustBeginTime() { Debug.Assert(IsRoot); // root clocks only; non-roots have constant begin time Debug.Assert(_rootData != null); // Process the effects of Seek, Begin, and Pause; delay request if not all durations are resolved in this subtree. if (_rootData.PendingSeekDestination.HasValue && !HasDescendantsWithUnresolvedDuration) { Debug.Assert(!RootBeginPending); // we can have either a begin or a seek, not both // Adjust the begin time such that our current time equals PendingSeekDestination _beginTime = CurrentGlobalTime - DivideTimeSpan(_rootData.PendingSeekDestination.Value, _appliedSpeedRatio); if (CanGrow) // One of our descendants has set this flag on us { _currentIterationBeginTime = _beginTime; // We relied on a combination of _currentIterationBeginTime and _currentIteration for our state _currentIteration = null; // Therefore, we should reset both to reset our position ResetSlipOnSubtree(); } UpdateSyncBeginTime(); _rootData.PendingSeekDestination = null; // We have seeked, so raise all events signifying that no assumptions can be made about our state PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.RaiseCurrentStateInvalidated(); subtree.Current.RaiseCurrentTimeInvalidated(); subtree.Current.RaiseCurrentGlobalSpeedInvalidated(); } } else if (RootBeginPending) { // RootBeginPending is set when a root is parented to a tree (in AddToRoot()). // It allows us to interpret Timeline.BeginTime as an offset from the current // time and thus schedule a begin in the future. _beginTime = CurrentGlobalTime + _timeline.BeginTime; if (CanGrow) // One of our descendants has set this flag on us { _currentIterationBeginTime = _beginTime; // We should be just starting our first iteration now } UpdateSyncBeginTime(); RootBeginPending = false; } else if ((IsInteractivelyPaused || _rootData.InteractiveSpeedRatio == 0) && (_syncData == null || !_syncData.IsInSyncPeriod)) // We were paused at the last tick, so move _beginTime by the delta from last tick to this one // Only perform this iff we are *continuously* moving, e.g. if we haven't seeked between ticks. // [....] NOTE: If we are syncing, then the [....] code should be the one to make this adjustment // by using the Media's current time (which should already be paused). { if (_beginTime.HasValue) { // Adjust for the speed of this timelineClock _beginTime += _timeManager.LastTickDelta; UpdateSyncBeginTime(); if (_currentIterationBeginTime.HasValue) // One of our descendants has set this flag on us { _currentIterationBeginTime += _timeManager.LastTickDelta; } } } // Adjust for changes to the speed ratio if (_rootData.PendingSpeedRatio.HasValue) { double pendingSpeedRatio = _rootData.PendingSpeedRatio.Value * _timeline.SpeedRatio; // If the calculated speed ratio is 0, we reset it to 1. I believe this is // because we don't want to support pausing by setting speed ratio. Instead // they should call pause. if (pendingSpeedRatio == 0) { pendingSpeedRatio = 1; } Debug.Assert(_beginTime.HasValue); // Below code uses the above assumption that beginTime has a value TimeSpan previewParentTime = CurrentGlobalTime; if (_currentIterationBeginTime.HasValue) { // Adjusting SpeedRatio is not a discontiuous event, we don't want to reset slip after doing this _currentIterationBeginTime = previewParentTime - MultiplyTimeSpan(previewParentTime - _currentIterationBeginTime.Value, _appliedSpeedRatio / pendingSpeedRatio); } else { _beginTime = previewParentTime - MultiplyTimeSpan(previewParentTime - _beginTime.Value, _appliedSpeedRatio / pendingSpeedRatio); } RaiseCurrentGlobalSpeedInvalidated(); // _appliedSpeedRatio represents the speed ratio we're actually using // for this Clock. _appliedSpeedRatio = pendingSpeedRatio; // _rootData.InteractiveSpeedRatio represents the actual user set interactive // speed ratio value even though we may override it if it's 0. _rootData.InteractiveSpeedRatio = _rootData.PendingSpeedRatio.Value; // Clear out the new pending speed ratio since we've finished applying it. _rootData.PendingSpeedRatio = null; UpdateSyncBeginTime(); } return; } // Apply the effects of having DFR set on a root clock internal void ApplyDesiredFrameRateToGlobalTime() { if (HasDesiredFrameRate) { _rootData.LastAdjustedGlobalTime = _rootData.CurrentAdjustedGlobalTime; _rootData.CurrentAdjustedGlobalTime = GetCurrentDesiredFrameTime(_timeManager.InternalCurrentGlobalTime); } } // Apply the effects of having DFR set on a root clock internal void ApplyDesiredFrameRateToNextTick() { Debug.Assert(IsRoot); if (HasDesiredFrameRate && InternalNextTickNeededTime.HasValue) { // If we have a desired frame rate, it should greater than // zero (the default for the field.) Debug.Assert(_rootData.DesiredFrameRate > 0); // We "round" our next tick needed time up to the next frame TimeSpan nextDesiredTick = InternalNextTickNeededTime == TimeSpan.Zero ? _rootData.CurrentAdjustedGlobalTime : InternalNextTickNeededTime.Value; InternalNextTickNeededTime = GetNextDesiredFrameTime(nextDesiredTick); } } // Determines the current iteration and uncorrected linear time accounting for _timeline.AutoReverse // At the end of this function call, the following state attributes are initialized: // CurrentIteration private bool ComputeCurrentIteration(TimeSpan parentTime, double parentSpeed, TimeSpan? expirationTime, out TimeSpan localProgress) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_currentClockState != ClockState.Stopped); Debug.Assert(_currentDuration != Duration.Automatic, "_currentDuration should never be Automatic."); Debug.Assert(_beginTime.HasValue); Debug.Assert(parentTime >= _beginTime.Value); // We are active or in postfill RepeatBehavior repeatBehavior = _timeline.RepeatBehavior; // Apply speed and offset, convert down to TimeSpan TimeSpan beginTimeForOffsetComputation = _currentIterationBeginTime.HasValue ? _currentIterationBeginTime.Value : _beginTime.Value; TimeSpan offsetFromBegin = MultiplyTimeSpan(parentTime - beginTimeForOffsetComputation, _appliedSpeedRatio); // This may be set redundantly in one case, but simplifies code IsBackwardsProgressingGlobal = _parent.IsBackwardsProgressingGlobal; if (_currentDuration.HasTimeSpan) // For finite duration, use modulo arithmetic to compute current iteration { if (_currentDuration.TimeSpan == TimeSpan.Zero) // We must be post-filling if we have gotten here { Debug.Assert(_currentClockState != ClockState.Active); // Assign localProgress to avoid compiler error localProgress = TimeSpan.Zero; // CurrentTime will always be zero. _currentTime = TimeSpan.Zero; Double currentProgress; if (repeatBehavior.HasCount) { Double repeatCount = repeatBehavior.Count; if (repeatCount <= 1.0) { currentProgress = repeatCount; _currentIteration = 1; } else { Double wholePart = (Double)((Int32)repeatCount); if (repeatCount == wholePart) { currentProgress = 1.0; _currentIteration = (Int32)repeatCount; } else { currentProgress = repeatCount - wholePart; _currentIteration = (Int32)(repeatCount + 1.0d); } } } else { // RepeatBehavior.HasTimeSpan cases: // I guess we could repeat a 0 Duration inside of a 0 or // greater TimeSpan an infinite number of times. But that's // not really helpful to the user. // RepeatBehavior.Forever cases: // The situation here is that we have done an infinite amount // of work in zero time, so exact answers are hard to determine: // The "correct" current iteration may be Double.PositiveInfinity, // however returning this just makes our API too tricky to work with. // There is no "correct" current progress. // In both cases we'll say we repeated one whole iteration exactly // once to make things easy for the user. _currentIteration = 1; currentProgress = 1.0; } // Adjust progress for AutoReverse. if (_timeline.AutoReverse) { if (currentProgress == 1.0) { currentProgress = 0.0; } else if (currentProgress < 0.5) { currentProgress *= 2.0; } else { currentProgress = 1.0 - ((currentProgress - 0.5) * 2.0); } } _currentProgress = currentProgress; return true; } else // CurrentDuration.TimeSpan != TimeSpan.Zero { if (_currentClockState == ClockState.Filling && repeatBehavior.HasCount && !_currentIterationBeginTime.HasValue) { // // This block definitely needs a long comment. // // Basically, roundoff errors in the computation of offsetFromBegin // can cause us to calculate localProgress incorrectly. // Normally this doesn't matter, since we'll be off by a // miniscule amount. However, if we have a Filling Clock that // is exactly on a boundary, offsetFromBegin % duration should be 0. // We check for this special case below. If we have any rounding // errors in this situation, the modulus will not be 0 and we'll // Fill at the wrong place (for example, we may think the Clock // is Filling at the very beginning of its second iteration when // it should be Filling at the very end of its first). // // The precision error is due to dividing, then multiplying by, // appliedSpeedRatio when computing offsetFromBegin(to see this trace // back the computation of parentTime, expirationTime, and // effectiveDuration). The specific codepath that does // this is only executed when we have a Filling clock with // RepeatBehavior.HasCount. In this special case we can avoid // the precision error by calculating offsetFromBegin directly. // TimeSpan optimizedOffsetFromBegin; double scalingFactor = repeatBehavior.Count; if (_timeline.AutoReverse) { scalingFactor *= 2; } optimizedOffsetFromBegin = MultiplyTimeSpan(_resolvedDuration.TimeSpan, scalingFactor); Debug_VerifyOffsetFromBegin(offsetFromBegin.Ticks, optimizedOffsetFromBegin.Ticks); offsetFromBegin = optimizedOffsetFromBegin; } int newIteration; if (_currentIterationBeginTime.HasValue) { ComputeCurrentIterationWithGrow(parentTime, expirationTime, out localProgress, out newIteration); } else // Regular scenario -- no Grow behavior { localProgress = TimeSpan.FromTicks(offsetFromBegin.Ticks % _currentDuration.TimeSpan.Ticks); newIteration = (int)(offsetFromBegin.Ticks / _resolvedDuration.TimeSpan.Ticks); // Iteration count starting from 0 } // Iteration boundary cases depend on which direction the parent progresses and if we are Filling if ((localProgress == TimeSpan.Zero) && (newIteration > 0) // Detect a boundary case past the first zero (begin point) && (_currentClockState == ClockState.Filling || _parent.IsBackwardsProgressingGlobal)) { // Special post-fill case: // We hit 0 progress because of wraparound in modulo arithmetic. However, for post-fill we don't // want to wrap around to zero; we compensate for that here. The only legal way to hit zero // post-fill is at the ends of autoreversed segments, which are handled by logic further below. // Note that parentTime is clamped to expirationTime in post-fill situations, so even if the // actual parentTime is larger, it would still get clamped and then potentially wrapped to 0. // We are at 100% progress of previous iteration, instead of 0% progress of next one // Back up to previous iteration localProgress = _currentDuration.TimeSpan; newIteration--; } // Invert the localProgress for odd (AutoReversed) paths if (_timeline.AutoReverse) { if ((newIteration & 1) == 1) // We are on a reversing segment { if (localProgress == TimeSpan.Zero) { // We're exactly at an AutoReverse inflection point. Any active children // of this clock will be filling for this point only. The next tick // time needs to be 0 so that they can go back to active; filling clocks // aren't ordinarily ticked. InternalNextTickNeededTime = TimeSpan.Zero; } localProgress = _currentDuration.TimeSpan - localProgress; IsBackwardsProgressingGlobal = !IsBackwardsProgressingGlobal; parentSpeed = -parentSpeed; // Negate parent speed here for tick logic, since we negated localProgress } newIteration = newIteration / 2; // Definition of iteration with AutoReverse is a front and back segment, divide by 2 } _currentIteration = 1 + newIteration; // Officially, iterations are numbered from 1 // This is where we predict tick logic for approaching an iteration boundary // We only need to do this if NTWA == false because otherwise, we already have NTNT = zero if (_currentClockState == ClockState.Active && parentSpeed != 0 && !NeedsTicksWhenActive) { TimeSpan timeUntilNextBoundary; if (localProgress == TimeSpan.Zero) // We are currently exactly at a boundary { timeUntilNextBoundary = DivideTimeSpan(_currentDuration.TimeSpan, Math.Abs(parentSpeed)); } else if (parentSpeed > 0) // We are approaching the next iteration boundary (end or decel zone) { TimeSpan decelBegin = MultiplyTimeSpan(_currentDuration.TimeSpan, 1.0 - _timeline.DecelerationRatio); timeUntilNextBoundary = DivideTimeSpan(decelBegin - localProgress, parentSpeed); } else // parentSpeed < 0, we are approaching the previous iteration boundary { TimeSpan accelEnd = MultiplyTimeSpan(_currentDuration.TimeSpan, _timeline.AccelerationRatio); timeUntilNextBoundary = DivideTimeSpan(accelEnd - localProgress, parentSpeed); } TimeSpan proposedNextTickTime = CurrentGlobalTime + timeUntilNextBoundary; if (!InternalNextTickNeededTime.HasValue || proposedNextTickTime < InternalNextTickNeededTime.Value) { InternalNextTickNeededTime = proposedNextTickTime; } } } } else // CurrentDuration is Forever { Debug.Assert(_currentDuration == Duration.Forever, "_currentDuration has an invalid enum value."); Debug.Assert(_currentClockState == ClockState.Active || (_currentClockState == ClockState.Filling && expirationTime.HasValue && parentTime >= expirationTime)); localProgress = offsetFromBegin; _currentIteration = 1; // We have infinite duration, so iteration is 1 } return false; // We aren't done computing state yet } // This should only be called for nodes which have RepeatBehavior and have ancestors which can slip; // It gets called after we move from our current iteration to a new one private void ComputeCurrentIterationWithGrow(TimeSpan parentTime, TimeSpan? expirationTime, out TimeSpan localProgress, out int newIteration) { Debug.Assert(this is ClockGroup, "ComputeCurrentIterationWithGrow should only run on ClockGroups."); Debug.Assert(CanGrow, "ComputeCurrentIterationWithGrow should only run on clocks with CanGrow."); Debug.Assert(_currentIterationBeginTime.HasValue, "ComputeCurrentIterationWithGrow should only be called when _currentIterationBeginTime has a value."); Debug.Assert(_resolvedDuration.HasTimeSpan, "ComputeCurrentIterationWithGrow should only be called when _resolvedDuration has a value."); // We must have a computed duration Debug.Assert(_currentDuration.HasTimeSpan, "ComputeCurrentIterationWithGrow should only be called when _currentDuration has a value."); TimeSpan offsetFromBegin = MultiplyTimeSpan(parentTime - _currentIterationBeginTime.Value, _appliedSpeedRatio); int iterationIncrement; if (offsetFromBegin < _currentDuration.TimeSpan) // We fall within the same iteration as during last tick { localProgress = offsetFromBegin; iterationIncrement = 0; } else // offsetFromBegin is larger than _currentDuration, so we have moved at least one iteration up { // This iteration variable is actually 0-based, but if we got into this IF block, we are past 0th iteration long offsetOnLaterIterations = (offsetFromBegin - _currentDuration.TimeSpan).Ticks; localProgress = TimeSpan.FromTicks(offsetOnLaterIterations % _resolvedDuration.TimeSpan.Ticks); iterationIncrement = 1 + (int)(offsetOnLaterIterations / _resolvedDuration.TimeSpan.Ticks); // Now, adjust to a new current iteration: // Use the current and resolved values of Duration to compute the beginTime for the latest iteration // We know that we at least have passed the current iteration, so add _currentIteration; // If we also passed subsequent iterations, then assume they have perfect durations (_resolvedDuration) each. _currentIterationBeginTime += _currentDuration.TimeSpan + MultiplyTimeSpan(_resolvedDuration.TimeSpan, iterationIncrement - 1); // If we hit the Filling state, we could fall exactly on the iteration finish boundary. // In this case, step backwards one iteration so that we are one iteration away from the finish if (_currentClockState == ClockState.Filling && expirationTime.HasValue && _currentIterationBeginTime >= expirationTime) { if (iterationIncrement > 1) // We last added a resolvedDuration, subtract it back out { _currentIterationBeginTime -= _resolvedDuration.TimeSpan; } else // iterationIncrement == 1, we only added a currentDuration, subtract it back out { _currentIterationBeginTime -= _currentDuration.TimeSpan; } } else // We have not encountered a false finish due to entering Fill state { // Reset all children's slip time here; NOTE that this will change our effective duration, // but this will not become important until the next tick, when it will be recomputed anyway. ResetSlipOnSubtree(); } } newIteration = _currentIteration.HasValue ? iterationIncrement + (_currentIteration.Value - 1) : iterationIncrement; } /// /// Determine if we are active, filling, or off /// parentTime is clamped if it is inside the postfill zone /// We have to handle reversed parent differently because we have closed-open intervals in global time /// /// Our computed expiration time, null if infinite. /// Our parent time. /// Our parent speed. /// Whether we are called from within a tick. ///private bool ComputeCurrentState(TimeSpan? expirationTime, ref TimeSpan parentTime, double parentSpeed, bool isInTick) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_beginTime.HasValue); FillBehavior fillBehavior = _timeline.FillBehavior; if (parentTime < _beginTime) // Including special backward progressing case { ResetCachedStateToStopped(); return true; // Nothing more to compute here } else if ( expirationTime.HasValue && parentTime >= expirationTime) // We are in postfill zone { RaiseCompletedForRoot(isInTick); if (fillBehavior == FillBehavior.HoldEnd) #if IMPLEMENTED // Uncomment when we enable new FillBehaviors || fillBehavior == FillBehavior.HoldBeginAndEnd) #endif { ResetCachedStateToFilling(); parentTime = expirationTime.Value; // Clamp parent time to expiration time // We still don't know our current time or progress at this point } else { ResetCachedStateToStopped(); return true; // We are off, nothing more to compute } } else // Else we are inside the active interval and thus active { _currentClockState = ClockState.Active; } // This is where we short-circuit Next Tick Needed logic while we are active if (parentSpeed != 0 && _currentClockState == ClockState.Active && NeedsTicksWhenActive) { InternalNextTickNeededTime = TimeSpan.Zero; // We need ticks immediately } return false; // There is more state to compute } // If we reach this function, we are active private bool ComputeCurrentSpeed(double localSpeed) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_currentClockState == ClockState.Active); // Must be active at this point if (IsInteractivelyPaused) { _currentGlobalSpeed = 0; } else { localSpeed *= _appliedSpeedRatio; if (IsBackwardsProgressingGlobal) // Negate speed if we are on a backwards arc of an autoreversing timeline { localSpeed = -localSpeed; } // Get global speed by multiplying by parent global speed _currentGlobalSpeed = localSpeed * _parent._currentGlobalSpeed; } return false; // There may be more state to compute yet } // Determines the time and local speed with accel+decel // At the end of this function call, the following state attributes are initialized: // CurrentProgress // CurrentTime private bool ComputeCurrentTime(TimeSpan localProgress, out double localSpeed) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_currentClockState != ClockState.Stopped); Debug.Assert(_currentDuration != Duration.Automatic, "_currentDuration should never be Automatic."); if (_currentDuration.HasTimeSpan) // Finite duration, need to apply accel/decel { Debug.Assert(_currentDuration.TimeSpan > TimeSpan.Zero, "ComputeCurrentTime was entered with _currentDuration <= 0"); double userAcceleration = _timeline.AccelerationRatio; double userDeceleration = _timeline.DecelerationRatio; double transitionTime = userAcceleration + userDeceleration; // The following assert is enforced when the Acceleration or Deceleration are set. Debug.Assert(transitionTime <= 1, "The values of the accel and decel attributes incorrectly add to more than 1.0"); Debug.Assert(transitionTime >= 0, "The values of the accel and decel attributes incorrectly add to less than 0.0"); double durationInTicks = (double)_currentDuration.TimeSpan.Ticks; double t = ((double)localProgress.Ticks) / durationInTicks; // For tracking progress if (transitionTime == 0) // Case of no accel/decel { localSpeed = 1; _currentTime = localProgress; } else { double maxRate = 2 / (2 - transitionTime); if (t < userAcceleration) { // Acceleration phase localSpeed = maxRate * t / userAcceleration; t = maxRate * t * t / (2 * userAcceleration); // Fix for bug 118853: Animations with Deceleration cause the Timing system to // keep ticking while idle. Only reset NextTickNeededTime when we are // Active. When we (or our parent) is Filling, there is no non-linear // unpredictability to our behavior that requires us to reset NextTickNeededTime. if (_currentClockState == ClockState.Active && _parent._currentClockState == ClockState.Active) { // We are in a non-linear segment, cannot linearly predict anything InternalNextTickNeededTime = TimeSpan.Zero; } } else if (t <= (1 - userDeceleration)) { // Run-rate phase localSpeed = maxRate; t = maxRate * (t - userAcceleration / 2); } else { // Deceleration phase double tc = 1 - t; // t's complement from 1 localSpeed = maxRate * tc / userDeceleration; t = 1 - maxRate * tc * tc / (2 * userDeceleration); // Fix for bug 118853: Animations with Deceleration cause the Timing system to // keep ticking while idle. Only reset NextTickNeededTime when we are // Active. When we (or our parent) is Filling, there is no non-linear // unpredictability to our behavior that requires us to reset NextTickNeededTime. if (_currentClockState == ClockState.Active && _parent._currentClockState == ClockState.Active) { // We are in a non-linear segment, cannot linearly predict anything InternalNextTickNeededTime = TimeSpan.Zero; } } _currentTime = TimeSpan.FromTicks((long)((t * durationInTicks) + 0.5)); } _currentProgress = t; } else // CurrentDuration is Forever { Debug.Assert(_currentDuration == Duration.Forever, "_currentDuration has an invalid enum value."); _currentTime = localProgress; _currentProgress = 0; localSpeed = 1; } return (_currentClockState != ClockState.Active); // Proceed to calculate global speed if we are active } // Compute the duration private void ResolveDuration() { Debug.Assert(!IsTimeManager); if (!HasResolvedDuration) { Duration duration = NaturalDuration; if (duration != Duration.Automatic) { _resolvedDuration = duration; _currentDuration = duration; // If CurrentDuration is different, we update it later in this method HasResolvedDuration = true; } else { Debug.Assert(_resolvedDuration == Duration.Forever, "_resolvedDuration should be Forever when NaturalDuration is Automatic."); } } if (CanGrow) { _currentDuration = CurrentDuration; if (_currentDuration == Duration.Automatic) { _currentDuration = Duration.Forever; // We treat Automatic as unresolved current duration } } // We have descendants (such as Media) which don't know their duration yet. Note that this won't prevent us // from resolving our own Duration when it is explicitly set on the ParallelTimeline; therefore, we keep // a separate flag for the entire subtree. if (HasDescendantsWithUnresolvedDuration) { UpdateDescendantsWithUnresolvedDuration(); // See if this is still the case } } // This returns the effective duration of a clock. The effective duration is basically the // length of the clock's active period, taking into account speed ratio, repeat, and autoreverse. // Null is used to represent an infinite effective duration. private TimeSpan? ComputeEffectiveDuration() { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped || IsRoot); ResolveDuration(); Debug.Assert(_resolvedDuration != Duration.Automatic, "_resolvedDuration should never be Automatic."); Debug.Assert(_currentDuration != Duration.Automatic, "_currentDuration should never be Automatic."); TimeSpan? effectiveDuration; RepeatBehavior repeatBehavior = _timeline.RepeatBehavior; if (_currentDuration.HasTimeSpan && _currentDuration.TimeSpan == TimeSpan.Zero) { // Zero-duration case ignores any repeat behavior effectiveDuration = TimeSpan.Zero; } else if (repeatBehavior.HasCount) { if (repeatBehavior.Count == 0) // This clause avoids multiplying an infinite duration by zero { effectiveDuration = TimeSpan.Zero; } else if (_currentDuration == Duration.Forever) { effectiveDuration = null; // We use Null to represent infinite duration } else if (!CanGrow) // Case of finite duration { Debug.Assert(_currentDuration.HasTimeSpan, "_currentDuration is invalid, neither Forever nor a TimeSpan."); Debug.Assert(_currentDuration == _resolvedDuration, "For clocks which cannot grow, _currentDuration must equal _resolvedDuration."); double scalingFactor = repeatBehavior.Count / _appliedSpeedRatio; if (_timeline.AutoReverse) { scalingFactor *= 2; } effectiveDuration = MultiplyTimeSpan(_currentDuration.TimeSpan, scalingFactor); } else // Finite duration, CanGrow: _currentDuration may be different from _resolvedDuration { Debug.Assert(_resolvedDuration.HasTimeSpan, "_resolvedDuration is invalid, neither Forever nor a TimeSpan."); Debug.Assert(_currentDuration.HasTimeSpan, "_currentDuration is invalid, neither Forever nor a TimeSpan."); TimeSpan previousIterationDuration = TimeSpan.Zero; double presentAndFutureIterations = repeatBehavior.Count; double presentAndFutureDuration; // Note: this variable is not scaled by speedRatio, we scale it further down: // If we have growth, we have to take prior iterations into account as a FIXED time span if (CanGrow && _currentIterationBeginTime.HasValue && _currentIteration.HasValue) { Debug.Assert(_beginTime.HasValue); // _currentIterationBeginTime.HasValue implies _beginTime.HasValue presentAndFutureIterations -= (_currentIteration.Value - 1); previousIterationDuration = _currentIterationBeginTime.Value - _beginTime.Value; } if (presentAndFutureIterations <= 1) // This means we are on our last iteration { presentAndFutureDuration = ((double)_currentDuration.TimeSpan.Ticks) * presentAndFutureIterations; } else // presentAndFutureIterations > 1, we are not at the last iteration, so count _currentDuration a full one time { presentAndFutureDuration = ((double)_currentDuration.TimeSpan.Ticks) // Current iteration; below is the future iteration length + ((double)_resolvedDuration.TimeSpan.Ticks) * (presentAndFutureIterations - 1); } if (_timeline.AutoReverse) { presentAndFutureDuration *= 2; // Double the remaining duration with AutoReverse } effectiveDuration = TimeSpan.FromTicks((long)(presentAndFutureDuration / _appliedSpeedRatio + 0.5)) + previousIterationDuration; } } else if (repeatBehavior.HasDuration) { effectiveDuration = repeatBehavior.Duration; } else // Repeat behavior is Forever { Debug.Assert(repeatBehavior == RepeatBehavior.Forever); // Only other valid enum value effectiveDuration = null; } return effectiveDuration; } // Run new eventing logic on root nodes // Note that Completed events are fired from a different place (currently, ComputeCurrentState) private void ComputeEvents(TimeSpan? expirationTime, TimeIntervalCollection parentIntervalCollection) { // We clear CurrentIntervals here in case that we don't reinitialize it in the method. // Unless there is something to project, we assume the null state ClearCurrentIntervalsToNull(); if (_beginTime.HasValue // If we changed to a paused state during this tick then we still // changed state and the events should be computed. // If we were paused and we resumed during this tick, even though // we are now active, no progress has been made during this tick. && !(IsInteractivelyPaused ^ PauseStateChangedDuringTick)) { Duration postFillDuration; // This is Zero when we have no fill zone if (expirationTime.HasValue) { postFillDuration = Duration.Forever; } else { postFillDuration = TimeSpan.Zero; // There is no reachable postfill zone because the active period is infinite } // if (!expirationTime.HasValue // If activePeriod extends forever, || expirationTime >= _beginTime) // OR if activePeriod extends to or beyond _beginTime, { // Check for CurrentTimeInvalidated TimeIntervalCollection activePeriod; if (expirationTime.HasValue) { if (expirationTime == _beginTime) { activePeriod = TimeIntervalCollection.Empty; } else { activePeriod = TimeIntervalCollection.CreateClosedOpenInterval(_beginTime.Value, expirationTime.Value); } } else // expirationTime is infinity { activePeriod = TimeIntervalCollection.CreateInfiniteClosedInterval(_beginTime.Value); } // If we have an intersection between parent domain times and the interval over which we // change, our time was invalidated if (parentIntervalCollection.Intersects(activePeriod)) { ComputeIntervalsWithParentIntersection( parentIntervalCollection, activePeriod, expirationTime, postFillDuration); } else if (postFillDuration != TimeSpan.Zero && // Our active period is finite and we have fill behavior _timeline.FillBehavior == FillBehavior.HoldEnd) // Check for state changing between Filling and Stopped { ComputeIntervalsWithHoldEnd( parentIntervalCollection, expirationTime); } } } // It is important to launch this method at the end of ComputeEvents, because it checks // whether the StateInvalidated event had been raised earlier in this method. if (PendingInteractiveRemove) { RaiseRemoveRequestedForRoot(); RaiseCompletedForRoot(true); // At this time, we also want to raise the Completed event; // this code always runs during a tick. PendingInteractiveRemove = false; } } // Find end time that is defined by our repeat behavior. Null is used to represent // an infinite expiration time. private bool ComputeExpirationTime(out TimeSpan? expirationTime) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped || IsRoot); TimeSpan? effectiveDuration; // if (!_beginTime.HasValue) { Debug.Assert(!_currentIterationBeginTime.HasValue, "_currentIterationBeginTime should not have a value when _beginTime has no value."); expirationTime = null; return true; } Debug.Assert(_beginTime.HasValue); effectiveDuration = ComputeEffectiveDuration(); if (effectiveDuration.HasValue) { expirationTime = _beginTime + effectiveDuration; // Precaution against slipping at the last frame of media: don't permit the clock to finish this tick yet if (_syncData != null && _syncData.IsInSyncPeriod && !_syncData.SyncClockHasReachedEffectiveDuration) { expirationTime += TimeSpan.FromMilliseconds(50); // This compensation is roughly one frame of video } } else { expirationTime = null; // infinite expiration time } return false; // More state to compute } // Compute values for root children that reflect interactivity // We may modify SpeedRatio if the interactive SpeedRatio was changed private bool ComputeInteractiveValues() { bool exitEarly = false; Debug.Assert(IsRoot); // Check for a pending stop first. This allows us to exit early. if (PendingInteractiveStop) { PendingInteractiveStop = false; IsInteractivelyStopped = true; // If we are disabled, no other interactive state is kept _beginTime = null; _currentIterationBeginTime = null; if (CanGrow) { ResetSlipOnSubtree(); } // Process the state for the whole subtree; the events will later // be replaced with invalidation-style events PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) // { Clock current = subtree.Current; if (current._currentClockState != ClockState.Stopped) { current.ResetCachedStateToStopped(); current.RaiseCurrentStateInvalidated(); current.RaiseCurrentTimeInvalidated(); current.RaiseCurrentGlobalSpeedInvalidated(); } else { subtree.SkipSubtree(); } } } if (IsInteractivelyStopped) { // If we are disabled, no other interactive state is kept Debug.Assert(_beginTime == null); Debug.Assert(_currentClockState == ClockState.Stopped); ResetCachedStateToStopped(); InternalNextTickNeededTime = null; // Can't return here: still need to process pending pause and resume // which can be set independent of the current state of the clock. exitEarly = true; } else { // Clocks that are currently paused or have a pending seek, begin, or change to the // speed ratio need to adjust their begin time. AdjustBeginTime(); } // // If we were about to pause or resume, set flags accordingly. // This must be done after adjusting the begin time so that we don't // appear to pause one tick early. // if (PendingInteractivePause) { Debug.Assert(!IsInteractivelyPaused); // Enforce invariant: cannot be pausePending when already paused Debug.Assert(!PendingInteractiveResume); // Enforce invariant: cannot be both pause and resumePending PendingInteractivePause = false; RaiseCurrentGlobalSpeedInvalidated(); // Update paused state for the entire subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.IsInteractivelyPaused = true; subtree.Current.PauseStateChangedDuringTick = true; } } if (PendingInteractiveResume) { Debug.Assert(IsInteractivelyPaused); Debug.Assert(!PendingInteractivePause); // We will no longer have to do paused begin time adjustment PendingInteractiveResume = false; // During pause, our speed was zero. Unless we are filling, invalidate the speed. if (_currentClockState != ClockState.Filling) { RaiseCurrentGlobalSpeedInvalidated(); } // Update paused state for the entire subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.IsInteractivelyPaused = false; subtree.Current.PauseStateChangedDuringTick = true; } } return exitEarly; } private void ComputeIntervalsWithHoldEnd( TimeIntervalCollection parentIntervalCollection, TimeSpan? endOfActivePeriod) { Debug.Assert(endOfActivePeriod.HasValue); TimeIntervalCollection fillPeriod = TimeIntervalCollection.CreateInfiniteClosedInterval(endOfActivePeriod.Value); if (parentIntervalCollection.Intersects(fillPeriod)) // We enter or leave Fill period { TimeSpan relativeBeginTime = _currentIterationBeginTime.HasValue ? _currentIterationBeginTime.Value : _beginTime.Value; ComputeCurrentFillInterval(parentIntervalCollection, relativeBeginTime, endOfActivePeriod.Value, _currentDuration, _appliedSpeedRatio, _timeline.AccelerationRatio, _timeline.DecelerationRatio, _timeline.AutoReverse); if (parentIntervalCollection.IntersectsInverseOf(fillPeriod)) // ... and we don't intersect the Active period, so we must go in or out of the Stopped period. { RaiseCurrentStateInvalidated(); RaiseCurrentTimeInvalidated(); RaiseCurrentGlobalSpeedInvalidated(); AddNullPointToCurrentIntervals(); // Count the stopped state by projecting the null point. } } } private void ComputeIntervalsWithParentIntersection( TimeIntervalCollection parentIntervalCollection, TimeIntervalCollection activePeriod, TimeSpan? endOfActivePeriod, Duration postFillDuration) { // Make sure that our periodic function is aligned to the boundary of the current iteration, regardless of prior slip TimeSpan relativeBeginTime = _currentIterationBeginTime.HasValue ? _currentIterationBeginTime.Value : _beginTime.Value; RaiseCurrentTimeInvalidated(); // Check for state changing between Active and the union of (Filling, Stopped) if (parentIntervalCollection.IntersectsInverseOf(activePeriod)) { RaiseCurrentStateInvalidated(); RaiseCurrentGlobalSpeedInvalidated(); } else if (parentIntervalCollection.IntersectsPeriodicCollection( relativeBeginTime, _currentDuration, _appliedSpeedRatio, _timeline.AccelerationRatio, _timeline.DecelerationRatio, _timeline.AutoReverse)) // Else we were always inside the active period, check for non-linear speed invalidations { RaiseCurrentGlobalSpeedInvalidated(); } // Else our speed has not changed, but our iteration may have been invalidated else if (parentIntervalCollection.IntersectsMultiplePeriods( relativeBeginTime, _currentDuration, _appliedSpeedRatio)) { HasDiscontinuousTimeMovementOccured = true; if (_syncData != null) { _syncData.SyncClockDiscontinuousEvent = true; // Notify the syncing node of discontinuity } } // Compute our output intervals ComputeCurrentIntervals(parentIntervalCollection, relativeBeginTime, endOfActivePeriod, postFillDuration, _currentDuration, _appliedSpeedRatio, _timeline.AccelerationRatio, _timeline.DecelerationRatio, _timeline.AutoReverse); } /// /// GillesK: performTickOperations means that we process the pending events /// private void ComputeLocalStateHelper(bool performTickOperations, bool seekedAlignedToLastTick) { Debug.Assert(!IsTimeManager); TimeSpan? parentTime; // Computed parent-local time TimeSpan? expirationTime; // Computed expiration time with respect to repeat behavior and resolved duration; TimeSpan localProgress; // Computed time inside simple duration TimeSpan parentTimeValue; double? parentSpeed; // Parent's CurrentGlobalSpeed TimeIntervalCollection parentIntervalCollection; double localSpeed; bool returnDelayed = false; // // In this function, 'true' return values allow us to exit early // We first compute parent parameters; with SlipBehavior, we may modify our parentIntervalCollection if (ComputeParentParameters(out parentTime, out parentSpeed, out parentIntervalCollection, seekedAlignedToLastTick)) { returnDelayed = true; } // Now take potential SlipBehavior into account: if (_syncData != null && _syncData.IsInSyncPeriod && _parent.CurrentState != ClockState.Stopped) // We are already in a slip zone { Debug.Assert(parentTime.HasValue); // If parent isn't stopped, it must have valid time and speed Debug.Assert(parentSpeed.HasValue); // ComputeSyncSlip(ref parentIntervalCollection, parentTime.Value, parentSpeed.Value); } ResolveDuration(); // We only calculate the Interactive values when we are processing the pending events if (performTickOperations && IsRoot) { // Special case for root-children, which may have interactivity if (ComputeInteractiveValues()) { returnDelayed = true; } } // Check whether we are entering a [....] period. This includes cases when we have // ticked before the beginning, then past the end of a [....] period; we still have to // move back to the exact beginning of the tick period. We handle cases where // we seek (HasSeekOccuredAfterLastTick) in a special way, by not synchronizing with the beginning. // Also, if the parent has been paused prior to this tick, we cannot enter the [....] zone, so skip the call. if (_syncData != null && !_syncData.IsInSyncPeriod && _parent.CurrentState != ClockState.Stopped && (!parentIntervalCollection.IsEmptyOfRealPoints || HasSeekOccuredAfterLastTick)) { Debug.Assert(parentTime.HasValue); // Cannot be true unless parent is stopped // We use the parent's TIC as a way of determining its earliest non-null time ComputeSyncEnter(ref parentIntervalCollection, parentTime.Value); } if (ComputeExpirationTime(out expirationTime)) { returnDelayed = true; } // Run the eventing logic here if (performTickOperations) { ComputeEvents(expirationTime, parentIntervalCollection); } if (returnDelayed) // If we delayed returning until now, proceed to do so { return; } Debug.Assert(_beginTime.HasValue); Debug.Assert(parentTime.HasValue); parentTimeValue = parentTime.Value; // Determines the next time we need to tick if (ComputeNextTickNeededTime(expirationTime, parentTimeValue, parentSpeed.Value)) { return; } // Determines if we are active, filling, or off if (ComputeCurrentState(expirationTime, ref parentTimeValue, parentSpeed.Value, performTickOperations)) { return; } // Determines the current iteration if (ComputeCurrentIteration(parentTimeValue, parentSpeed.Value, expirationTime, out localProgress)) { return; } // Determines the current time and local speed with accel+decel if (ComputeCurrentTime(localProgress, out localSpeed)) { return; } // Determines the current speed if (ComputeCurrentSpeed(localSpeed)) { return; } } // // Determine the next needed tick time for approaching a StateInvalidated boundary // // Note that ComputeCurrentIteration and ComputeCurrentState also modify the // NextTickNeededTime and consequently must run after this method. // private bool ComputeNextTickNeededTime(TimeSpan? expirationTime, TimeSpan parentTime, double parentSpeed) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped); Debug.Assert(_parent._currentClockState != ClockState.Stopped); Debug.Assert(_beginTime.HasValue); if (parentSpeed == 0) { // InternalNextTickNeededTime = IsInteractivelyPaused ? TimeSpan.Zero : (TimeSpan?)null; } else { double invertedParentSpeed = 1.0 / parentSpeed; TimeSpan? timeUntilNextBoundary = null; // // Calculate the time in ms until begin or expiration time. // They are positive if we're heading towards one of these periods, negative if heading away. // This takes into account reversing clocks (so a clock heading back to begin will have // a positive timeUntilBegin). // // timeUntilNextBoundary will be the first of these three boundaries that we hit. // Negative values are obviously ignored. // TimeSpan timeUntilBegin = MultiplyTimeSpan(_beginTime.Value - parentTime, invertedParentSpeed); // // If the time until a boundary is 0 (i.e. we've ticked exactly on a boundary) // we'll ask for another tick immediately. // This is only relevant for reversing clocks, which, when on a boundary, are defined // to have the 'previous' state, not the 'next' state. Thus they need one more // tick for the state change to happen. // if (timeUntilBegin >= TimeSpan.Zero) { timeUntilNextBoundary = timeUntilBegin; } if (expirationTime.HasValue) { TimeSpan timeUntilExpiration = MultiplyTimeSpan(expirationTime.Value - parentTime, invertedParentSpeed); if (timeUntilExpiration >= TimeSpan.Zero && (!timeUntilNextBoundary.HasValue || timeUntilExpiration < timeUntilNextBoundary.Value)) { timeUntilNextBoundary = timeUntilExpiration; } } // // Set the next tick needed time depending on whether we're // headed towards a boundary. // if (timeUntilNextBoundary.HasValue) { // We are moving towards some ClockState boundary either begin or expiration InternalNextTickNeededTime = CurrentGlobalTime + timeUntilNextBoundary; } else { // We are not moving towards any boundary points InternalNextTickNeededTime = null; } } return false; } // Compute the parent's time; by the time we reach this method, we know that we // have a non-root parent. private bool ComputeParentParameters(out TimeSpan? parentTime, out double? parentSpeed, out TimeIntervalCollection parentIntervalCollection, bool seekedAlignedToLastTick) { Debug.Assert(!IsTimeManager); Debug.Assert(!IsInteractivelyStopped || IsRoot); if (IsRoot) // We are a root child, use time manager time { Debug.Assert(_rootData != null, "A root Clock must have the _rootData structure initialized."); HasSeekOccuredAfterLastTick = seekedAlignedToLastTick || (_rootData.PendingSeekDestination != null); // We may have a seek request pending // We don't have a TimeManager that is on, so we are off, nothing more to compute if (_timeManager == null || _timeManager.InternalIsStopped) { ResetCachedStateToStopped(); parentTime = null; // Assign parentTime to avoid compiler error parentSpeed = null; InternalNextTickNeededTime = TimeSpan.Zero; // When TimeManager wakes up, we will need an update parentIntervalCollection = TimeIntervalCollection.Empty; return true; } else // We have a valid global time; { parentSpeed = 1.0; // TimeManager defines the rate at which time moves, e.g. it moves at 1X speed parentIntervalCollection = _timeManager.InternalCurrentIntervals; if (HasDesiredFrameRate) { // Change the parent's interval collection to include all time intervals since the last time // we ticked this root node. Due to DFR, we may have skipped a number of "important" ticks. parentTime = _rootData.CurrentAdjustedGlobalTime; if (!parentIntervalCollection.IsEmptyOfRealPoints) { // parentIntervalCollection = parentIntervalCollection.SetBeginningOfConnectedInterval( _rootData.LastAdjustedGlobalTime); } } else { parentTime = _timeManager.InternalCurrentGlobalTime; } return false; } } else // We are a deeper node { HasSeekOccuredAfterLastTick = seekedAlignedToLastTick || _parent.HasSeekOccuredAfterLastTick; // We may have a seek request pending parentTime = _parent._currentTime; // This is Null if parent is off; we still init the 'out' parameter parentSpeed = _parent._currentGlobalSpeed; parentIntervalCollection = _parent.CurrentIntervals; // Find the parent's current time if (_parent._currentClockState != ClockState.Stopped) // We have a parent that is active or filling { return false; } else // Else parent is off, so we are off, nothing more to compute { // Before setting our state to Stopped make sure that we // fire the proper event if we change state. if (_currentClockState != ClockState.Stopped) { RaiseCurrentStateInvalidated(); RaiseCurrentGlobalSpeedInvalidated(); RaiseCurrentTimeInvalidated(); } ResetCachedStateToStopped(); InternalNextTickNeededTime = null; return true; } } } // Abbreviations for variables expressing time units: // PT: Parent time (e.g. our begin time is expressed in parent time coordinates) // LT: Local time (e.g. our duration is expressed in local time coordinates) // ST: [....] time -- this is the same as local time iff (_syncData.SyncClock == this) e.g. we are the [....] clock, // otherwise it is our child's time coordinates (this happens when we are a container with SlipBehavior.Slip). // SPT: The [....] clock's parent's time coordinates. When this IS the [....] clock, this is our parent coordinates. // otherwise, it is our local coordinates. private void ComputeSyncEnter(ref TimeIntervalCollection parentIntervalCollection, TimeSpan currentParentTimePT) { Debug.Assert(!IsTimeManager); Debug.Assert(_parent != null); Debug.Assert(_parent.CurrentState != ClockState.Stopped); // Parent is not stopped, so its TIC cannot be empty Debug.Assert(HasSeekOccuredAfterLastTick || (!parentIntervalCollection.IsEmptyOfRealPoints && parentIntervalCollection.FirstNodeTime <= currentParentTimePT)); // SyncData points to our child if we have SlipBehavior, for CanSlip nodes it points to the node itself Debug.Assert(_syncData.SyncClock == this || _syncData.SyncClock._parent == this); Debug.Assert(CanSlip || _timeline is ParallelTimeline && ((ParallelTimeline)_timeline).SlipBehavior == SlipBehavior.Slip); Debug.Assert(_syncData != null); Debug.Assert(!_syncData.IsInSyncPeriod); // Verify our limitations on slip functionality, but don't throw here for perf Debug.Assert(_timeline.AutoReverse == false); Debug.Assert(_timeline.AccelerationRatio == 0); Debug.Assert(_timeline.DecelerationRatio == 0); // With these limitations, we can easily preview our CurrentTime: if (_beginTime.HasValue && currentParentTimePT >= _beginTime.Value) { TimeSpan relativeBeginTimePT = _currentIterationBeginTime.HasValue ? _currentIterationBeginTime.Value : _beginTime.Value; TimeSpan previewCurrentOffsetPT = currentParentTimePT - relativeBeginTimePT; // This is our time offset (not yet scaled by speed) TimeSpan previewCurrentTimeLT = MultiplyTimeSpan(previewCurrentOffsetPT, _appliedSpeedRatio); // This is what our time would be // We can only enter [....] period if we are past the syncClock's begin time if (_syncData.SyncClock == this || previewCurrentTimeLT >= _syncData.SyncClockBeginTime) { // We have two very different scenarios: seek and non-seek enter if (HasSeekOccuredAfterLastTick) // We have seeked, see if we fell into a [....] period { // If we haven't returned yet, we are not past the end of the [....] period on the child // Also, we are not Stopped prior to BeginTime. TimeSpan? expirationTimePT; ComputeExpirationTime(out expirationTimePT); // This is to verify we did not seek past our active period duration if (!expirationTimePT.HasValue || currentParentTimePT < expirationTimePT.Value) { TimeSpan ourSyncTimeST = (_syncData.SyncClock == this) ? previewCurrentTimeLT : MultiplyTimeSpan(previewCurrentTimeLT - _syncData.SyncClockBeginTime, _syncData.SyncClockSpeedRatio); TimeSpan? syncClockEffectiveDurationST = _syncData.SyncClockEffectiveDuration; if (_syncData.SyncClock == this || !syncClockEffectiveDurationST.HasValue || ourSyncTimeST < syncClockEffectiveDurationST) { // If the [....] child has a specified duration Duration syncClockDuration = _syncData.SyncClockResolvedDuration; if (syncClockDuration.HasTimeSpan) { _syncData.PreviousSyncClockTime = TimeSpan.FromTicks(ourSyncTimeST.Ticks % syncClockDuration.TimeSpan.Ticks); _syncData.PreviousRepeatTime = ourSyncTimeST - _syncData.PreviousSyncClockTime; } else if (syncClockDuration == Duration.Forever) { _syncData.PreviousSyncClockTime = ourSyncTimeST; _syncData.PreviousRepeatTime = TimeSpan.Zero; } else { Debug.Assert(syncClockDuration == Duration.Automatic); // If we seek into an Automatic syncChild's duration, we may overseek it, so throw an exception throw new InvalidOperationException(SR.Get(SRID.Timing_SeekDestinationAmbiguousDueToSlip)); } // This is the heart of the HasSeekOccuredAfterLastTick codepath; we don't adjust our // time, but note to do so for the succeeding ticks. _syncData.IsInSyncPeriod = true; } } } else // Non-seek, regular case { TimeSpan? previousSyncParentTimeSPT = (_syncData.SyncClock == this) ? parentIntervalCollection.FirstNodeTime : _currentTime; if (!previousSyncParentTimeSPT.HasValue || _syncData.SyncClockDiscontinuousEvent || previousSyncParentTimeSPT.Value <= _syncData.SyncClockBeginTime) // Not seeking this tick, different criteria for entering [....] period. // We don't care if we overshot the beginTime, because we will seek backwards // to match the child's beginTime exactly. // NOTE: _currentTime is actually our time at last tick, since it wasn't yet updated. { // First, adjust our beginTime so that we match the syncClock's begin, accounting for SpeedRatio TimeSpan timeIntoSyncPeriodPT = previewCurrentOffsetPT; if (_syncData.SyncClock != this) // SyncClock is our child; account for SyncClock starting later than us { timeIntoSyncPeriodPT -= DivideTimeSpan(_syncData.SyncClockBeginTime, _appliedSpeedRatio); } // Offset our position to [....] with media begin if (_currentIterationBeginTime.HasValue) { _currentIterationBeginTime += timeIntoSyncPeriodPT; } else { _beginTime += timeIntoSyncPeriodPT; } // UpdateSyncBeginTime(); // This ensures that our _cutoffTime is correctly applied // Now, update the parent TIC to compensate for our slip parentIntervalCollection = parentIntervalCollection.SlipBeginningOfConnectedInterval(timeIntoSyncPeriodPT); _syncData.IsInSyncPeriod = true; _syncData.PreviousSyncClockTime = TimeSpan.Zero; _syncData.PreviousRepeatTime = TimeSpan.Zero; _syncData.SyncClockDiscontinuousEvent = false; } } } } } // Abbreviations for variables expressing time units: // PT: Parent time (e.g. our begin time is expressed in parent time coordinates) // LT: Local time (e.g. our duration is expressed in local time coordinates) // ST: [....] time -- this is the same as local time iff (_syncData.SyncClock == this) e.g. we are the [....] clock, // otherwise it is our child's time coordinates (this happens when we are a container with SlipBehavior.Slip). // SPT: The [....] clock's parent's time coordinates. When this IS the [....] clock, this is our parent coordinates. // otherwise, it is our local coordinates. private void ComputeSyncSlip(ref TimeIntervalCollection parentIntervalCollection, TimeSpan currentParentTimePT, double currentParentSpeed) { Debug.Assert(!IsTimeManager); Debug.Assert(_parent != null); Debug.Assert(_syncData != null); Debug.Assert(_syncData.IsInSyncPeriod); // SyncData points to our child if we have SlipBehavior, for CanSlip nodes it points to the node itself Debug.Assert(_syncData.SyncClock == this || _syncData.SyncClock._parent == this); // The overriding assumption for slip limitations is that the parent's intervals are // "connected", e.g. not broken into multiple intervals. This is always true for roots. Debug.Assert(!parentIntervalCollection.IsEmpty); // The parent isn't Stopped, so it must have a TIC // From now on, we assume that the parent's TIC begins from the parent's CurrentTime at // at the previous tick. Ensure that the parent is moving forward. Debug.Assert(parentIntervalCollection.IsEmptyOfRealPoints || parentIntervalCollection.FirstNodeTime <= currentParentTimePT); Debug.Assert(currentParentSpeed >= 0); // We now extract this information from the TIC. If we are paused, we have an empty TIC and assume parent time has not changed. TimeSpan previousParentTimePT = parentIntervalCollection.IsEmptyOfRealPoints ? currentParentTimePT : parentIntervalCollection.FirstNodeTime; TimeSpan parentElapsedTimePT = currentParentTimePT - previousParentTimePT; // Our elapsed time is assumed to be a simple linear scale of the parent's time, // as long as we are inside of the [....] period. TimeSpan ourProjectedElapsedTimeLT = MultiplyTimeSpan(parentElapsedTimePT, _appliedSpeedRatio); TimeSpan syncTimeST = _syncData.SyncClock.GetCurrentTimeCore(); TimeSpan syncElapsedTimeST = syncTimeST - _syncData.PreviousSyncClockTime; // Elapsed from last tick if (syncElapsedTimeST > TimeSpan.Zero) // Only store the last value if it is greater than // the old value. Note we can use either >= or > here. { // Check whether [....] has reached the end of our effective duration TimeSpan? effectiveDurationST = _syncData.SyncClockEffectiveDuration; Duration syncDuration = _syncData.SyncClockResolvedDuration; if (effectiveDurationST.HasValue && (_syncData.PreviousRepeatTime + syncTimeST >= effectiveDurationST.Value)) { _syncData.IsInSyncPeriod = false; // This is the last time we need to [....] _syncData.PreviousRepeatTime = TimeSpan.Zero; _syncData.SyncClockDiscontinuousEvent = false; // Make sure we don't reenter the [....] period } // Else check if we should wrap the simple duration due to repeats, and set previous times accordingly else if (syncDuration.HasTimeSpan && syncTimeST >= syncDuration.TimeSpan) { // If we have a single repetition, then we would be done here; // However, we may just have reached the end of an iteration on repeating media; // In this case, we still [....] this particular moment, but we should reset the // previous [....] clock time to zero, and increment the PreviousRepeatTime. // This tick, media should pick up a corresponding DiscontinuousMovement caused // by a repeat, and reset itself to zero as well. _syncData.PreviousSyncClockTime = TimeSpan.Zero; _syncData.PreviousRepeatTime += syncDuration.TimeSpan; } else // Don't need to wrap around { _syncData.PreviousSyncClockTime = syncTimeST; } } else // If the [....] timeline went backwards, pretend it just didn't move. { syncElapsedTimeST = TimeSpan.Zero; } // Convert elapsed time to local coordinates, not necessarily same as [....] clock coordinates TimeSpan syncElapsedTimeLT = (_syncData.SyncClock == this) ? syncElapsedTimeST : DivideTimeSpan(syncElapsedTimeST, _syncData.SyncClockSpeedRatio); // This is the actual slip formula: how much is the slipping clock lagging behind? TimeSpan parentTimeSlipPT = parentElapsedTimePT - DivideTimeSpan(syncElapsedTimeLT, _appliedSpeedRatio); // NOTE: The above line does the same as this: // parentTimeSlip = syncSlip / _appliedSpeedRatio // ...but it maintains greater accuracy and prevents a bug where parentTimeSlip ends up 1 tick greater // that it should be, thus becoming larger than parentElapsedTimePT and causing us to suddenly fall out // of our [....] period. // Unless the media is exactly perfect, we will have non-zero slip time; we assume that it isn't // perfect, and always adjust our time accordingly. if (_currentIterationBeginTime.HasValue) { _currentIterationBeginTime += parentTimeSlipPT; } else { _beginTime += parentTimeSlipPT; } // UpdateSyncBeginTime(); parentIntervalCollection = parentIntervalCollection.SlipBeginningOfConnectedInterval(parentTimeSlipPT); return; } private void ResetSlipOnSubtree() { // Reset all children's slip time here; NOTE that this will change our effective duration, // but this should not become important until the next tick, when it will be recomputed anyway. PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, false); // No iteration at this node while (subtree.MoveNext()) { Clock current = subtree.Current; Debug.Assert(!current.IsRoot, "Root nodes never should reset their Slip amounts with ResetSlipOnSubtree(), even when seeking."); if (current._syncData != null) { current._syncData.IsInSyncPeriod = false; current._syncData.SyncClockDiscontinuousEvent = true; } if (current.CanSlip) { current._beginTime = current._timeline.BeginTime; // _beginTime could have slipped with media nodes current._currentIteration = null; current.UpdateSyncBeginTime(); current.HasDiscontinuousTimeMovementOccured = true; } else if (current.CanGrow) // If it's a repeating container with slippable descendants... { current._currentIterationBeginTime = current._beginTime; // ...reset its current iteration as well current._currentDuration = current._resolvedDuration; // Revert currentDuration back to default size } else // Otherwise we know not to traverse any further { subtree.SkipSubtree(); } } } #endregion // Local State Computation Helpers #region Event Helpers ////// Adds a delegate to the list of event handlers on this object. /// /// /// A unique identifier for the event handler. Since Clock events /// mirror Timeline events the callers of this method will pass in /// keys from Timeline /// /// The delegate to add private void AddEventHandler(EventPrivateKey key, Delegate handler) { Debug.Assert(!IsTimeManager); if (_eventHandlersStore == null) { _eventHandlersStore = new EventHandlersStore(); } _eventHandlersStore.Add(key, handler); VerifyNeedsTicksWhenActive(); } ////// Immediately fire the Loaded Event /// private void FireCompletedEvent() { FireEvent(Timeline.CompletedKey); } ////// Immediately fire the GlobalSpeedInvalidated Event /// private void FireCurrentGlobalSpeedInvalidatedEvent() { FireEvent(Timeline.CurrentGlobalSpeedInvalidatedKey); } ////// Immediately fire the State Invalidated Event /// private void FireCurrentStateInvalidatedEvent() { FireEvent(Timeline.CurrentStateInvalidatedKey); } ////// Immediately fire the CurrentTimeInvalidated Event /// private void FireCurrentTimeInvalidatedEvent() { FireEvent(Timeline.CurrentTimeInvalidatedKey); } ////// Fires the given event /// /// The unique key representing the event to fire private void FireEvent(EventPrivateKey key) { if (_eventHandlersStore != null) { EventHandler handler = (EventHandler)_eventHandlersStore.Get(key); if (handler != null) { handler(this, null); } } } ////// Immediately fire the RemoveRequested Event /// private void FireRemoveRequestedEvent() { FireEvent(Timeline.RemoveRequestedKey); } // Find the last closest time that falls on the boundary of the Desired Frame Rate private TimeSpan GetCurrentDesiredFrameTime(TimeSpan time) { return GetDesiredFrameTime(time, +0); } // Find the closest time that falls on the boundary of the Desired Frame Rate, advancing [frameOffset] frames forward private TimeSpan GetDesiredFrameTime(TimeSpan time, int frameOffset) { Debug.Assert(_rootData.DesiredFrameRate > 0); Int64 desiredFrameRate = _rootData.DesiredFrameRate; Int64 desiredFrameNumber = (time.Ticks * desiredFrameRate) / s_TimeSpanTicksPerSecond + frameOffset; Int64 desiredFrameTick = (desiredFrameNumber * s_TimeSpanTicksPerSecond) / desiredFrameRate; return TimeSpan.FromTicks(desiredFrameTick); } // Find the next closest time that falls on the boundary of the Desired Frame Rate private TimeSpan GetNextDesiredFrameTime(TimeSpan time) { return GetDesiredFrameTime(time, +1); } ////// Removes a delegate from the list of event handlers on this object. /// /// /// A unique identifier for the event handler. Since Clock events /// mirror Timeline events the callers of this method will pass in /// keys from Timeline /// /// The delegate to remove private void RemoveEventHandler(EventPrivateKey key, Delegate handler) { Debug.Assert(!IsTimeManager); if (_eventHandlersStore != null) { _eventHandlersStore.Remove(key, handler); if (_eventHandlersStore.Count == 0) { _eventHandlersStore = null; } } UpdateNeedsTicksWhenActive(); } #endregion // Event Helpers ////// Adds this clock to the time manager. /// private void AddToTimeManager() { Debug.Assert(!IsTimeManager); Debug.Assert(_parent == null); Debug.Assert(_timeManager == null); TimeManager timeManager = MediaContext.From(Dispatcher).TimeManager; if (timeManager == null) { // The time manager has not been created or has been released // This occurs when we are shutting down. Simply return. return; } _parent = timeManager.TimeManagerClock; SetTimeManager(_parent._timeManager); Int32? desiredFrameRate = Timeline.GetDesiredFrameRate(_timeline); if (desiredFrameRate.HasValue) { HasDesiredFrameRate = true; _rootData.DesiredFrameRate = desiredFrameRate.Value; } // Store this clock in the root clock's child collection _parent.InternalRootChildren.Add(WeakReference); // Create a finalizer object that will clean up after we are gone _subtreeFinalizer = new SubtreeFinalizer(_timeManager); // Fix up depth values PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { Clock current = subtree.Current; current._depth = current._parent._depth + 1; } // Perform any necessary updates if (IsInTimingTree) { // Mark the tree as dirty _timeManager.SetDirty(); } TimeIntervalCollection currentIntervals = TimeIntervalCollection.CreatePoint(_timeManager.InternalCurrentGlobalTime); currentIntervals.AddNullPoint(); _timeManager.InternalCurrentIntervals = currentIntervals; // // Recompute the local state at this subtree // // Since _beginTime for a root clock takes the current time into account, // it needs to be set during a tick. The call to ComputeLocalState // here isn't on a tick boundary, so we don't want to begin the clock yet. _beginTime = null; _currentIterationBeginTime = null; subtree.Reset(); while (subtree.MoveNext()) { subtree.Current.ComputeLocalState(); // Compute the state of the node subtree.Current.ClipNextTickByParent(); // Perform NextTick clipping, stage 1 // Make a note to visit for stage 2, only for ClockGroups subtree.Current.NeedsPostfixTraversal = (subtree.Current is ClockGroup); } _parent.ComputeTreeStateRoot(); // Re-clip the next tick estimates by children // Adding is an implicit begin, so do that here. This is done after // ComputeLocalState so that the begin will be picked up on the // next tick. Note that if _timeline.BeginTime == null we won't // start the clock. if (_timeline.BeginTime != null) { RootBeginPending = true; } NotifyNewEarliestFutureActivity(); // Make sure we get ticks } ////// Helper for more elegant code dividing a TimeSpan by a double /// private TimeSpan DivideTimeSpan(TimeSpan timeSpan, double factor) { Debug.Assert(factor != 0); // Divide by zero return TimeSpan.FromTicks((long)(((double)timeSpan.Ticks) / factor + 0.5)); } ////// Gets the value of a flag specified by the ClockFlags enum /// private bool GetFlag(ClockFlags flagMask) { return (_flags & flagMask) == flagMask; } ////// Helper for more elegant code multiplying a TimeSpan by a double /// private static TimeSpan MultiplyTimeSpan(TimeSpan timeSpan, double factor) { return TimeSpan.FromTicks((long)(factor * (double)timeSpan.Ticks + 0.5)); } private void NotifyNewEarliestFutureActivity() { PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); // First reset the children while (subtree.MoveNext()) { subtree.Current.InternalNextTickNeededTime = TimeSpan.Zero; } Clock current = _parent; // Propagate the fact that we will need an update sooner up the chain while (current != null && current.InternalNextTickNeededTime != TimeSpan.Zero) { current.InternalNextTickNeededTime = TimeSpan.Zero; if (current.IsTimeManager) // We went all the way up to the root node, notify TimeManager { _timeManager.NotifyNewEarliestFutureActivity(); break; } current = current._parent; } if (_timeManager != null) { // If we get here from within a Tick, this will force MediaContext to perform another subsequent Tick // on the TimeManager. This will apply the requested interactive operations, so their results will // immediately become visible. _timeManager.SetDirty(); } } // State that must remain *constant* outside of the active period private void ResetCachedStateToFilling() { _currentGlobalSpeed = 0; IsBackwardsProgressingGlobal = false; _currentClockState = ClockState.Filling; } ////// Calls RaiseCompleted on this subtree when called on a root. /// /// Whether we are in a tick. private void RaiseCompletedForRoot(bool isInTick) { // Only perform this operation from root nodes. Also, to avoid constantly calling Completed after passing // expirationTime, check that the state is invalidated, so we only raise the event as we are finishing. if (IsRoot && (CurrentStateInvalidatedEventRaised || !isInTick)) { // If we are a root node, notify the entire subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.RaiseCompleted(); } } } private void RaiseRemoveRequestedForRoot() { Debug.Assert(IsRoot); // This should only be called on root-child clocks // If we are a root node, notify the entire subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current.RaiseRemoveRequested(); } } ////// Sets a specified flag with the given value /// private void SetFlag(ClockFlags flagMask, bool value) { if (value) { _flags |= flagMask; } else { _flags &= ~(flagMask); } } ////// Sets the time manager for the subtree rooted at this timeline. /// /// /// The new time manager. /// private void SetTimeManager(TimeManager timeManager) { Debug.Assert(!IsTimeManager); // Optimize the no-op case away if (this._timeManager != timeManager) { // Set the new time manager for the whole subtree PrefixSubtreeEnumerator subtree = new PrefixSubtreeEnumerator(this, true); while (subtree.MoveNext()) { subtree.Current._timeManager = timeManager; } if (timeManager != null) { // If we are joining a new time manager, issue any deferred calls subtree.Reset(); while (subtree.MoveNext()) { Clock current = subtree.Current; // } } } } private void UpdateNeedsTicksWhenActive() { // Currently we'll set NeedsTicksWhenActive to true // if any of the three events are set on this clock. // if (_eventHandlersStore == null) { NeedsTicksWhenActive = false; } else { NeedsTicksWhenActive = true; } } // This wrapper is invoked anytime we invalidate the _beginTime private void UpdateSyncBeginTime() { if (_syncData != null) { _syncData.UpdateClockBeginTime(); } } private void VerifyNeedsTicksWhenActive() { if (!NeedsTicksWhenActive) // We may need to update the tree to know that we need ticks { NeedsTicksWhenActive = true; // Use this as a hint for NeedsTicksWhenActive NotifyNewEarliestFutureActivity(); } } #endregion // Private Methods // // Private Properties // #region Private Properties ////// True if we are in a running timing tree, false otherwise. /// private bool IsInTimingTree { get { Debug.Assert(!IsTimeManager); return (_timeManager != null) && (_timeManager.State != TimeState.Stopped); } } ////// This isn't an Interactive method: InternalBegin does /// not make use of this flag. It is set in AddToRoot() to /// notify a root clock that it must begin at the next tick. /// Until this is set_beginTime for a root clock will be null; /// AdjustBeginTime() sets _beginTime properly. /// private bool RootBeginPending { get { return GetFlag(ClockFlags.RootBeginPending); } set { SetFlag(ClockFlags.RootBeginPending, value); } } #endregion // Private Properties // // Nested Types // #region Nested Types [Flags] private enum ClockFlags : uint { IsTimeManager = 1 << 0, IsRoot = 1 << 1, IsBackwardsProgressingGlobal = 1 << 2, IsInteractivelyPaused = 1 << 3, IsInteractivelyStopped = 1 << 4, PendingInteractivePause = 1 << 5, PendingInteractiveResume = 1 << 6, PendingInteractiveStop = 1 << 7, PendingInteractiveRemove = 1 << 8, CanGrow = 1 << 9, CanSlip = 1 << 10, CurrentStateInvalidatedEventRaised = 1 << 11, CurrentTimeInvalidatedEventRaised = 1 << 12, CurrentGlobalSpeedInvalidatedEventRaised = 1 << 13, CompletedEventRaised = 1 << 14, RemoveRequestedEventRaised = 1 << 15, IsInEventQueue = 1 << 16, NeedsTicksWhenActive = 1 << 17, NeedsPostfixTraversal = 1 << 18, PauseStateChangedDuringTick = 1 << 19, RootBeginPending = 1 << 20, HasControllableRoot = 1 << 21, HasResolvedDuration = 1 << 22, HasDesiredFrameRate = 1 << 23, HasDiscontinuousTimeMovementOccured = 1 << 24, HasDescendantsWithUnresolvedDuration = 1 << 25, HasSeekOccuredAfterLastTick = 1 << 26, } ////// The result of a ResolveTimes method call. /// private enum ResolveCode { ////// Nothing changed in resolving new times. /// NoChanges, ////// Resolved times are different than before. /// NewTimes, ////// The children of this timeline need a full reset time resolution. This flag /// indicates that a partial resolution needs to be prunned at the current /// timeline in favor of a full resolution for its children. /// NeedNewChildResolve } ////// Implements a finalizer for a clock subtree. /// private class SubtreeFinalizer { ////// Creates a finalizer for a clock subtree. /// internal SubtreeFinalizer(TimeManager timeManager) { _timeManager = timeManager; } ////// Finalizes a clock subtree. /// ~SubtreeFinalizer() { #pragma warning disable 1634, 1691 #pragma warning suppress 6525 _timeManager.ScheduleClockCleanup(); } private TimeManager _timeManager; } ////// Holds [....]-related data when it is applicable /// internal class SyncData { internal SyncData(Clock syncClock) { Debug.Assert(syncClock != null); Debug.Assert(syncClock.GetCanSlip()); Debug.Assert(syncClock.IsRoot || syncClock._timeline.BeginTime.HasValue); // Only roots may later validate their _beginTime _syncClock = syncClock; UpdateClockBeginTime(); // This will update the remaining dependencies } // This method should be invoked anytime we invalidate the SyncClock's _beginTime only internal void UpdateClockBeginTime() { // _syncClockBeginTime = _syncClock._beginTime; _syncClockSpeedRatio = _syncClock._appliedSpeedRatio; _syncClockResolvedDuration = SyncClockResolvedDuration; // This is to detect media finding its true duration } internal Clock SyncClock { get { return _syncClock; } } internal Duration SyncClockResolvedDuration { get { // Duration can only change its value while it is Automatic if (!_syncClockResolvedDuration.HasTimeSpan) { _syncClockEffectiveDuration = _syncClock.ComputeEffectiveDuration(); // null == infinity _syncClockResolvedDuration = _syncClock._resolvedDuration; } return _syncClockResolvedDuration; } } internal bool SyncClockHasReachedEffectiveDuration { get { if (_syncClockEffectiveDuration.HasValue) // If the [....] clock has a finite duration { return (_previousRepeatTime + _syncClock.GetCurrentTimeCore() >= _syncClockEffectiveDuration.Value); } else { return false; } } } // NOTE: This value is in SyncNode coordinates, not its parent's coordinates internal TimeSpan? SyncClockEffectiveDuration { get { return _syncClockEffectiveDuration; } } internal double SyncClockSpeedRatio { get { return _syncClockSpeedRatio; } } internal bool IsInSyncPeriod { get { return _isInSyncPeriod; } set { _isInSyncPeriod = value; } } internal bool SyncClockDiscontinuousEvent { get { return _syncClockDiscontinuousEvent; } set { _syncClockDiscontinuousEvent = value; } } internal TimeSpan PreviousSyncClockTime { get { return _previousSyncClockTime; } set { _previousSyncClockTime = value; } } internal TimeSpan PreviousRepeatTime { get { return _previousRepeatTime; } set { _previousRepeatTime = value; } } internal TimeSpan SyncClockBeginTime { get { Debug.Assert(_syncClockBeginTime.HasValue); // This should never be queried on a root without beginTime return _syncClockBeginTime.Value; } } private Clock _syncClock; private double _syncClockSpeedRatio; private bool _isInSyncPeriod, _syncClockDiscontinuousEvent; private Duration _syncClockResolvedDuration = Duration.Automatic; // Duration -- *local* coordinates private TimeSpan? _syncClockEffectiveDuration; // This reflects RepeatBehavior (local coordinates) private TimeSpan? _syncClockBeginTime; private TimeSpan _previousSyncClockTime; private TimeSpan _previousRepeatTime; // How many complete iterations we already have done } ////// Holds data specific to root clocks. /// internal class RootData { internal RootData() { } internal TimeSpan CurrentAdjustedGlobalTime { get { return _currentAdjustedGlobalTime; } set { _currentAdjustedGlobalTime = value; } } internal Int32 DesiredFrameRate { get { return _desiredFrameRate; } set { _desiredFrameRate = value; } } internal Double InteractiveSpeedRatio { get { return _interactiveSpeedRatio; } set { _interactiveSpeedRatio = value; } } internal TimeSpan LastAdjustedGlobalTime { get { return _lastAdjustedGlobalTime; } set { _lastAdjustedGlobalTime = value; } } ////// The time to which we want to seek this timeline at the next tick /// internal TimeSpan? PendingSeekDestination { get { return _pendingSeekDestination; } set { _pendingSeekDestination = value; } } internal Double? PendingSpeedRatio { get { return _pendingSpeedRatio; } set { _pendingSpeedRatio = value; } } private Int32 _desiredFrameRate; private double _interactiveSpeedRatio = 1.0; private double? _pendingSpeedRatio; private TimeSpan _currentAdjustedGlobalTime; private TimeSpan _lastAdjustedGlobalTime; private TimeSpan? _pendingSeekDestination; } #endregion // Nested Types // // Debugging Instrumentation // #region Debugging instrumentation ////// Debug-only method to verify that the recomputed input time /// is close to the original. See ComputeCurrentIteration /// /// original input time /// input time without rounding errors [Conditional("DEBUG")] private void Debug_VerifyOffsetFromBegin(long inputTime, long optimizedInputTime) { long error = (long)Math.Max(_appliedSpeedRatio, 1.0); // Assert the computed inputTime is very close to the original. // _appliedSpeedRatio is the upper bound of the error (in Ticks) caused by the // calculation of inputTime. The reason is that we truncate (floor) during the // computation of EffectiveDuration, losing up to 0.99999.... off the number. // The computation of inputTime multiplies the truncated value by _appliedSpeedRatio, // so _appliedSpeedRatio is guaranteed to be close to but higher than the actual error. Debug.Assert(Math.Abs(optimizedInputTime - inputTime) <= error, "This optimized inputTime does not match the original - did the calculation of inputTime change?"); } #if DEBUG ////// Dumps the description of the subtree rooted at this clock. /// internal void Dump() { System.Text.StringBuilder builder = new System.Text.StringBuilder(); builder.Capacity = 1024; builder.Append("======================================================================\n"); builder.Append("Clocks rooted at Clock "); builder.Append(_debugIdentity); builder.Append('\n'); builder.Append("----------------------------------------------------------------------\n"); builder.Append("Flags LastBegin LastEnd NextBegin NextEnd Name\n"); builder.Append("----------------------------------------------------------------------\n"); if (IsTimeManager) { RootBuildInfoRecursive(builder); } else { BuildInfoRecursive(builder, 0); } builder.Append("----------------------------------------------------------------------\n"); Trace.Write(builder.ToString()); } ////// Dumps the description of all clocks in the known clock table. /// internal static void DumpAll() { System.Text.StringBuilder builder = new System.Text.StringBuilder(); int clockCount = 0; builder.Capacity = 1024; builder.Append("======================================================================\n"); builder.Append("Clocks in the GC heap\n"); builder.Append("----------------------------------------------------------------------\n"); builder.Append("Flags LastBegin LastEnd NextBegin NextEnd Name\n"); builder.Append("----------------------------------------------------------------------\n"); lock (_debugLockObject) { if (_objectTable.Count > 0) { // Output the clocks sorted by ID int[] idTable = new int[_objectTable.Count]; _objectTable.Keys.CopyTo(idTable, 0); Array.Sort(idTable); for (int index = 0; index < idTable.Length; index++) { WeakReference weakRef = (WeakReference)_objectTable[idTable[index]]; Clock clock = (Clock)weakRef.Target; if (clock != null) { clock.BuildInfo(builder, 0, true); clockCount++; } } } } if (clockCount == 0) { builder.Append("There are no Clocks in the GC heap.\n"); } builder.Append("----------------------------------------------------------------------\n"); Trace.Write(builder.ToString()); } ////// Dumps the description of the subtree rooted at this clock. /// /// /// A StringBuilder that accumulates the description text. /// /// /// The depth of recursion for this clock. /// // Normally a virtual method would be implemented for the dervied class // to handle the children property, but the asmmeta would be different // since this is only availabe in a debug build. For this reason we're // handling the children in the base class. internal void BuildInfoRecursive(System.Text.StringBuilder builder, int depth) { // Add the info for this clock BuildInfo(builder, depth, false); // Recurse into the children ClockGroup thisGroup = this as ClockGroup; if (thisGroup != null) { depth++; Listchildren = thisGroup.InternalChildren; if (children != null) { for (int childIndex = 0; childIndex < children.Count; childIndex++) { children[childIndex].BuildInfoRecursive(builder, depth); } } } } /// /// Dumps the description of the subtree rooted at this root clock. /// /// /// A StringBuilder that accumulates the description text. /// internal void RootBuildInfoRecursive(System.Text.StringBuilder builder) { // Add the info for this clock BuildInfo(builder, 0, false); // Recurse into the children. Don't use the enumerator because // that would remove dead references, which would be an undesirable // side-effect for a debug output method. Listchildren = ((ClockGroup) this).InternalRootChildren; for (int index = 0; index < children.Count; index++) { Clock child = (Clock)children[index].Target; if (child != null) { child.BuildInfoRecursive(builder, 1); } } } /// /// Dumps the description of this clock. /// /// /// A StringBuilder that accumulates the description text. /// /// /// The depth of recursion for this clock. /// /// /// True to dump the ID of the parent clock, false otherwise. /// internal void BuildInfo(System.Text.StringBuilder builder, int depth, bool includeParentID) { builder.Append(depth); builder.Append(GetType().Name); builder.Append(' '); builder.Append(_debugIdentity); builder.Append(' '); _timeline.BuildInfo(builder, 0, false); } ////// Finds a previously registered object. /// /// /// The ID of the object to look for /// ////// The object if found, null otherwise. /// internal static Clock Find(int id) { Clock clock = null; lock (_debugLockObject) { object handleReference = _objectTable[id]; if (handleReference != null) { WeakReference weakRef = (WeakReference)handleReference; clock = (Clock)weakRef.Target; if (clock == null) { // Object has been destroyed, so remove the weakRef. _objectTable.Remove(id); } } } return clock; } ////// Cleans up the known timeline clocks table by removing dead weak /// references. /// internal static void CleanKnownClocksTable() { lock (_debugLockObject) { Hashtable removeTable = new Hashtable(); // Identify dead references foreach (DictionaryEntry e in _objectTable) { WeakReference weakRef = (WeakReference) e.Value; if (weakRef.Target == null) { removeTable[e.Key] = weakRef; } } // Remove dead references foreach (DictionaryEntry e in removeTable) { _objectTable.Remove(e.Key); } } } #endif // DEBUG #endregion // Debugging instrumentation // // Data // #region Data private ClockFlags _flags; private int? _currentIteration; // Precalculated current iteration private double? _currentProgress; // Precalculated current progress value private double? _currentGlobalSpeed; // Precalculated current global speed value private TimeSpan? _currentTime; // Precalculated current global time private ClockState _currentClockState; // Precalculated current clock state private RootData _rootData = null; // Keeps track of root-related data for DesiredFrameRate internal SyncData _syncData = null; // Keeps track of [....]-related data for SlipBehavior // Stores the clock's begin time as an offset from the clock's // parent's begin time (i.e. a begin time of 2 sec means begin // 2 seconds afer the parent starts). For non-root clocks this // is always the same as its timeline's begin time. // For root clocks, the begin time is adjusted in response to seeks, // pauses, etc (see AdjustBeginTime()) // // This must be null when the clock is stopped. internal TimeSpan? _beginTime; // This is only used for repeating timelines which have CanSlip children/descendants; // In this case, we use this variable instead of _beginTime to compute the current state // of the clock (in conjunction with _currentIteration, so we know how many we have // already completed.) This makes us agnostic to slip/growth in our past iterations. private TimeSpan? _currentIterationBeginTime; // How soon this Clock needs another tick internal TimeSpan? _nextTickNeededTime = null; private WeakReference _weakReference; private SubtreeFinalizer _subtreeFinalizer; private EventHandlersStore _eventHandlersStore; ////// Cache Duration for perf reasons and also to accommodate one time /// only resolution of natural duration. /// If Timeline.Duration is Duration.Automatic, we will at first treat /// this as Duration.Forever, but will periodically ask what the resolved /// duration is. Once we receive an answer, that will be the duration /// used from then on, and we won't ask again. /// Otherwise, this will be set to Timeline.Duration when the Clock is /// created and won't change. /// internal Duration _resolvedDuration; ////// This is a cached estimated duration of the current iteration of this clock. /// For clocks which return false to CanGrow, this should always equal the /// _resolvedDuration. However, for clocks with CanGrow, this may change from /// tick to tick. Based on how far we are in the present iteration, and how /// on track our slipping children are, we make estimates of when we will finish /// the iteration, which are reflected by our estimated _currentDuration. /// internal Duration _currentDuration; ////// For a root, this is the Timeline's speed ratio multiplied by the /// interactive speed ratio. It's updated whenever the interactive speed /// ratio is updated. If the interactive speed ratio is 0, we'll use the /// Timeline's speed ratio, that is, treat interactive speed ratio as 1. /// This is why this is "applied" speed ratio. /// /// For a non-root, this is just a cache for the Timeline's speed ratio. /// private Double _appliedSpeedRatio; #region Linking data internal Timeline _timeline; internal TimeManager _timeManager; internal ClockGroup _parent; // Parents can only be ClockGroups since // they're the only ones having children internal int _childIndex; internal int _depth; static Int64 s_TimeSpanTicksPerSecond = TimeSpan.FromSeconds(1).Ticks; #endregion // Linking data #region Debug data #if DEBUG internal int _debugIdentity; internal static int _nextIdentity; internal static Hashtable _objectTable = new Hashtable(); internal static object _debugLockObject = new object(); #endif // DEBUG #endregion // Debug data #endregion // Data } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- IUnknownConstantAttribute.cs
- SendKeys.cs
- ToolStripRenderer.cs
- DataTableReader.cs
- ExtensionQuery.cs
- ListViewInsertedEventArgs.cs
- RoutedEventValueSerializer.cs
- DesignTimeParseData.cs
- BinaryObjectInfo.cs
- CodeNamespace.cs
- Decimal.cs
- StringBuilder.cs
- Expressions.cs
- ConnectionStringsExpressionEditor.cs
- EncoderBestFitFallback.cs
- EventlogProvider.cs
- TextRunProperties.cs
- FrameworkTextComposition.cs
- RadioButtonAutomationPeer.cs
- XmlSchemaElement.cs
- ButtonBase.cs
- DocumentViewerHelper.cs
- HtmlTextArea.cs
- DefaultPropertiesToSend.cs
- CompiledQuery.cs
- NumberFunctions.cs
- StylusPointProperty.cs
- AvtEvent.cs
- ImageUrlEditor.cs
- KnownBoxes.cs
- SettingsBindableAttribute.cs
- Constraint.cs
- CodeIterationStatement.cs
- CompositeScriptReferenceEventArgs.cs
- Win32.cs
- ICspAsymmetricAlgorithm.cs
- ImageFormatConverter.cs
- Parameter.cs
- ImageProxy.cs
- WrappedKeySecurityTokenParameters.cs
- MediaSystem.cs
- CompressStream.cs
- DataGridViewCellStyleChangedEventArgs.cs
- TextTreeRootTextBlock.cs
- TextFormatterContext.cs
- SafeBitVector32.cs
- Splitter.cs
- PreProcessor.cs
- SqlRowUpdatedEvent.cs
- DataRowCollection.cs
- IncomingWebRequestContext.cs
- ParagraphResult.cs
- TemplateInstanceAttribute.cs
- WebPartDeleteVerb.cs
- DataRowView.cs
- ClientRolePrincipal.cs
- AsyncResult.cs
- MsmqInputMessagePool.cs
- HttpWriter.cs
- versioninfo.cs
- BackgroundFormatInfo.cs
- Single.cs
- SimplePropertyEntry.cs
- QfeChecker.cs
- Model3D.cs
- DependencyObject.cs
- DoubleCollectionValueSerializer.cs
- ProxyWebPartConnectionCollection.cs
- DoubleAnimationUsingPath.cs
- EdmFunction.cs
- ResourceKey.cs
- CompositionAdorner.cs
- MatrixCamera.cs
- UnsafeCollabNativeMethods.cs
- SQLDoubleStorage.cs
- IUnknownConstantAttribute.cs
- MailAddressCollection.cs
- MailWriter.cs
- ParameterModifier.cs
- QueryExpression.cs
- BorderGapMaskConverter.cs
- OneWayElement.cs
- EndpointNotFoundException.cs
- LinearGradientBrush.cs
- ping.cs
- odbcmetadatacollectionnames.cs
- DataBindingsDialog.cs
- XPathSelfQuery.cs
- indexingfiltermarshaler.cs
- SponsorHelper.cs
- MediaScriptCommandRoutedEventArgs.cs
- FrameworkEventSource.cs
- WebControlParameterProxy.cs
- XpsFixedDocumentReaderWriter.cs
- TranslateTransform.cs
- BindingExpressionUncommonField.cs
- Facet.cs
- PersistenceProviderElement.cs
- ModuleElement.cs
- SettingsSavedEventArgs.cs