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 , manager(m)
135 {}
136 Private() = delete;
137 Private(const Private&) = default;
138 Private(Private&&) = delete;
139 Private& operator=(const Private&) = default;
141
143 QByteArray imageBuffer;
146 QImage frame;
148 int partIndex = 0; // Consecutive file number
150 const QDir* outputDir;
152
157
159 {
160 KisImageSP image = canvas->image();
161
162 // Make sure we can actually capture something right now
163 {
164 QMutexLocker lock(manager->captureMutex());
165 if (manager->canStartCapture()) {
166 // we don't want image->barrierLock() because it will wait until
167 // the full stroke is finished
169 // Grab the next index while the capture mutex is held
171 } else {
172 return STATUS_BLOCKED;
173 }
174 }
175
176 // Create detached paint device that can be converted to target colorspace
177 KisPaintDeviceSP device = new KisPaintDevice(image->colorSpace());
178 device->makeCloneFromRough(image->projection(), image->bounds());
179 image->unlock();
180
181 const bool needSrgbConversion = [&]() {
183 || image->colorSpace()->colorModelId() != RGBAColorModelID) {
184 return true;
185 }
186 const bool hasPrimaries = image->colorSpace()->profile()->hasColorants();
188 if (hasPrimaries) {
189 const ColorPrimaries primaries = image->colorSpace()->profile()->getColorPrimaries();
190 if (gamma == TRC_IEC_61966_2_1 && primaries == PRIMARIES_ITU_R_BT_709_5) {
191 return false;
192 }
193 }
194 return true;
195 }();
196
197 if (targetCs && needSrgbConversion) {
198 device->convertTo(targetCs);
199 }
200
201 // truncate uneven image width/height making it even for subdivided size too
202 const quint32 bitmask = ~(0xFFFFFFFFu >> (31 - settings->resolution));
203 const quint32 width = image->width() & bitmask;
204 const quint32 height = image->height() & bitmask;
205 const int bufferSize = device->pixelSize() * width * height;
206
207 bool resize = imageBuffer.size() != bufferSize;
208 if (resize)
209 imageBuffer.resize(bufferSize);
210
211 if (resize || frameResolution != settings->resolution) {
212 const int divider = 1 << settings->resolution;
213 const int outWidth = width / divider;
214 const int outHeight = height / divider;
215 uchar *outData = reinterpret_cast<uchar *>(imageBuffer.data());
216
217 frame = QImage(outData, outWidth, outHeight, QImage::Format_ARGB32);
218 }
219
220 device->readBytes(reinterpret_cast<quint8 *>(imageBuffer.data()), 0, 0, width, height);
221
222 imageBufferWidth = width;
223 imageBufferHeight = height;
224 return STATUS_OK;
225 }
226
227 // Calculate ARGB average value using carry save adder:
228 // https://www.qt.io/blog/2009/01/20/50-scaling-of-argb32-image
229 inline quint32 avg(quint32 c1, quint32 c2)
230 {
231 return (((c1 ^ c2) & 0xfefefefeUL) >> 1) + (c1 & c2);
232 }
233
235 {
236 quint32 *buffer = reinterpret_cast<quint32 *>(imageBuffer.data());
237 quint32 *out = buffer;
238
239 for (int y = 0; y < imageBufferHeight; y += 2) {
240 const quint32 *in1 = buffer + y * imageBufferWidth;
241 const quint32 *in2 = in1 + imageBufferWidth;
242
243 for (int x = 0; x < imageBufferWidth; x += 2) {
244 *out = avg(
245 avg(in1[x], in1[x + 1]),
246 avg(in2[x], in2[x + 1])
247 );
248
249 ++out;
250 }
251 }
252
253 imageBufferWidth /= 2;
255 }
256
257 inline quint32 blendSourceOver(const int alpha, const quint32 source, const quint32 destination)
258 {
259 // co = αs x Cs + αb x Cb x (1 – αs)
260 // αo = 1, αb = 1
261
262 const int inverseAlpha = 255 - alpha;
263 return qRgb(
264 (alpha * qRed(source) + inverseAlpha * qRed(destination)) >> 8,
265 (alpha * qGreen(source) + inverseAlpha * qGreen(destination)) >> 8,
266 (alpha * qBlue(source) + inverseAlpha * qBlue(destination)) >> 8
267 );
268 }
269
271 {
272 const quint32 background = 0xFFFFFFFF;
273 quint32 *buffer = reinterpret_cast<quint32 *>(imageBuffer.data());
274 const quint32 *end = buffer + imageBufferWidth * imageBufferHeight;
275 while (buffer != end) {
276 const int alpha = qAlpha(*buffer);
277 switch (alpha) {
278 case 0xFF: // fully opaque
279 break;
280 case 0x00: // fully transparent - just replace to background
281 *buffer = background;
282 break;
283 default: // partly transparent - do color blending
284 *buffer = blendSourceOver(alpha, *buffer, background);
285 break;
286 }
287 ++buffer;
288 }
289 }
290
292 {
293 if (!outputDir->exists() && !outputDir->mkpath(settings->outputDirectory))
294 return STATUS_ERROR;
295
296 const QString fileName = QString("%1").arg(partIndex, 7, 10, QLatin1Char('0'));
297 const QString &filePath = QString("%1%2.%3").arg(settings->outputDirectory, fileName,
299
300 int factor = -1; // default value
301 switch (settings->format) {
303 factor = settings->quality; // 0...100
304 break;
306 factor = qBound(0, 100 - (settings->compression * 10), 100); // 0..10 -> 100..0
307 break;
308 }
309
310 if (!frame.save(filePath, RecorderFormatInfo::fileFormat(settings->format).data(), factor)) {
311 QFile(filePath).remove(); // remove corrupted frame
312 return STATUS_ERROR;
313 }
314
315 return STATUS_OK;
316 }
317
318};
319
321 unsigned int i,
323 const RecorderWriterSettings& s,
324 const QDir& d,
326 : d(new Private(c, s, d, m))
327 , id(i)
328{}
329
331{
332 delete d;
333}
334
336{
337 if (static_cast<int>(id) != writerId)
338 return;
339
340 int captureStatus = d->captureImage();
341 if (captureStatus != STATUS_OK) {
342 Q_EMIT capturingDone(id, captureStatus);
343 return;
344 }
345
346 // downscale image buffer
347 for (int res = 0; res < d->settings->resolution; ++res)
349
351
352 int writeStatus = d->writeFrame();
353
354 Q_EMIT capturingDone(id, writeStatus);
355}
356
357
359{
362
365 unsigned int i,
367 const RecorderWriterSettings& s,
368 const QDir& d
369 )
370 : thread(QThreadPtr::create(m))
371 , writer(RecorderWriterPtr::create(i, c, s, d, m))
372 {}
373
374 bool inUse{false};
377};
378
380
382{
383public:
385 : q(q_ptr)
386 , recorderThreads(rt)
387 {}
388
391 volatile std::atomic_bool enabled = false; // enable recording only for active documents
392 volatile std::atomic_bool imageModified = false;
393 volatile std::atomic_bool isForceBlackTool = false;
394 volatile std::atomic_bool isActivateBlackTool = false;
395 volatile std::atomic_bool toolActivated = false;
396 int partIndex = 0; // Consecutive file number
397 std::atomic_int freeWriterId = -1;
398 int interval = 1;
400 QTimer timer;
405
406 int findLastIndex(const QString &directory)
407 {
408 QElapsedTimer dbgTimer;
409 dbgTimer.start();
410
411 QDirIterator dirIterator(directory);
412 const QString &extension = RecorderFormatInfo::fileExtension(settings.format);
413 const QRegularExpression &snapshotFilePattern = RecorderConst::snapshotFilePatternFor(extension);
414
415 int recordIndex = -1;
416 while (dirIterator.hasNext()) {
417 dirIterator.next();
418
419 const QString &fileName = dirIterator.fileName();
420 const QRegularExpressionMatch &match = snapshotFilePattern.match(fileName);
421 if (match.hasMatch()) {
422 int index = match.captured(1).toInt();
423 if (recordIndex < index)
424 recordIndex = index;
425 }
426 }
427 dbgTools << "findLastPartNumber for" << directory << ": " << dbgTimer.elapsed() << "ms";
428
429 return recordIndex;
430 }
431
433 {
434 bool result = true;
435 bool alreadyWarn = false;
436 bool alreadyErr = false;
437 for(auto& el: writerPool)
438 {
439 el.thread->quit();
440 el.thread->wait(RecorderConst::waitThreadTimeoutMs);
441 disconnect(q, SIGNAL(startCapturing(int)), el.writer.get(), SLOT(onCaptureImage(int)));
442 disconnect(el.writer.get(), SIGNAL(capturingDone(int, int)), q, SLOT(onCapturingDone(int, int)));
443 if (el.thread->isRunning())
444 {
445 if (!alreadyWarn) {
446 warnResources << "One of the Recorder WriterPool threads has been blocked and has to be terminated. "
447 << "Thread Name: " << el.thread->objectName();
448 alreadyWarn = true;
449 }
450 el.thread->terminate();
451 if (!el.thread->wait(RecorderConst::waitThreadTimeoutMs))
452 {
453 if (!alreadyErr) {
454 errResources << "Something odd has been happen. Krita was unable to stop one of the Recorder WriterPool Threads. "
455 << "Thread Name: " << el.thread->objectName();
456 alreadyErr = true;
457 }
458 result = false;
459 }
460 }
461 }
462
463 writerPool.clear();
464 freeWriterId = -1;
465
466 if (!result)
467 Q_EMIT q->recorderStopWarning();
468
469 return result;
470 }
471
473 {
474 writerPool.reserve(recorderThreads.get());
475 while (static_cast<int>(recorderThreads.get()) > writerPool.size()) {
476 auto newWorkerId = writerPool.size();
477 freeWriterId = newWorkerId - 1; // Set the value to the last existing writerEl index ->
478 // The next call of searchForFreeWriter() will than automatically find newWorkerId
479
480 writerPool.append(WriterPoolEl(q, newWorkerId, canvas, settings, outputDir));
481
482 auto writerPtr = writerPool[newWorkerId].writer;
483 auto threadPtr = writerPool[newWorkerId].thread;
484 threadPtr->setObjectName(QString("Krita-Recorder-WriterPool#%1").arg(newWorkerId));
485 connect(q, SIGNAL(startCapturing(int)), writerPtr.get(), SLOT(onCaptureImage(int)));
486 connect(writerPtr.get(), SIGNAL(capturingDone(int, int)), q, SLOT(onCapturingDone(int, int)));
487 writerPtr->moveToThread(threadPtr.get());
488 threadPtr->start(QThread::IdlePriority);
489 }
490 }
491
493 {
494 auto j = freeWriterId + 1;
495 for(auto i = 0; i < writerPool.size(); i++, j++)
496 {
497 freeWriterId = j % writerPool.size();
498 if (writerPool[freeWriterId].thread->isRunning() && !writerPool[freeWriterId].inUse)
499 return;
500 }
501 freeWriterId = -1;
502 }
503};
504
506 : d(new Private(this, recorderThreads))
507 , exporterSettings(es)
508{
509 d->timer.setTimerType(Qt::PreciseTimer);
510}
511
516
518{
519 // Restart writers if canvas changes
520 bool restart = d->timer.isActive();
521 if (restart) {
522 stop(false);
523 }
524
525 if (d->canvas) {
526 KoToolProxy *proxy = d->canvas->toolProxy();
527 KisToolProxy *kritaProxy = dynamic_cast<KisToolProxy*>(proxy);
528
529 disconnect(proxy, SIGNAL(toolChanged(QString)), this, SLOT(onToolChanged(QString)));
530 disconnect(kritaProxy, SIGNAL(toolPrimaryActionActivated(bool)), this, SLOT(onToolPrimaryActionActivated(bool)));
531 disconnect(d->canvas->image(), SIGNAL(sigImageUpdated(QRect)), this, SLOT(onImageModified()));
532 }
533
534 d->canvas = canvas;
535
536 if (d->canvas) {
537 KoToolProxy *proxy = d->canvas->toolProxy();
538 KisToolProxy *kritaProxy = dynamic_cast<KisToolProxy*>(proxy);
539
540 connect(proxy, SIGNAL(toolChanged(QString)), this, SLOT(onToolChanged(QString)),
541 Qt::DirectConnection); // need to handle it even if our event loop is not running
542 connect(kritaProxy, SIGNAL(toolPrimaryActionActivated(bool)), this, SLOT(onToolPrimaryActionActivated(bool)),
543 Qt::DirectConnection);
544 connect(d->canvas->image(), SIGNAL(sigImageUpdated(QRect)), this, SLOT(onImageModified()),
545 Qt::DirectConnection); // because it spams
546 }
547
548 if (restart) {
549 start(false);
550 }
551}
552
554{
555 // Restart writers if setup changes
556 bool restart = d->timer.isActive();
557 if (restart) {
558 stop(false);
559 }
560
561 d->settings = settings;
562 d->outputDir.setPath(settings.outputDirectory);
563
565
566 if (restart) {
567 start(false);
568 }
569}
570
571void RecorderWriterManager::start(bool toggleEnabled)
572{
573 if (d->timer.isActive())
574 return;
575
576 if (!d->canvas)
577 return;
578
579 d->enabled = true;
580 d->imageModified = false;
581
582 connect(&d->timer, SIGNAL (timeout()), this, SLOT (onTimer()));
584 d->interval = static_cast<int>(1000.0/static_cast<double>(exporterSettings.fps));
585 } else {
586 d->interval = static_cast<int>(qMax(d->settings.captureInterval, .1) * 1000.0);
587 }
589 d->timer.start(d->interval);
590 if (toggleEnabled) {
591 Q_EMIT started();
592 }
593}
594
595bool RecorderWriterManager::stop(bool toggleEnabled)
596{
597 if (!d->timer.isActive())
598 return true;
599
600 d->timer.stop();
601 auto result = d->clearWriterPool();
603 if (toggleEnabled) {
604 Q_EMIT stopped();
605 }
606 return result;
607}
608
609void RecorderWriterManager::setEnabled(bool enabled = false)
610{
611 d->enabled = enabled;
612}
613
615{
616 if (d->isForceBlackTool)
617 return false;
619 return false;
620 return true;
621}
622
627
629{
630 return &d->captureMutex;
631}
632
634{
635 if (!d->enabled || !d->canvas)
636 return;
637
638 // take snapshots only if main window is active
639 // else some dialogs like filters may disappear when canvas->image()->lock() is called
640 if (qobject_cast<KisMainWindow*>(QApplication::activeWindow()) == nullptr)
641 return;
642
644 (d->canvas->image()->isIsolatingLayer() || d->canvas->image()->isIsolatingGroup())) {
645 return;
646 }
647
648 if (!d->imageModified)
649 return;
650
651 d->imageModified = false;
652
653 if (!canStartCapture())
654 return;
655
657
658 if (d->freeWriterId == -1)
659 {
660 Q_EMIT lowPerformanceWarning();
661 return;
662 }
663
664 d->writerPool[d->freeWriterId].inUse = true;
665 d->writerPool[d->freeWriterId].thread->setPriority(QThread::HighPriority);
668}
669
670void RecorderWriterManager::onCapturingDone(int workerId, int status)
671{
672 if (workerId >= d->writerPool.size())
673 return;
674 d->writerPool[workerId].inUse = false;
675 d->writerPool[workerId].thread->setPriority(QThread::IdlePriority);
677 if (status == RecorderWriter::STATUS_ERROR) {
678 stop();
679 Q_EMIT frameWriteFailed();
680 }
681}
682
684{
685 if (!d->enabled || !canStartCapture() )
686 return;
687
689 (d->canvas->image()->isIsolatingLayer() || d->canvas->image()->isIsolatingGroup()))
690 return;
691
692 d->imageModified = true;
693}
694
695void RecorderWriterManager::onToolChanged(const QString &toolId)
696{
697 QMutexLocker lock(&d->captureMutex);
698 d->isForceBlackTool = forceBlacklistedTools.contains(toolId);
699 d->isActivateBlackTool = activateBlacklistedTools.contains(toolId);
700}
701
703{
704 QMutexLocker lock(&d->captureMutex);
705 d->toolActivated = activated;
706}
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
void unlock()
Definition kis_image.cc:832
KisPaintDeviceSP projection() const
qint32 width() const
void immediateLockForReadOnly()
Definition kis_image.cc:820
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 startCapturing(int writerId)
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 onCapturingDone(int workerId, int status)
void onToolChanged(const QString &toolId)
Private(Private &&)=delete
QPointer< KisCanvas2 > canvas
RecorderWriterManager * manager
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)
Private(QPointer< KisCanvas2 > c, const RecorderWriterSettings &s, const QDir &d, RecorderWriterManager *m)
const RecorderWriterSettings * settings
Private(const Private &)=default
static constexpr int STATUS_OK
RecorderWriter()=delete
static constexpr int STATUS_ERROR
void onCaptureImage(int writerId)
Private *const d
void capturingDone(int writerId, int status)
static constexpr int STATUS_BLOCKED
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:109
#define warnResources
Definition kis_debug.h:89
#define dbgTools
Definition kis_debug.h:51
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(RecorderWriterManager *m, unsigned int i, QPointer< KisCanvas2 > c, const RecorderWriterSettings &s, const QDir &d)