Krita Source Code Documentation
Loading...
Searching...
No Matches
KisPlaybackEngineQT.cpp
Go to the documentation of this file.
1/* This file is part of the KDE project
2 SPDX-FileCopyrightText: 2022 Emmet O'Neill <emmetoneill.pdx@gmail.com>
3 SPDX-FileCopyrightText: 2022 Eoin O'Neill <eoinoneill1991@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
9
10#include "kis_debug.h"
11#include "kis_canvas2.h"
13#include "kis_image.h"
15
16#include <QElapsedTimer>
17#include <QTimer>
21
22#include "KisPlaybackEngineQT.h"
23
24
25namespace {
26
30class PlaybackDriver : public QObject
31{
32 Q_OBJECT
33public:
34 PlaybackDriver(QObject* parent = nullptr);
35 ~PlaybackDriver();
36
37 void setPlaybackState(PlaybackState newState);
38
39 void setFramerate(int rate);
40 void setSpeed(qreal speed);
41 double speed();
42 void setDropFrames(bool drop);
43 bool dropFrames();
44
45Q_SIGNALS:
46 void throttledShowFrame();
47
48private:
49 void updatePlaybackLoopInterval(const int& in_fps, const qreal& in_speed);
50
51private:
52 QTimer m_playbackLoop;
53 double m_speed;
54 int m_fps;
55 bool m_dropFrames;
56};
57
58PlaybackDriver::PlaybackDriver(QObject *parent)
59 : QObject(parent)
60 , m_speed(1.0)
61 , m_fps(24)
62 , m_dropFrames(true)
63{
64 m_playbackLoop.setTimerType(Qt::PreciseTimer);
65 connect( &m_playbackLoop, SIGNAL(timeout()), this, SIGNAL(throttledShowFrame()) );
66}
67
68PlaybackDriver::~PlaybackDriver()
69{
70}
71
72void PlaybackDriver::setPlaybackState(PlaybackState newState) {
73 switch (newState) {
75 m_playbackLoop.start();
76 break;
79 default:
80 m_playbackLoop.stop();
81 break;
82 }
83}
84
85void PlaybackDriver::setFramerate(int rate) {
87 m_fps = rate;
88 updatePlaybackLoopInterval(m_fps, m_speed);
89}
90
91void PlaybackDriver::setSpeed(qreal speed) {
93 m_speed = speed;
94 updatePlaybackLoopInterval(m_fps, m_speed);
95}
96
97double PlaybackDriver::speed()
98{
99 return m_speed;
100}
101
102void PlaybackDriver::setDropFrames(bool drop) {
103 m_dropFrames = drop;
104}
105
106bool PlaybackDriver::dropFrames() {
107 return m_dropFrames;
108}
109
110void PlaybackDriver::updatePlaybackLoopInterval(const int &in_fps, const qreal &in_speed) {
111 int loopMS = qRound( 1000.f / (qreal(in_fps) * in_speed));
112 m_playbackLoop.setInterval(loopMS);
113}
114
115}
116
117// ======
118
126 static constexpr int frameStatsWindow = 50;
127
129 : averageTimePerFrame(frameStatsWindow)
130 , waitingForFrame(false)
131 , droppedFramesStat(frameStatsWindow)
132
133 {
134 timeSinceLastFrame.start();
135 }
136
137 void reset() {
138 timeSinceLastFrame.start();
139 averageTimePerFrame.reset(frameStatsWindow);
140 waitingForFrame = false;
141 droppedFramesStat.reset(frameStatsWindow);
142 }
143
144 QElapsedTimer timeSinceLastFrame;
147
149};
150
151// ====== KisPlaybackEngineQT ======
152
154public:
156 : driver(new PlaybackDriver())
157 {
158 }
159
161 }
162
163 QScopedPointer<PlaybackDriver> driver;
165};
166
168 : KisPlaybackEngine(parent)
169 , m_d(new Private())
170{
171}
172
176
177void KisPlaybackEngineQT::seek(int frameIndex, SeekOptionFlags flags)
178{
179 if (!activeCanvas())
180 return;
181
182 KIS_SAFE_ASSERT_RECOVER_RETURN(activeCanvas()->animationState());
183
185 KIS_SAFE_ASSERT_RECOVER_RETURN(displayProxy);
186
187 KIS_SAFE_ASSERT_RECOVER_RETURN(frameIndex >= 0);
188
189 if (displayProxy->activeFrame() != frameIndex || flags & SEEK_FINALIZE) {
190 displayProxy->displayFrame(frameIndex, flags & SEEK_FINALIZE);
191 }
192}
193
200
201boost::optional<int64_t> KisPlaybackEngineQT::activeFramesPerSecond() const
202{
203 if (activeCanvas()) {
205 } else {
206 return boost::none;
207 }
208}
209
211{
213
214 if (activeCanvas()->animationState()->playbackState() == PLAYING) {
215 const int droppedFrames = m_d->measure.droppedFramesStat.rollingSum();
216 const int totalFrames =
217 m_d->measure.droppedFramesStat.rollingCount() +
218 droppedFrames;
219
220 stats.droppedFramesPortion = qreal(droppedFrames) / totalFrames;
221 stats.expectedFps = qreal(activeFramesPerSecond().get_value_or(24)) * m_d->driver->speed();
222
223 const qreal avgTimePerFrame = m_d->measure.averageTimePerFrame.rollingMeanSafe();
224 stats.realFps = !qFuzzyIsNull(avgTimePerFrame) ? 1000.0 / avgTimePerFrame : 0.0;
225 }
226
227 return stats;
228}
229
231{
233
234 KIS_SAFE_ASSERT_RECOVER_RETURN(activeCanvas()->animationState());
235 KIS_SAFE_ASSERT_RECOVER_RETURN(activeCanvas()->animationState()->playbackState() == PLAYING);
236
238 KIS_SAFE_ASSERT_RECOVER_RETURN(displayProxy);
239
242 KIS_SAFE_ASSERT_RECOVER_RETURN(animInterface);
243
244 // If we're waiting for each frame, then we delay our callback.
245 if (m_d->measure.waitingForFrame) {
246 // Without drop frames on, we need to factor out time that we're waiting
247 // for a frame from our time
248 return;
249 }
250
251 const int currentFrame = displayProxy->activeFrame();
252 const int startFrame = animInterface->activePlaybackRange().start();
253 const int endFrame = animInterface->activePlaybackRange().end();
254
255 const int timeSinceLastFrame = m_d->measure.timeSinceLastFrame.restart();
256 const int timePerFrame = qRound(1000.0 / qreal(activeFramesPerSecond().get_value_or(24)) / m_d->driver->speed());
257 m_d->measure.averageTimePerFrame(timeSinceLastFrame);
258
259
260 // Drop frames logic...
261 int extraFrames = 0;
262 if (m_d->driver->dropFrames()) {
263 const int offset = timeSinceLastFrame - timePerFrame;
264 extraFrames = qMax(0, offset) / timePerFrame;
265 }
266
267 m_d->measure.droppedFramesStat(extraFrames);
268
269 { // just advance the frame ourselves based on the displayProxy's active frame.
270 int targetFrame = currentFrame + 1 + extraFrames;
271
272 targetFrame = frameWrap(targetFrame, startFrame, endFrame);
273
274 if (currentFrame != targetFrame) {
275 // We only wait when drop frames is enabled.
276 m_d->measure.waitingForFrame = !m_d->driver->dropFrames();
277
278 bool neededRefresh = displayProxy->displayFrame(targetFrame, false);
279
280 // If we didn't need to refresh, we just continue as usual.
281 m_d->measure.waitingForFrame &= neededRefresh;
282 }
283 }
284}
285
287{
288 KisCanvas2* canvas = dynamic_cast<KisCanvas2*>(p_canvas);
289
290 struct StopAndResume {
291 StopAndResume(KisPlaybackEngineQT* p_self)
292 : m_self(p_self) {
293 KIS_SAFE_ASSERT_RECOVER_RETURN(m_self->m_d->driver);
294
295 m_self->m_d->driver->setPlaybackState(PlaybackState::STOPPED);
296 }
297
298 ~StopAndResume() {
299 KIS_SAFE_ASSERT_RECOVER_RETURN(m_self->m_d->driver);
300
301 if (m_self->activeCanvas()) {
302 m_self->m_d->driver->setPlaybackState(m_self->activeCanvas()->animationState()->playbackState());
303 }
304 }
305
306 private:
307 KisPlaybackEngineQT* m_self;
308 };
309
310 if (activeCanvas() == canvas) {
311 return;
312 }
313
314 if (activeCanvas()) {
316
318
319 // Disconnect internal..
320 m_d->driver.data()->disconnect(this);
321
322 { // Disconnect old Image Anim Interface, prepare for new one..
323 auto image = activeCanvas()->image();
324 KisImageAnimationInterface* aniInterface = image ? image->animationInterface() : nullptr;
325 if (aniInterface) {
326 this->disconnect(image->animationInterface());
327 image->animationInterface()->disconnect(this);
328 }
329 }
330
331 { // Disconnect old display proxy, prepare for new one.
332 KisFrameDisplayProxy* displayProxy = animationState->displayProxy();
333
334 if (displayProxy) {
335 displayProxy->disconnect(this);
336 }
337 }
338
339 { // Disconnect old animation state, prepare for new one..
340 if (animationState) {
341 this->disconnect(animationState);
342 animationState->disconnect(this);
343 }
344 }
345 }
346
347 StopAndResume stopResume(this);
348
350
351 if (activeCanvas()) {
353 KIS_ASSERT(animationState);
354
356
357 { // Animation State Connections
358 connect(animationState, &KisCanvasAnimationState::sigPlaybackStateChanged, this, [this](PlaybackState state){
360
361 if (state == PLAYING) {
362 m_d->measure.reset();
363 }
364
365 m_d->driver->setPlaybackState(state);
366 });
367
368 connect(animationState, &KisCanvasAnimationState::sigPlaybackSpeedChanged, this, [this](qreal value){
370 m_d->driver->setSpeed(value);
371 });
372 m_d->driver->setSpeed(animationState->playbackSpeed());
373 }
374
375 { // Display proxy connections
376 KisFrameDisplayProxy* displayProxy = animationState->displayProxy();
377 KIS_ASSERT(displayProxy);
378 connect(displayProxy, &KisFrameDisplayProxy::sigFrameDisplayRefreshed, this, [this](){
379 m_d->measure.waitingForFrame = false;
380 });
381
382 connect(displayProxy, &KisFrameDisplayProxy::sigFrameRefreshSkipped, this, [this](){
383 m_d->measure.waitingForFrame = false;
384 });
385 }
386
387
388 { // Animation Interface Connections
389 auto image = activeCanvas()->image();
390 KIS_ASSERT(image);
391 KisImageAnimationInterface* aniInterface = image->animationInterface();
392 KIS_ASSERT(aniInterface);
393
394 connect(aniInterface, &KisImageAnimationInterface::sigFramerateChanged, this, [this](){
395 if (!activeCanvas())
396 return;
397
398 KisImageWSP img = activeCanvas()->image();
400 KisImageAnimationInterface* aniInterface = img->animationInterface();
401 KIS_SAFE_ASSERT_RECOVER_RETURN(aniInterface);
402
403 m_d->driver->setFramerate(aniInterface->framerate());
404 });
405
406 m_d->driver->setFramerate(aniInterface->framerate());
407 }
408
409 // Internal connections
410 connect(m_d->driver.data(), SIGNAL(throttledShowFrame()), this, SLOT(throttledDriverCallback()));
411
412 }
413}
414
416{
417 setCanvas(nullptr);
418}
419
420#include "KisPlaybackEngineQT.moc"
float value(const T *src, size_t ch)
@ SEEK_FINALIZE
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
KisCanvasAnimationState * animationState() const
KisImageWSP image() const
The KisCanvasAnimationState class stores all of the canvas-specific animation state.
void sigPlaybackStateChanged(PlaybackState state)
class KisFrameDisplayProxy * displayProxy()
void sigPlaybackSpeedChanged(qreal value)
The KisFrameDisplayProxy class sits between the KisCanvas (within its KisCanvasAnimationState) and it...
void sigFrameRefreshSkipped()
sigFrameRefreshSkipped tracks whether asynchronous "slow" refreshes are skipped due to the frame bein...
bool displayFrame(int frame, bool forceReproject)
Tell the DisplayProxy to show a new frame.
void sigFrameDisplayRefreshed()
int activeFrame() const
Gets the active frame, the frame that is intended to be shown. This should always reflect the actual ...
const KisTimeSpan & activePlaybackRange() const
activePlaybackRange
KisImageAnimationInterface * animationInterface() const
The KisPlaybackEngineQT class is an implementation of KisPlaybackEngine that drives animation playbac...
void setDropFramesMode(bool value) override
QScopedPointer< Private > m_d
KisPlaybackEngineQT(QObject *parent=nullptr)
PlaybackStats playbackStatistics() const override
void seek(int frameIndex, SeekOptionFlags flags=SEEK_FINALIZE|SEEK_PUSH_AUDIO) override
void throttledDriverCallback()
throttledDriverCallback handles signals from the internal driver that drives playback within this eng...
void setCanvas(KoCanvasBase *canvas) override
boost::optional< int64_t > activeFramesPerSecond() const
Krita's base animation playback engine for producing image frame changes and associated audio.
virtual void setDropFramesMode(bool value)
int frameWrap(int frame, int startFrame, int endFrame)
class KisCanvas2 * activeCanvas() const
virtual void setCanvas(KoCanvasBase *p_canvas) override
A simple wrapper class that hides boost includes from QtCreator preventing it from crashing when one ...
A simple wrapper class that hides boost includes from QtCreator preventing it from crashing when one ...
int start() const
int end() const
static bool qFuzzyIsNull(half h)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
ChildIterator< value_type, is_const > parent(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:327
Struct used to keep track of all frame time variance and accommodate for skipped frames....
KisRollingMeanAccumulatorWrapper averageTimePerFrame
QElapsedTimer timeSinceLastFrame
KisRollingSumAccumulatorWrapper droppedFramesStat
QScopedPointer< PlaybackDriver > driver