Krita Source Code Documentation
Loading...
Searching...
No Matches
recorder_writer.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2020 Dmitrii Utkin <loentar@gmail.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.1-only
5 */
6
7#include "recorder_writer.h"
9#include "recorder_const.h"
11
12#include <kis_canvas2.h>
13#include <kis_image.h>
14#include <KisDocument.h>
15#include <KoToolProxy.h>
16#include "kis_tool_proxy.h"
17#include <KisMainWindow.h>
18
19#include <QDir>
20#include <QDirIterator>
21#include <QElapsedTimer>
22#include <QImage>
23#include <QRegularExpression>
24#include <QApplication>
25#include <QMutexLocker>
26#include <QPointer>
27#include <QTimer>
28#include <QVector>
29#include <QSharedPointer>
30#include <atomic>
31
32namespace
33{
34 const QStringList forceBlacklistedTools = {
35 "KisToolTransform",
36 "KisToolPolyline",
37 "KisToolPolygon",
38 }; // disable recorder when toggled to one of these tools.
39 const QStringList activateBlacklistedTools = {
40 "KritaShape/KisToolLine",
41 "KritaShape/KisToolRectangle",
42 "KritaShape/KisToolEllipse",
43 }; // disable recorder when toggled to one of these tools and activated tool(left button pressed on canvas).
44}
45
47{
48 auto oldValue = threads;
49 threads = static_cast<unsigned int>(
50 qBound(1, value, static_cast<int>(ThreadSystemValue::MaxThreadCount))
51 );
52 return oldValue != threads;
53}
54
56{
57 auto oldValue = get();
58 if (set(value)) {
59 // Emit signal to GUI that the value has been changed
60 Q_EMIT notifyValueChange(oldValue < get());
61 }
62}
63
64unsigned int ThreadCounter::get() const
65{
66 return threads;
67}
68
70{
71 QMutexLocker lock(&inUseMutex);
72 return setUsedImpl(value);
73}
74
76{
77 QMutexLocker lock(&inUseMutex);
78 auto oldValue = getUsed();
79 if (setUsedImpl(value)) {
80 // Emit signal to GUI that the value has been changed
81 Q_EMIT notifyInUseChange(oldValue < getUsed());
82 }
83}
84
86{
87 QMutexLocker lock(&inUseMutex);
88 auto oldValue = getUsed();
89 if (setUsedImpl(inUse + 1)) {
90 // Emit signal to GUI that the value has been changed
91 Q_EMIT notifyInUseChange(oldValue < getUsed());
92 }
93}
95{
96 QMutexLocker lock(&inUseMutex);
97 if (setUsedImpl(inUse - 1)) {
98 // Emit signal to GUI that the value has been changed
99 Q_EMIT notifyInUseChange(false);
100 }
101}
102
103unsigned int ThreadCounter::getUsed() const
104{
105 return inUse;
106}
107
109{
110 auto oldValue = inUse;
111 inUse = static_cast<unsigned int>(
112 qBound(0, value, static_cast<int>(threads))
113 );
114 return oldValue != inUse;
115}
116
118{
119public:
121 : canvas(c)
122 , settings(&s)
123 , outputDir(&d)
124 {}
125 Private() = delete;
126 Private(const Private&) = default;
127 Private(Private&&) = delete;
128 Private& operator=(const Private&) = default;
130
132 QByteArray imageBuffer;
135 QImage frame;
137 int partIndex = 0; // Consecutive file number
138 bool isActive = false;
140 const QDir* outputDir;
141
146
148 {
149 KisImageSP image = canvas->image();
150
151 const bool needSrgbConversion = [&]() {
153 || image->colorSpace()->colorModelId() != RGBAColorModelID) {
154 return true;
155 }
156 const bool hasPrimaries = image->colorSpace()->profile()->hasColorants();
158 if (hasPrimaries) {
159 const ColorPrimaries primaries = image->colorSpace()->profile()->getColorPrimaries();
160 if (gamma == TRC_IEC_61966_2_1 && primaries == PRIMARIES_ITU_R_BT_709_5) {
161 return false;
162 }
163 }
164 return true;
165 }();
166
167 if (targetCs && needSrgbConversion) {
168 device->convertTo(targetCs);
169 }
170
171 // truncate uneven image width/height making it even for subdivided size too
172 const quint32 bitmask = ~(0xFFFFFFFFu >> (31 - settings->resolution));
173 const quint32 width = image->width() & bitmask;
174 const quint32 height = image->height() & bitmask;
175 const int bufferSize = device->pixelSize() * width * height;
176
177 bool resize = imageBuffer.size() != bufferSize;
178 if (resize)
179 imageBuffer.resize(bufferSize);
180
181 if (resize || frameResolution != settings->resolution) {
182 const int divider = 1 << settings->resolution;
183 const int outWidth = width / divider;
184 const int outHeight = height / divider;
185 uchar *outData = reinterpret_cast<uchar *>(imageBuffer.data());
186
187 frame = QImage(outData, outWidth, outHeight, QImage::Format_ARGB32);
188 }
189
190 device->readBytes(reinterpret_cast<quint8 *>(imageBuffer.data()), 0, 0, width, height);
191
192 imageBufferWidth = width;
193 imageBufferHeight = height;
194 }
195
196 // Calculate ARGB average value using carry save adder:
197 // https://www.qt.io/blog/2009/01/20/50-scaling-of-argb32-image
198 inline quint32 avg(quint32 c1, quint32 c2)
199 {
200 return (((c1 ^ c2) & 0xfefefefeUL) >> 1) + (c1 & c2);
201 }
202
204 {
205 quint32 *buffer = reinterpret_cast<quint32 *>(imageBuffer.data());
206 quint32 *out = buffer;
207
208 for (int y = 0; y < imageBufferHeight; y += 2) {
209 const quint32 *in1 = buffer + y * imageBufferWidth;
210 const quint32 *in2 = in1 + imageBufferWidth;
211
212 for (int x = 0; x < imageBufferWidth; x += 2) {
213 *out = avg(
214 avg(in1[x], in1[x + 1]),
215 avg(in2[x], in2[x + 1])
216 );
217
218 ++out;
219 }
220 }
221
222 imageBufferWidth /= 2;
224 }
225
226 inline quint32 blendSourceOver(const int alpha, const quint32 source, const quint32 destination)
227 {
228 // co = αs x Cs + αb x Cb x (1 – αs)
229 // αo = 1, αb = 1
230
231 const int inverseAlpha = 255 - alpha;
232 return qRgb(
233 (alpha * qRed(source) + inverseAlpha * qRed(destination)) >> 8,
234 (alpha * qGreen(source) + inverseAlpha * qGreen(destination)) >> 8,
235 (alpha * qBlue(source) + inverseAlpha * qBlue(destination)) >> 8
236 );
237 }
238
240 {
241 const quint32 background = 0xFFFFFFFF;
242 quint32 *buffer = reinterpret_cast<quint32 *>(imageBuffer.data());
243 const quint32 *end = buffer + imageBufferWidth * imageBufferHeight;
244 while (buffer != end) {
245 const int alpha = qAlpha(*buffer);
246 switch (alpha) {
247 case 0xFF: // fully opaque
248 break;
249 case 0x00: // fully transparent - just replace to background
250 *buffer = background;
251 break;
252 default: // partly transparent - do color blending
253 *buffer = blendSourceOver(alpha, *buffer, background);
254 break;
255 }
256 ++buffer;
257 }
258 }
259
260 int writeFrame(int index)
261 {
262 if (!outputDir->exists() && !outputDir->mkpath(settings->outputDirectory))
263 return false;
264
265 const QString fileName = QString("%1").arg(index, 7, 10, QLatin1Char('0'));
266 const QString &filePath = QString("%1%2.%3").arg(settings->outputDirectory, fileName,
268
269 int factor = -1; // default value
270 switch (settings->format) {
272 factor = settings->quality; // 0...100
273 break;
275 factor = qBound(0, 100 - (settings->compression * 10), 100); // 0..10 -> 100..0
276 break;
277 }
278
279 bool result = frame.save(filePath, RecorderFormatInfo::fileFormat(settings->format).data(), factor);
280 if (!result)
281 QFile(filePath).remove(); // remove corrupted frame
282 return result;
283 }
284
285};
286
288 unsigned int i,
290 const RecorderWriterSettings& s,
291 const QDir& d)
292 : d(new Private(c, s, d))
293 , id(i)
294{}
295
297{
298 delete d;
299}
300
301void RecorderWriter::onCaptureImage(int writerId, int index, KisPaintDeviceSP device)
302{
303 if (static_cast<int>(id) != writerId)
304 return;
305
306 d->isActive = true;
307
308 d->captureImage(device);
309
310 // downscale image buffer
311 for (int res = 0; res < d->settings->resolution; ++res)
313
315
316 bool isFrameWritten = d->writeFrame(index);
317
318 d->isActive = false;
319 Q_EMIT capturingDone(id, isFrameWritten);
320}
321
323{
324 return d->isActive;
325}
326
328{
331
333 QObject* threadParent,
334 unsigned int i,
336 const RecorderWriterSettings& s,
337 const QDir& d
338 )
339 : thread(QThreadPtr::create(threadParent))
340 , writer(RecorderWriterPtr::create(i, c, s, d))
341 {}
342
343 bool inUse{false};
346};
347
349
351{
352public:
354 : q(q_ptr)
355 , recorderThreads(rt)
356 {}
357
360 volatile std::atomic_bool enabled = false; // enable recording only for active documents
361 volatile std::atomic_bool imageModified = false;
362 volatile std::atomic_bool isForceBlackTool = false;
363 volatile std::atomic_bool isActivateBlackTool = false;
364 volatile std::atomic_bool toolActivated = false;
365 int partIndex = 0; // Consecutive file number
366 std::atomic_int freeWriterId = -1;
367 int interval = 1;
369 QTimer timer;
373
374 int findLastIndex(const QString &directory)
375 {
376 QElapsedTimer dbgTimer;
377 dbgTimer.start();
378
379 QDirIterator dirIterator(directory);
380 const QString &extension = RecorderFormatInfo::fileExtension(settings.format);
381 const QRegularExpression &snapshotFilePattern = RecorderConst::snapshotFilePatternFor(extension);
382
383 int recordIndex = -1;
384 while (dirIterator.hasNext()) {
385 dirIterator.next();
386
387 const QString &fileName = dirIterator.fileName();
388 const QRegularExpressionMatch &match = snapshotFilePattern.match(fileName);
389 if (match.hasMatch()) {
390 int index = match.captured(1).toInt();
391 if (recordIndex < index)
392 recordIndex = index;
393 }
394 }
395 dbgTools << "findLastPartNumber for" << directory << ": " << dbgTimer.elapsed() << "ms";
396
397 return recordIndex;
398 }
399
401 {
402 bool result = true;
403 bool alreadyWarn = false;
404 bool alreadyErr = false;
405
406 for (auto &el : writerPool) {
407 if (el.inUse && !el.writer->isActive()) {
408 // There's a pending job, but the writer didn't recieve the signal yet
409
410 // Wait until all the jobs processes
411 canvas->image()->waitForDone();
412
413 // Make sure the signals are delivered
414 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
415
416 //we can exit the loop, all the signals have been delivered and we don't need to check further
417 break;
418 }
419 }
420
421 for (auto &el : writerPool) {
422 el.thread->quit();
423 el.thread->wait(RecorderConst::waitThreadTimeoutMs);
424 disconnect(q, SIGNAL(startCapturing(int, int, KisPaintDeviceSP)), el.writer.get(), SLOT(onCaptureImage(int, int, KisPaintDeviceSP)));
425 disconnect(el.writer.get(), SIGNAL(capturingDone(int, bool)), q, SLOT(onCapturingDone(int, bool)));
426 if (el.thread->isRunning())
427 {
428 if (!alreadyWarn) {
429 warnResources << "One of the Recorder WriterPool threads has been blocked and has to be terminated. "
430 << "Thread Name: " << el.thread->objectName();
431 alreadyWarn = true;
432 }
433 el.thread->terminate();
434 if (!el.thread->wait(RecorderConst::waitThreadTimeoutMs))
435 {
436 if (!alreadyErr) {
437 errResources << "Something odd has been happen. Krita was unable to stop one of the Recorder WriterPool Threads. "
438 << "Thread Name: " << el.thread->objectName();
439 alreadyErr = true;
440 }
441 result = false;
442 }
443 }
444 }
445
446 writerPool.clear();
447 freeWriterId = -1;
448
449 if (!result)
450 Q_EMIT q->recorderStopWarning();
451
452 return result;
453 }
454
456 {
457 writerPool.reserve(recorderThreads.get());
458 while (static_cast<int>(recorderThreads.get()) > writerPool.size()) {
459 auto newWorkerId = writerPool.size();
460 freeWriterId = newWorkerId - 1; // Set the value to the last existing writerEl index ->
461 // The next call of searchForFreeWriter() will than automatically find newWorkerId
462
463 writerPool.append(WriterPoolEl(q, newWorkerId, canvas, settings, outputDir));
464
465 auto writerPtr = writerPool[newWorkerId].writer;
466 auto threadPtr = writerPool[newWorkerId].thread;
467 threadPtr->setObjectName(QString("Krita-Recorder-WriterPool#%1").arg(newWorkerId));
468 connect(q, SIGNAL(startCapturing(int, int, KisPaintDeviceSP)), writerPtr.get(), SLOT(onCaptureImage(int, int, KisPaintDeviceSP)));
469 connect(writerPtr.get(), SIGNAL(capturingDone(int, bool)), q, SLOT(onCapturingDone(int, bool)));
470 writerPtr->moveToThread(threadPtr.get());
471 threadPtr->start(QThread::IdlePriority);
472 }
473 }
474
476 {
477 auto j = freeWriterId + 1;
478 for(auto i = 0; i < writerPool.size(); i++, j++)
479 {
480 freeWriterId = j % writerPool.size();
481 if (writerPool[freeWriterId].thread->isRunning() && !writerPool[freeWriterId].inUse)
482 return;
483 }
484 freeWriterId = -1;
485 }
486
487 bool canStartCapture() // skip capture when use some tools.
488 {
490 return false;
492 return false;
493 return true;
494 }
495};
496
498 : d(new Private(this, recorderThreads))
499 , exporterSettings(es)
500{
501 d->timer.setTimerType(Qt::PreciseTimer);
502}
503
508
510{
511 // Restart writers if canvas changes
512 bool restart = d->timer.isActive();
513 if (restart) {
514 stop(false);
515 }
516
517 if (d->canvas) {
518 KoToolProxy *proxy = d->canvas->toolProxy();
519 KisToolProxy *kritaProxy = dynamic_cast<KisToolProxy*>(proxy);
520
521 disconnect(proxy, SIGNAL(toolChanged(QString)), this, SLOT(onToolChanged(QString)));
522 disconnect(kritaProxy, SIGNAL(toolPrimaryActionActivated(bool)), this, SLOT(onToolPrimaryActionActivated(bool)));
523 disconnect(d->canvas->image(), SIGNAL(sigImageUpdated(QRect)), this, SLOT(onImageModified()));
524 }
525
526 d->canvas = canvas;
527
528 if (d->canvas) {
529 KoToolProxy *proxy = d->canvas->toolProxy();
530 KisToolProxy *kritaProxy = dynamic_cast<KisToolProxy*>(proxy);
531
532 connect(proxy, SIGNAL(toolChanged(QString)), this, SLOT(onToolChanged(QString)),
533 Qt::DirectConnection); // need to handle it even if our event loop is not running
534 connect(kritaProxy, SIGNAL(toolPrimaryActionActivated(bool)), this, SLOT(onToolPrimaryActionActivated(bool)),
535 Qt::DirectConnection);
536 connect(d->canvas->image(), SIGNAL(sigImageUpdated(QRect)), this, SLOT(onImageModified()),
537 Qt::DirectConnection); // because it spams
538 }
539
540 if (restart) {
541 start(false);
542 }
543}
544
546{
547 // Restart writers if setup changes
548 bool restart = d->timer.isActive();
549 if (restart) {
550 stop(false);
551 }
552
553 d->settings = settings;
554 d->outputDir.setPath(settings.outputDirectory);
555
557
558 if (restart) {
559 start(false);
560 }
561}
562
563void RecorderWriterManager::start(bool toggleEnabled)
564{
565 if (d->timer.isActive())
566 return;
567
568 if (!d->canvas)
569 return;
570
571 d->enabled = true;
572 d->imageModified = false;
573
574 connect(&d->timer, SIGNAL (timeout()), this, SLOT (onTimer()));
576 d->interval = static_cast<int>(1000.0/static_cast<double>(exporterSettings.fps));
577 } else {
578 d->interval = static_cast<int>(qMax(d->settings.captureInterval, .1) * 1000.0);
579 }
581 d->timer.start(d->interval);
582 if (toggleEnabled) {
583 Q_EMIT started();
584 }
585}
586
587bool RecorderWriterManager::stop(bool toggleEnabled)
588{
589 //Check if the timer is active, so that we don't try to clear the writer pool multiple times
590 if (!d->timer.isActive())
591 return true;
592
593 d->timer.stop();
594 auto result = d->clearWriterPool();
596 if (toggleEnabled) {
597 Q_EMIT stopped();
598 }
599 return result;
600}
601
602void RecorderWriterManager::setEnabled(bool enabled = false)
603{
604 d->enabled = enabled;
605}
606
608{
609 if (!d->enabled || !d->canvas)
610 return;
611
612 // take snapshots only if main window is active
613 // else some dialogs like filters may disappear when canvas->image()->lock() is called
614 if (qobject_cast<KisMainWindow*>(QApplication::activeWindow()) == nullptr)
615 return;
616
618 (d->canvas->image()->isIsolatingLayer() || d->canvas->image()->isIsolatingGroup())) {
619 return;
620 }
621
622 if (!d->imageModified)
623 return;
624
625 d->imageModified = false;
626
627 if (!d->canStartCapture())
628 return;
629
631
632 if (d->freeWriterId == -1)
633 {
634 Q_EMIT lowPerformanceWarning();
635 return;
636 }
637
638 d->writerPool[d->freeWriterId].inUse = true;
639 d->writerPool[d->freeWriterId].thread->setPriority(QThread::HighPriority);
641
642 RecorderImageCopyingJob *job = new RecorderImageCopyingJob(d->canvas->image()->projection(),
643 d->canvas->image()->bounds(),
645 ++d->partIndex);
646
647 connect(job,
648 SIGNAL(sigImageCopyingDoneWithData(int, int, KisPaintDeviceSP)),
649 this,
650 SIGNAL(startCapturing(int, int, KisPaintDeviceSP)));
651
652 d->canvas->image()->addSpontaneousJob(job);
653}
654
655void RecorderWriterManager::onCapturingDone(int workerId, bool success)
656{
657 if (workerId >= d->writerPool.size())
658 return;
659 d->writerPool[workerId].inUse = false;
660 d->writerPool[workerId].thread->setPriority(QThread::IdlePriority);
662 if (!success) {
663 stop();
664 Q_EMIT frameWriteFailed();
665 }
666}
667
669{
670 if (!d->enabled || !d->canStartCapture())
671 return;
672
674 (d->canvas->image()->isIsolatingLayer() || d->canvas->image()->isIsolatingGroup()))
675 return;
676
677 d->imageModified = true;
678}
679
680void RecorderWriterManager::onToolChanged(const QString &toolId)
681{
682 d->isForceBlackTool = forceBlacklistedTools.contains(toolId);
683 d->isActivateBlackTool = activateBlacklistedTools.contains(toolId);
684}
685
687{
688 d->toolActivated = activated;
689}
float value(const T *src, size_t ch)
KisMagneticGraph::vertex_descriptor source(typename KisMagneticGraph::edge_descriptor e, KisMagneticGraph g)
const KoID Integer8BitsColorDepthID("U8", ki18n("8-bit integer/channel"))
const KoID RGBAColorModelID("RGBA", ki18n("RGB/Alpha"))
ColorPrimaries
The colorPrimaries enum Enum of colorants, follows ITU H.273 for values 0 to 255, and has extra known...
@ PRIMARIES_ITU_R_BT_709_5
TransferCharacteristics
The transferCharacteristics enum Enum of transfer characteristics, follows ITU H.273 for values 0 to ...
@ TRC_IEC_61966_2_1
const KoColorSpace * colorSpace() const
qint32 width() const
qint32 height() const
quint32 pixelSize() const
void convertTo(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent=KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags=KoColorConversionTransformation::internalConversionFlags(), KUndo2Command *parentCommand=nullptr, KoUpdater *progressUpdater=nullptr)
void readBytes(quint8 *data, qint32 x, qint32 y, qint32 w, qint32 h) const
virtual KoID colorModelId() const =0
virtual KoID colorDepthId() const =0
virtual const KoColorProfile * profile() const =0
QString id() const
Definition KoID.cpp:63
volatile std::atomic_bool toolActivated
int findLastIndex(const QString &directory)
RecorderWriterManager *const q
volatile std::atomic_bool isActivateBlackTool
volatile std::atomic_bool imageModified
Private(RecorderWriterManager *q_ptr, ThreadCounter &rt)
volatile std::atomic_bool isForceBlackTool
RecorderWriterSettings settings
volatile std::atomic_bool enabled
RecorderWriterManager()=delete
void start(bool toggleEnabled=true)
void onCapturingDone(int workerId, bool success)
void onToolPrimaryActionActivated(bool activated)
void startCapturing(int writerId, int index, KisPaintDeviceSP device)
void setCanvas(QPointer< KisCanvas2 > canvas)
bool stop(bool toggleEnabled=true)
void setEnabled(bool enabled)
void setup(const RecorderWriterSettings &settings)
ThreadCounter recorderThreads
const RecorderExportSettings & exporterSettings
void onToolChanged(const QString &toolId)
Private(Private &&)=delete
QPointer< KisCanvas2 > canvas
quint32 avg(quint32 c1, quint32 c2)
const KoColorSpace * targetCs
Private & operator=(Private &&)=delete
Private & operator=(const Private &)=default
void captureImage(KisPaintDeviceSP device)
quint32 blendSourceOver(const int alpha, const quint32 source, const quint32 destination)
const RecorderWriterSettings * settings
Private(QPointer< KisCanvas2 > c, const RecorderWriterSettings &s, const QDir &d)
Private(const Private &)=default
RecorderWriter()=delete
void onCaptureImage(int writerId, int index, KisPaintDeviceSP device)
Private *const d
void capturingDone(int writerId, bool success)
void setUsedAndNotify(int value)
bool setUsedImpl(int value)
unsigned int getUsed() const
unsigned int get() const
void notifyValueChange(bool valueWasIncreased)
unsigned int threads
void setAndNotify(int value)
bool setUsed(int value)
bool set(int value)
unsigned int inUse
void notifyInUseChange(bool valueWasIncreased)
#define errResources
Definition kis_debug.h:113
#define warnResources
Definition kis_debug.h:92
#define dbgTools
Definition kis_debug.h:52
QRegularExpression snapshotFilePatternFor(const QString &extension)
constexpr int waitThreadTimeoutMs
QLatin1String fileFormat(RecorderFormat format)
QLatin1String fileExtension(RecorderFormat format)
const unsigned int MaxThreadCount
virtual ColorPrimaries getColorPrimaries() const
getColorPrimaries
virtual bool hasColorants() const =0
virtual TransferCharacteristics getTransferCharacteristics() const
getTransferCharacteristics This function should be subclassed at some point so we can get the value f...
const KoColorSpace * colorSpace(const QString &colorModelId, const QString &colorDepthId, const KoColorProfile *profile)
static KoColorSpaceRegistry * instance()
const KoColorProfile * p709SRGBProfile() const
QSharedPointer< RecorderWriter > writer
QSharedPointer< QThread > thread
WriterPoolEl(QObject *threadParent, unsigned int i, QPointer< KisCanvas2 > c, const RecorderWriterSettings &s, const QDir &d)