bool CanvasAnimatedControl::Tick()

in winrt/lib/xaml/CanvasAnimatedControl.cpp [995:1279]


bool CanvasAnimatedControl::Tick(
    CanvasSwapChain* swapChain, 
    bool areResourcesCreated)
{
    EventWrite_CanvasAnimatedControl_Tick_Start();
    auto tickEnd = MakeScopeWarden([] { EventWrite_CanvasAnimatedControl_Tick_Stop(); });

    RenderTarget* renderTarget = GetCurrentRenderTarget();

    if (IsSuspended())
        return false;

    if (!IsLoaded())
        return false;

    //
    // Access shared state that's shared between the UI thread and the
    // update/render thread.  This is done in one place in order to hold the
    // lock for as little time as possible.
    //

    Color clearColor;
    Size currentSize;
    float currentDpi;
    GetClearColorSizeAndDpi(&clearColor, &currentSize, &currentDpi);

    auto lock = Lock(m_sharedStateMutex);

    m_sharedState.SizeSeenByGameLoop = currentSize;
    m_sharedState.IsInTick = true;

    auto tickEnd2 = MakeScopeWarden(
        [&]
        {
            if (!lock.owns_lock())
                lock.lock();
            
            m_sharedState.IsInTick = false;
        });

    m_stepTimer.SetTargetElapsedTicks(m_sharedState.TargetElapsedTime);
    m_stepTimer.SetFixedTimeStep(m_sharedState.IsStepTimerFixedStep);

    if (m_sharedState.ShouldResetElapsedTime)
    {
        m_stepTimer.ResetElapsedTime();
    }

    bool deviceNeedsReCreationWithNewOptions = m_sharedState.DeviceNeedsReCreationWithNewOptions;
    m_sharedState.DeviceNeedsReCreationWithNewOptions = false;
    m_sharedState.ShouldResetElapsedTime = false;

    // If the opacity has changed then the swap chain will need to be
    // recreated before we can draw.

    if (renderTarget->Target &&
        GetAlphaModeFromClearColor(clearColor) != renderTarget->AlphaMode)
    {
        // This will cause the update/render thread to stop, giving the UI
        // thread an opportunity to recreate the swap chain.
        return false;
    }

    // If the device needs to be re-created with different options, this 
    // needs to happen before we can draw.
    if (deviceNeedsReCreationWithNewOptions)
    {
        return false;
    }

    bool forceDraw = m_sharedState.NeedsDraw;

    // At this point we know that we're going to handle the NeedsDraw (unless
    // there's some kind of failure) so we can reset this flag now.  This is
    // particularly relevant for the ClearColor, since this indicates that we've
    // 'consumed' that color.
    m_sharedState.NeedsDraw = false;

    bool invalidated = m_sharedState.Invalidated; 

    // If resources are created then we know that we'll be able to process the
    // Invalidated request, so we can reset this flag now.
    if (areResourcesCreated)
        m_sharedState.Invalidated = false;

    // Handle Paused state
    bool isPaused = m_sharedState.IsPaused;
    int64_t timeSpentPaused = 0;

    if (!isPaused)
    {
        timeSpentPaused = m_sharedState.TimeSpentPaused;

        if (areResourcesCreated)
        {
            m_sharedState.TimeWhenPausedWasSet = 0;
            m_sharedState.TimeSpentPaused = 0;
        }
    } 

    // We update, but forego drawing if the control is not in a visible state.
    // Force-draws, like those due to device lost, are not performed either.
    // Drawing behavior resumes when the control becomes visible once again.
    bool isVisible = IsVisible();

    //
    // Copy out the list of async actions, to avoid retaining the lock while
    // they are being fired.
    //
    // The async actions are only executed after resources have been created.
    // This means that:
    //
    // - the actions can assume that there's a valid device and
    // - there's no risk of them running concurrently with
    //   CreateResources.
    //
    std::vector<ComPtr<AnimatedControlAsyncAction>> pendingActions;
    
    if (areResourcesCreated)
        std::swap(pendingActions, m_sharedState.PendingAsyncActions);

    lock.unlock();

    //
    // Run any async actions
    //
    if (!pendingActions.empty())
    {
        IssueAsyncActions(pendingActions);

        // One of the async actions may have changed the shared state, in which case
        // we want to respond immediately.
        if (!invalidated)
        {
            auto lock2 = Lock(m_sharedStateMutex);
            
            invalidated = m_sharedState.Invalidated;
            if (areResourcesCreated)
                m_sharedState.Invalidated = false;
        }
    }

    //
    // Now do the update/render for this tick
    //

    UpdateResult updateResult{};

    EventWrite_CanvasAnimatedControl_Update_Start(areResourcesCreated, isPaused);
    if (areResourcesCreated && !isPaused)
    {
        bool forceUpdate = false;

        if (!m_hasUpdated)
        {
            // For the first update we reset the timer.  This handles the
            // possibility of there being a long delay between construction and
            // the first update.
            m_stepTimer.ResetElapsedTime();
            forceUpdate = true;
        }

        updateResult = Update(forceUpdate, timeSpentPaused);

        m_hasUpdated |= updateResult.Updated;
    }
    EventWrite_CanvasAnimatedControl_Update_Stop(updateResult.Updated);

    //
    // We only ever Draw/Present if an Update has actually happened.  This
    // results in us waiting until the next vblank to update.
    // This is desireable since using Present to wait for the vsync can
    // result in missed frames.
    //
    bool drew = false;
    if ((updateResult.Updated || forceDraw || invalidated) && isVisible)
    {
        bool zeroSizedTarget = currentSize.Width <= 0 || currentSize.Height <= 0;
        
        // A dpi change doesn't matter on a zero-sized target.
        bool dpiChangedOnNonZeroSizedTarget = renderTarget->Dpi != currentDpi && !zeroSizedTarget;

        bool sizeChanged = renderTarget->Size != currentSize;

        //
        // If the control's size or dpi has changed then the swapchain's buffers
        // need to be resized as appropriate.
        //
        if (sizeChanged || dpiChangedOnNonZeroSizedTarget)
        {
            if (zeroSizedTarget || !renderTarget->Target)
            {
                //
                // Switching between zero and non-zero sized rendertargets requires calling
                // CanvasSwapChainPanel::put_SwapChain, so must be done on the UI thread.
                // We must stop the update/render thread to allow this to happen.
                //
                return false;
            }
            else if (dpiChangedOnNonZeroSizedTarget)
            {
                //
                // A DPI change should stop and start the render thread, and
                // raise a CreateResources.
                //
                return false;
            }
            else
            {
                // This can be done on the update/render thread because:
                //
                //  - no XAML methods are called
                //
                //  - the current render target won't be updated by the UI thread
                //    while the update/render thread is running
                //
                ThrowIfFailed(renderTarget->Target->ResizeBuffersWithWidthAndHeightAndDpi(currentSize.Width, currentSize.Height, currentDpi));

                //
                // The size and dpi fields of the render target object represent the real, committed state of the render
                // target, while currentSize/currentDpi represent the thing last requested by the app.
                //
                renderTarget->Size = currentSize;
                renderTarget->Dpi = currentDpi;
            }
        }

        if (renderTarget->Target)
        {
            bool invokeDrawHandlers = (areResourcesCreated && (m_hasUpdated || invalidated));

            EventWrite_CanvasAnimatedControl_Draw_Start(invokeDrawHandlers, updateResult.IsRunningSlowly);
            Draw(renderTarget->Target.Get(), clearColor, invokeDrawHandlers, updateResult.IsRunningSlowly);
            EventWrite_CanvasAnimatedControl_Draw_Stop();
            EventWrite_CanvasAnimatedControl_Present_Start();            
            ThrowIfFailed(renderTarget->Target->Present());
            EventWrite_CanvasAnimatedControl_Present_Stop();

            drew = true;
        }
    }

    //
    // The call to Present() usually blocks until a previous frame has been
    // composed into the scene.  The happens because the swap chain has a
    // limited number of buffers available, and Present() won't return until
    // there's a buffer available to draw the next frame on.
    //
    // In some cases the Present() may return quickly.  For example, there may
    // already be a buffer free because the GPU is starting to catch up with the
    // GPU.  In fixed time step mode this will result in the next Tick happening
    // soon enough that there is no work to do, and so we don't do a
    // Draw/Present.
    //
    // Without the Present() call to block the CPU the game loop thread ends up
    // busy waiting, pegging a CPU, drawing more power and draining the battery
    // on a mobile device.  This is undesireable!
    //
    // To prevent this from happening we call WaitForVerticalBlank to delay the
    // next tick.
    //
    // Some caveats here:
    //
    //   - software devices do not support WaitForVerticalBlank. In this case
    //     the CanvasSwapChain does a Sleep(0).
    //
    //   - if there's no swap chain (eg the window is invisible) then we just
    //     sleep
    //
    if (!drew || !m_stepTimer.IsFixedTimeStep())
    {
        EventWrite_CanvasAnimatedControl_WaitForVerticalBlank_Start();
        if (swapChain)
        {
            ThrowIfFailed(swapChain->WaitForVerticalBlank());
        }
        else
        {
            GetAdapter()->Sleep(static_cast<DWORD>(StepTimer::TicksToMilliseconds(StepTimer::DefaultTargetElapsedTime)));
        }
        EventWrite_CanvasAnimatedControl_WaitForVerticalBlank_Stop();
    }
    
    return areResourcesCreated && !isPaused;
}