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"
8#include "recorder_const.h"
10
11#include <kis_canvas2.h>
12#include <kis_image.h>
13#include <KisDocument.h>
14#include <KoToolProxy.h>
15#include "kis_tool_proxy.h"
16#include <KisMainWindow.h>
17
18#include <QDir>
19#include <QDirIterator>
20#include <QElapsedTimer>
21#include <QImage>
22#include <QRegularExpression>
23#include <QApplication>
24#include <QMutexLocker>
25#include <QPointer>
26#include <QTimer>
27#include <QVector>
28#include <QSharedPointer>
29#include <atomic>
30
31namespace
32{
33 const QStringList forceBlacklistedTools = {
34 "KisToolTransform",
35 "KisToolPolyline",
36 "KisToolPolygon",
37 "KisToolSelectOutline",
38 "KisToolSelectPolygonal",
39 "KisToolEncloseAndFill",
40 "KisToolPath",
41 "KisToolCrop",
42 "KisToolSelectPath",
43 "KisToolSelectMagnetic",
44 "SvgTextTool",
45 }; // disable recorder when toggled to one of these tools.
46 const QStringList activateBlacklistedTools = {
47 "KritaTransform/KisToolMove",
48 "KritaShape/KisToolLine",
49 "KritaShape/KisToolRectangle",
50 "KritaShape/KisToolEllipse",
51 "KisToolSelectRectangular",
52 "KisToolSelectElliptical",
53 }; // disable recorder when toggled to one of these tools and activated tool(left button pressed on canvas).
54}
55
57{
58 auto oldValue = threads;
59 threads = static_cast<unsigned int>(
60 qBound(1, value, static_cast<int>(ThreadSystemValue::MaxThreadCount))
61 );
62 return oldValue != threads;
63}
64
66{
67 auto oldValue = get();
68 if (set(value)) {
69 // Emit signal to GUI that the value has been changed
70 Q_EMIT notifyValueChange(oldValue < get());
71 }
72}
73
74unsigned int ThreadCounter::get() const
75{
76 return threads;
77}
78
80{
81 QMutexLocker lock(&inUseMutex);
82 return setUsedImpl(value);
83}
84
86{
87 QMutexLocker lock(&inUseMutex);
88 auto oldValue = getUsed();
89 if (setUsedImpl(value)) {
90 // Emit signal to GUI that the value has been changed
91 Q_EMIT notifyInUseChange(oldValue < getUsed());
92 }
93}
94
96{
97 QMutexLocker lock(&inUseMutex);
98 auto oldValue = getUsed();
99 if (setUsedImpl(inUse + 1)) {
100 // Emit signal to GUI that the value has been changed
101 Q_EMIT notifyInUseChange(oldValue < getUsed());
102 }
103}
105{
106 QMutexLocker lock(&inUseMutex);
107 if (setUsedImpl(inUse - 1)) {
108 // Emit signal to GUI that the value has been changed
109 Q_EMIT notifyInUseChange(false);
110 }
111}
112
113unsigned int ThreadCounter::getUsed() const
114{
115 return inUse;
116}
117
119{
120 auto oldValue = inUse;
121 inUse = static_cast<unsigned int>(
122 qBound(0, value, static_cast<int>(threads))
123 );
124 return oldValue != inUse;
125}
126
128{
129public:
131 : canvas(c)
132 , settings(&s)
133 , outputDir(&d)
134 {}
135 Private() = delete;
136 Private(const Private&) = default;
137 Private(Private&&) = delete;
138 Private& operator=(const Private&) = default;
140
142 QByteArray imageBuffer;
145 QImage frame;
147 int partIndex = 0; // Consecutive file number
149 const QDir* outputDir;
150
155
157 {
158 KisImageSP image = canvas->image();
159
160 // Create detached paint device that can be converted to target colorspace
161 KisPaintDeviceSP device = new KisPaintDevice(image->colorSpace());
162
163 // we don't want image->barrierLock() because it will wait until the full stroke is finished
165 device->makeCloneFromRough(image->projection(), image->bounds());
166 image->unlock();
167
168 const bool needSrgbConversion = [&]() {
170 || image->colorSpace()->colorModelId() != RGBAColorModelID) {
171 return true;
172 }
173 const bool hasPrimaries = image->colorSpace()->profile()->hasColorants();
175 if (hasPrimaries) {
176 const ColorPrimaries primaries = image->colorSpace()->profile()->getColorPrimaries();
177 if (gamma == TRC_IEC_61966_2_1 && primaries == PRIMARIES_ITU_R_BT_709_5) {
178 return false;
179 }
180 }
181 return true;
182 }();
183
184 if (targetCs && needSrgbConversion) {
185 device->convertTo(targetCs);
186 }
187
188 // truncate uneven image width/height making it even for subdivided size too
189 const quint32 bitmask = ~(0xFFFFFFFFu >> (31 - settings->resolution));
190 const quint32 width = image->width() & bitmask;
191 const quint32 height = image->height() & bitmask;
192 const int bufferSize = device->pixelSize() * width * height;
193
194 bool resize = imageBuffer.size() != bufferSize;
195 if (resize)
196 imageBuffer.resize(bufferSize);
197
198 if (resize || frameResolution != settings->resolution) {
199 const int divider = 1 << settings->resolution;
200 const int outWidth = width / divider;
201 const int outHeight = height / divider;
202 uchar *outData = reinterpret_cast<uchar *>(imageBuffer.data());
203
204 frame = QImage(outData, outWidth, outHeight, QImage::Format_ARGB32);
205 }
206
207 device->readBytes(reinterpret_cast<quint8 *>(imageBuffer.data()), 0, 0, width, height);
208
209 imageBufferWidth = width;
210 imageBufferHeight = height;
211 }
212
213 // Calculate ARGB average value using carry save adder:
214 // https://www.qt.io/blog/2009/01/20/50-scaling-of-argb32-image
215 inline quint32 avg(quint32 c1, quint32 c2)
216 {
217 return (((c1 ^ c2) & 0xfefefefeUL) >> 1) + (c1 & c2);
218 }
219
221 {
222 quint32 *buffer = reinterpret_cast<quint32 *>(imageBuffer.data());
223 quint32 *out = buffer;
224
225 for (int y = 0; y < imageBufferHeight; y += 2) {
226 const quint32 *in1 = buffer + y * imageBufferWidth;
227 const quint32 *in2 = in1 + imageBufferWidth;
228
229 for (int x = 0; x < imageBufferWidth; x += 2) {
230 *out = avg(
231 avg(in1[x], in1[x + 1]),
232 avg(in2[x], in2[x + 1])
233 );
234
235 ++out;
236 }
237 }
238
239 imageBufferWidth /= 2;
241 }
242
243 inline quint32 blendSourceOver(const int alpha, const quint32 source, const quint32 destination)
244 {
245 // co = αs x Cs + αb x Cb x (1 – αs)
246 // αo = 1, αb = 1
247
248 const int inverseAlpha = 255 - alpha;
249 return qRgb(
250 (alpha * qRed(source) + inverseAlpha * qRed(destination)) >> 8,
251 (alpha * qGreen(source) + inverseAlpha * qGreen(destination)) >> 8,
252 (alpha * qBlue(source) + inverseAlpha * qBlue(destination)) >> 8
253 );
254 }
255
257 {
258 const quint32 background = 0xFFFFFFFF;
259 quint32 *buffer = reinterpret_cast<quint32 *>(imageBuffer.data());
260 const quint32 *end = buffer + imageBufferWidth * imageBufferHeight;
261 while (buffer != end) {
262 const int alpha = qAlpha(*buffer);
263 switch (alpha) {
264 case 0xFF: // fully opaque
265 break;
266 case 0x00: // fully transparent - just replace to background
267 *buffer = background;
268 break;
269 default: // partly transparent - do color blending
270 *buffer = blendSourceOver(alpha, *buffer, background);
271 break;
272 }
273 ++buffer;
274 }
275 }
276
278 {
279 if (!outputDir->exists() && !outputDir->mkpath(settings->outputDirectory))
280 return false;
281
282 const QString fileName = QString("%1").arg(partIndex, 7, 10, QLatin1Char('0'));
283 const QString &filePath = QString("%1%2.%3").arg(settings->outputDirectory, fileName,
285
286 int factor = -1; // default value
287 switch (settings->format) {
289 factor = settings->quality; // 0...100
290 break;
292 factor = qBound(0, 100 - (settings->compression * 10), 100); // 0..10 -> 100..0
293 break;
294 }
295
296 bool result = frame.save(filePath, RecorderFormatInfo::fileFormat(settings->format).data(), factor);
297 if (!result)
298 QFile(filePath).remove(); // remove corrupted frame
299 return result;
300 }
301
302};
303
305 unsigned int i,
307 const RecorderWriterSettings& s,
308 const QDir& d)
309 : d(new Private(c, s, d))
310 , id(i)
311{}
312
314{
315 delete d;
316}
317
318void RecorderWriter::onCaptureImage(int writerId, int index)
319{
320 if (static_cast<int>(id) != writerId)
321 return;
322
323 d->captureImage();
324
325 // downscale image buffer
326 for (int res = 0; res < d->settings->resolution; ++res)
328
330
331 d->partIndex = index;
332
333 bool isFrameWritten = d->writeFrame();
334
335 Q_EMIT capturingDone(id, isFrameWritten);
336}
337
338
340{
343
345 QObject* threadParent,
346 unsigned int i,
348 const RecorderWriterSettings& s,
349 const QDir& d
350 )
351 : thread(QThreadPtr::create(threadParent))
352 , writer(RecorderWriterPtr::create(i, c, s, d))
353 {}
354
355 bool inUse{false};
358};
359
361
363{
364public:
366 : q(q_ptr)
367 , recorderThreads(rt)
368 {}
369
372 volatile std::atomic_bool enabled = false; // enable recording only for active documents
373 volatile std::atomic_bool imageModified = false;
374 volatile std::atomic_bool isForceBlackTool = false;
375 volatile std::atomic_bool isActivateBlackTool = false;
376 volatile std::atomic_bool toolActivated = false;
377 int partIndex = 0; // Consecutive file number
378 std::atomic_int freeWriterId = -1;
379 int interval = 1;
381 QTimer timer;
385
386 int findLastIndex(const QString &directory)
387 {
388 QElapsedTimer dbgTimer;
389 dbgTimer.start();
390
391 QDirIterator dirIterator(directory);
392 const QString &extension = RecorderFormatInfo::fileExtension(settings.format);
393 const QRegularExpression &snapshotFilePattern = RecorderConst::snapshotFilePatternFor(extension);
394
395 int recordIndex = -1;
396 while (dirIterator.hasNext()) {
397 dirIterator.next();
398
399 const QString &fileName = dirIterator.fileName();
400 const QRegularExpressionMatch &match = snapshotFilePattern.match(fileName);
401 if (match.hasMatch()) {
402 int index = match.captured(1).toInt();
403 if (recordIndex < index)
404 recordIndex = index;
405 }
406 }
407 dbgTools << "findLastPartNumber for" << directory << ": " << dbgTimer.elapsed() << "ms";
408
409 return recordIndex;
410 }
411
413 {
414 bool result = true;
415 bool alreadyWarn = false;
416 bool alreadyErr = false;
417 for(auto& el: writerPool)
418 {
419 el.thread->quit();
420 el.thread->wait(RecorderConst::waitThreadTimeoutMs);
421 disconnect(q, SIGNAL(startCapturing(int, int)), el.writer.get(), SLOT(onCaptureImage(int, int)));
422 disconnect(el.writer.get(), SIGNAL(capturingDone(int, bool)), q, SLOT(onCapturingDone(int, bool)));
423 if (el.thread->isRunning())
424 {
425 if (!alreadyWarn) {
426 warnResources << "One of the Recorder WriterPool threads has been blocked and has to be terminated. "
427 << "Thread Name: " << el.thread->objectName();
428 alreadyWarn = true;
429 }
430 el.thread->terminate();
431 if (!el.thread->wait(RecorderConst::waitThreadTimeoutMs))
432 {
433 if (!alreadyErr) {
434 errResources << "Something odd has been happen. Krita was unable to stop one of the Recorder WriterPool Threads. "
435 << "Thread Name: " << el.thread->objectName();
436 alreadyErr = true;
437 }
438 result = false;
439 }
440 }
441 }
442
443 writerPool.clear();
444 freeWriterId = -1;
445
446 if (!result)
447 Q_EMIT q->recorderStopWarning();
448
449 return result;
450 }
451
453 {
454 writerPool.reserve(recorderThreads.get());
455 while (static_cast<int>(recorderThreads.get()) > writerPool.size()) {
456 auto newWorkerId = writerPool.size();
457 freeWriterId = newWorkerId - 1; // Set the value to the last existing writerEl index ->
458 // The next call of searchForFreeWriter() will than automatically find newWorkerId
459
460 writerPool.append(WriterPoolEl(q, newWorkerId, canvas, settings, outputDir));
461
462 auto writerPtr = writerPool[newWorkerId].writer;
463 auto threadPtr = writerPool[newWorkerId].thread;
464 threadPtr->setObjectName(QString("Krita-Recorder-WriterPool#%1").arg(newWorkerId));
465 connect(q, SIGNAL(startCapturing(int, int)), writerPtr.get(), SLOT(onCaptureImage(int, int)));
466 connect(writerPtr.get(), SIGNAL(capturingDone(int, bool)), q, SLOT(onCapturingDone(int, bool)));
467 writerPtr->moveToThread(threadPtr.get());
468 threadPtr->start(QThread::IdlePriority);
469 }
470 }
471
473 {
474 auto j = freeWriterId + 1;
475 for(auto i = 0; i < writerPool.size(); i++, j++)
476 {
477 freeWriterId = j % writerPool.size();
478 if (writerPool[freeWriterId].thread->isRunning() && !writerPool[freeWriterId].inUse)
479 return;
480 }
481 freeWriterId = -1;
482 }
483
484 bool canStartCapture() // skip capture when use some tools.
485 {
487 return false;
489 return false;
490 return true;
491 }
492};
493
495 : d(new Private(this, recorderThreads))
496 , exporterSettings(es)
497{
498 d->timer.setTimerType(Qt::PreciseTimer);
499}
500
505
507{
508 // Restart writers if canvas changes
509 bool restart = d->timer.isActive();
510 if (restart) {
511 stop(false);
512 }
513
514 if (d->canvas) {
515 KoToolProxy *proxy = d->canvas->toolProxy();
516 KisToolProxy *kritaProxy = dynamic_cast<KisToolProxy*>(proxy);
517
518 disconnect(proxy, SIGNAL(toolChanged(QString)), this, SLOT(onToolChanged(QString)));
519 disconnect(kritaProxy, SIGNAL(toolPrimaryActionActivated(bool)), this, SLOT(onToolPrimaryActionActivated(bool)));
520 disconnect(d->canvas->image(), SIGNAL(sigImageUpdated(QRect)), this, SLOT(onImageModified()));
521 }
522
523 d->canvas = canvas;
524
525 if (d->canvas) {
526 KoToolProxy *proxy = d->canvas->toolProxy();
527 KisToolProxy *kritaProxy = dynamic_cast<KisToolProxy*>(proxy);
528
529 connect(proxy, SIGNAL(toolChanged(QString)), this, SLOT(onToolChanged(QString)),
530 Qt::DirectConnection); // need to handle it even if our event loop is not running
531 connect(kritaProxy, SIGNAL(toolPrimaryActionActivated(bool)), this, SLOT(onToolPrimaryActionActivated(bool)),
532 Qt::DirectConnection);
533 connect(d->canvas->image(), SIGNAL(sigImageUpdated(QRect)), this, SLOT(onImageModified()),
534 Qt::DirectConnection); // because it spams
535 }
536
537 if (restart) {
538 start(false);
539 }
540}
541
543{
544 // Restart writers if setup changes
545 bool restart = d->timer.isActive();
546 if (restart) {
547 stop(false);
548 }
549
550 d->settings = settings;
551 d->outputDir.setPath(settings.outputDirectory);
552
554
555 if (restart) {
556 start(false);
557 }
558}
559
560void RecorderWriterManager::start(bool toggleEnabled)
561{
562 if (d->timer.isActive())
563 return;
564
565 if (!d->canvas)
566 return;
567
568 d->enabled = true;
569 d->imageModified = false;
570
571 connect(&d->timer, SIGNAL (timeout()), this, SLOT (onTimer()));
573 d->interval = static_cast<int>(1000.0/static_cast<double>(exporterSettings.fps));
574 } else {
575 d->interval = static_cast<int>(qMax(d->settings.captureInterval, .1) * 1000.0);
576 }
578 d->timer.start(d->interval);
579 if (toggleEnabled) {
580 Q_EMIT started();
581 }
582}
583
584bool RecorderWriterManager::stop(bool toggleEnabled)
585{
586 if (!d->timer.isActive())
587 return true;
588
589 d->timer.stop();
590 auto result = d->clearWriterPool();
592 if (toggleEnabled) {
593 Q_EMIT stopped();
594 }
595 return result;
596}
597
598void RecorderWriterManager::setEnabled(bool enabled = false)
599{
600 d->enabled = enabled;
601}
602
604{
605 if (!d->enabled || !d->canvas)
606 return;
607
608 // take snapshots only if main window is active
609 // else some dialogs like filters may disappear when canvas->image()->lock() is called
610 if (qobject_cast<KisMainWindow*>(QApplication::activeWindow()) == nullptr)
611 return;
612
614 (d->canvas->image()->isIsolatingLayer() || d->canvas->image()->isIsolatingGroup())) {
615 return;
616 }
617
618 if (!d->imageModified)
619 return;
620
621 d->imageModified = false;
622
623 if (!d->canStartCapture())
624 return;
625
627
628 if (d->freeWriterId == -1)
629 {
630 Q_EMIT lowPerformanceWarning();
631 return;
632 }
633
634 d->writerPool[d->freeWriterId].inUse = true;
635 d->writerPool[d->freeWriterId].thread->setPriority(QThread::HighPriority);
638}
639
640void RecorderWriterManager::onCapturingDone(int workerId, bool success)
641{
642 if (workerId >= d->writerPool.size())
643 return;
644 d->writerPool[workerId].inUse = false;
645 d->writerPool[workerId].thread->setPriority(QThread::IdlePriority);
647 if (!success) {
648 stop();
649 Q_EMIT frameWriteFailed();
650 }
651}
652
654{
655 if (!d->enabled || !d->canStartCapture() )
656 return;
657
659 (d->canvas->image()->isIsolatingLayer() || d->canvas->image()->isIsolatingGroup()))
660 return;
661
662 d->imageModified = true;
663}
664
665void RecorderWriterManager::onToolChanged(const QString &toolId)
666{
667 d->isForceBlackTool = forceBlacklistedTools.contains(toolId);
668 d->isActivateBlackTool = activateBlacklistedTools.contains(toolId);
669}
670
672{
673 d->toolActivated = activated;
674}
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
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
const KoColorSpace * colorSpace() const
void unlock()
Definition kis_image.cc:805
KisPaintDeviceSP projection() const
qint32 width() const
void immediateLockForReadOnly()
Definition kis_image.cc:793
qint32 height() const
QRect bounds() const override
quint32 pixelSize() const
void makeCloneFromRough(KisPaintDeviceSP src, const QRect &minimalRect)
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 startCapturing(int writerId, int index)
void onToolPrimaryActionActivated(bool activated)
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
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)
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:105
#define warnResources
Definition kis_debug.h:85
#define dbgTools
Definition kis_debug.h:48
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)