Commit 7a47d58c67046fe65fd7978dc8a1dae7041f343e

Authored by Charles Otto
1 parent 50752fe9

Some code cleanup, also changes to thread pooling

Don't use the global thread pool for streams. Micro-managing the global thread
pool's active thread count has proven infeasible, therefore in order to avoid
thread based deadlocks, we don't use the global thread pool. Instead, we share
thread pools across sibling Stream transforms.

Misc. code cleanup and better last frame detection.
Showing 1 changed file with 226 additions and 139 deletions
openbr/plugins/stream.cpp
... ... @@ -160,9 +160,8 @@ class DataSource
160 160 public:
161 161 DataSource(int maxFrames=500)
162 162 {
  163 + // The sequence number of the last frame
163 164 final_frame = -1;
164   - last_issued = -2;
165   - last_received = -3;
166 165 for (int i=0; i < maxFrames;i++)
167 166 {
168 167 allFrames.addItem(new FrameData());
... ... @@ -181,57 +180,64 @@ public:
181 180 }
182 181  
183 182 // non-blocking version of getFrame
184   - FrameData * tryGetFrame()
  183 + // Returns a NULL FrameData if too many frames are out, or the
  184 + // data source is broken. Sets last_frame to true iff the FrameData
  185 + // returned is the last valid frame, and the data source is now broken.
  186 + FrameData * tryGetFrame(bool & last_frame)
185 187 {
  188 + last_frame = false;
  189 +
  190 + if (is_broken) {
  191 + return NULL;
  192 + }
  193 +
  194 + // Try to get a FrameData from the pool, if we can't it means too many
  195 + // frames are already out, and we will return NULL to indicate failure
186 196 FrameData * aFrame = allFrames.tryGetItem();
187   - if (aFrame == NULL)
  197 + if (aFrame == NULL) {
188 198 return NULL;
  199 + }
189 200  
190 201 aFrame->data.clear();
191 202 aFrame->sequenceNumber = -1;
192 203  
  204 + // Try to read a frame, if this returns false the data source is broken
193 205 bool res = getNext(*aFrame);
194 206  
195   - // The datasource broke.
196   - if (!res) {
  207 + // The datasource broke, update final_frame
  208 + if (!res)
  209 + {
  210 + QMutexLocker lock(&last_frame_update);
  211 + final_frame = lookAhead.back()->sequenceNumber;
197 212 allFrames.addItem(aFrame);
  213 + }
  214 + else lookAhead.push_back(aFrame);
198 215  
199   - QMutexLocker lock(&last_frame_update);
200   - // Did we already receive the last frame?
201   - final_frame = last_issued;
  216 + FrameData * rVal = lookAhead.first();
  217 + lookAhead.pop_front();
202 218  
203   - // We got the last frame before the data source broke,
204   - // better pulse lastReturned
205   - if (final_frame == last_received) {
206   - lastReturned.wakeAll();
207   - }
208   - else if (final_frame < last_received)
209   - std::cout << "Bad last frame " << final_frame << " but received " << last_received << std::endl;
210 219  
211   - return NULL;
  220 + if (rVal->sequenceNumber == final_frame) {
  221 + last_frame = true;
  222 + is_broken = true;
212 223 }
213   - last_issued = aFrame->sequenceNumber;
214   - return aFrame;
  224 +
  225 + return rVal;
215 226 }
216 227  
217   - // Returns true if the frame returned was the last
  228 + // Return a frame to the pool, returns true if the frame returned was the last
218 229 // frame issued, false otherwise
219 230 bool returnFrame(FrameData * inputFrame)
220 231 {
  232 + int frameNumber = inputFrame->sequenceNumber;
  233 +
221 234 allFrames.addItem(inputFrame);
222 235  
223 236 bool rval = false;
224 237  
225 238 QMutexLocker lock(&last_frame_update);
226   - last_received = inputFrame->sequenceNumber;
227   -
228   - if (inputFrame->sequenceNumber == final_frame) {
229   - // This is the reserveThread that matches the releaseThread in
230   - // Stream::projectUpdate, we do it here to prevent a gap where the
231   - // thread count is still increased, but a stream worker thread is not
232   - // running, which might allow something to start that shouldn't
233   - // start yet.
234   - QThreadPool::globalInstance()->reserveThread();
  239 +
  240 + if (frameNumber == final_frame) {
235 241 // We just received the last frame, better pulse
236 242 lastReturned.wakeAll();
237 243 rval = true;
... ... @@ -246,17 +252,57 @@ public:
246 252 lastReturned.wait(&last_frame_update);
247 253 }
248 254  
249   - virtual void close() = 0;
250   - virtual bool open(Template & output, int start_index=0) = 0;
251   - virtual bool isOpen() = 0;
  255 + bool open(Template & output, int start_index = 0)
  256 + {
  257 + is_broken = false;
  258 + // The last frame isn't initialized yet
  259 + final_frame = -1;
  260 + // Start our sequence numbers from the input index
  261 + next_sequence_number = start_index;
  262 +
  263 + // Actually open the data source
  264 + bool open_res = concreteOpen(output);
  265 +
  266 + // We couldn't open the data source
  267 + if (!open_res) {
  268 + is_broken = true;
  269 + return false;
  270 + }
  271 +
  272 + // Try to get a frame from the global pool
  273 + FrameData * firstFrame = allFrames.tryGetItem();
  274 +
  275 + // If this fails, things have gone pretty badly.
  276 + if (firstFrame == NULL) {
  277 + is_broken = true;
  278 + return false;
  279 + }
  280 +
  281 + // Read a frame from the video source
  282 + bool res = getNext(*firstFrame);
  283 +
  284 + // the data source broke already, we couldn't even get one frame
  285 + // from it.
  286 + if (!res) {
  287 + is_broken = true;
  288 + return false;
  289 + }
  290 +
  291 + lookAhead.append(firstFrame);
  292 + return true;
  293 + }
252 294  
  295 + virtual bool isOpen()=0;
  296 + virtual bool concreteOpen(Template & output) = 0;
253 297 virtual bool getNext(FrameData & input) = 0;
  298 + virtual void close() = 0;
254 299  
  300 + int next_sequence_number;
255 301 protected:
256 302 DoubleBuffer allFrames;
257 303 int final_frame;
258   - int last_issued;
259   - int last_received;
  304 + bool is_broken;
  305 + QList<FrameData *> lookAhead;
260 306  
261 307 QWaitCondition lastReturned;
262 308 QMutex last_frame_update;
... ... @@ -268,13 +314,8 @@ class VideoDataSource : public DataSource
268 314 public:
269 315 VideoDataSource(int maxFrames) : DataSource(maxFrames) {}
270 316  
271   - bool open(Template &input, int start_index=0)
  317 + bool concreteOpen(Template &input)
272 318 {
273   - final_frame = -1;
274   - last_issued = -2;
275   - last_received = -3;
276   -
277   - next_idx = start_index;
278 319 basis = input;
279 320 bool is_int = false;
280 321 int anInt = input.file.name.toInt(&is_int);
... ... @@ -309,25 +350,31 @@ private:
309 350 return false;
310 351  
311 352 output.data.append(Template(basis.file));
312   - output.data.last().append(cv::Mat());
  353 + output.data.last().m() = cv::Mat();
313 354  
314   - output.sequenceNumber = next_idx;
315   - next_idx++;
  355 + output.sequenceNumber = next_sequence_number;
  356 + next_sequence_number++;
316 357  
317   - bool res = video.read(output.data.last().last());
318   - output.data.last().last() = output.data.last().last().clone();
  358 + cv::Mat temp;
  359 + bool res = video.read(temp);
319 360  
320 361 if (!res) {
  362 + output.data.last().m() = cv::Mat();
321 363 close();
322 364 return false;
323 365 }
  366 +
  367 + // This clone is critical, if we don't do it then the matrix will
  368 + // be an alias of an internal buffer of the video source, leading
  369 + // to various problems later.
  370 + output.data.last().m() = temp.clone();
  371 +
324 372 output.data.last().file.set("FrameNumber", output.sequenceNumber);
325 373 return true;
326 374 }
327 375  
328 376 cv::VideoCapture video;
329 377 Template basis;
330   - int next_idx;
331 378 };
332 379  
333 380 // Given a template as input, return its matrices one by one on subsequent calls
... ... @@ -337,21 +384,16 @@ class TemplateDataSource : public DataSource
337 384 public:
338 385 TemplateDataSource(int maxFrames) : DataSource(maxFrames)
339 386 {
340   - current_idx = INT_MAX;
  387 + current_matrix_idx = INT_MAX;
341 388 data_ok = false;
342 389 }
343   - bool data_ok;
344 390  
345   - bool open(Template &input, int start_index=0)
  391 + bool concreteOpen(Template &input)
346 392 {
347 393 basis = input;
348   - current_idx = 0;
349   - next_sequence = start_index;
350   - final_frame = -1;
351   - last_issued = -2;
352   - last_received = -3;
  394 + current_matrix_idx = 0;
353 395  
354   - data_ok = current_idx < basis.size();
  396 + data_ok = current_matrix_idx < basis.size();
355 397 return data_ok;
356 398 }
357 399  
... ... @@ -361,39 +403,41 @@ public:
361 403  
362 404 void close()
363 405 {
364   - current_idx = INT_MAX;
  406 + current_matrix_idx = INT_MAX;
365 407 basis.clear();
366 408 }
367 409  
368 410 private:
369 411 bool getNext(FrameData & output)
370 412 {
371   - data_ok = current_idx < basis.size();
  413 + data_ok = current_matrix_idx < basis.size();
372 414 if (!data_ok)
373 415 return false;
374 416  
375   - output.data.append(basis[current_idx]);
376   - current_idx++;
  417 + output.data.append(basis[current_matrix_idx]);
  418 + current_matrix_idx++;
377 419  
378   - output.sequenceNumber = next_sequence;
379   - next_sequence++;
  420 + output.sequenceNumber = next_sequence_number;
  421 + next_sequence_number++;
380 422  
381 423 output.data.last().file.set("FrameNumber", output.sequenceNumber);
382 424 return true;
383 425 }
384 426  
385 427 Template basis;
386   - int current_idx;
387   - int next_sequence;
  428 + // Index of the next matrix to output from the template
  429 + int current_matrix_idx;
  430 +
  431 + // is current_matrix_idx in bounds?
  432 + bool data_ok;
388 433 };
389 434  
390   -// Given a template as input, create a VideoDataSource or a TemplateDataSource
391   -// depending on whether or not it looks like the input template has already
392   -// loaded frames into memory.
  435 +// Given a templatelist as input, create appropriate data source for each
  436 +// individual template
393 437 class DataSourceManager : public DataSource
394 438 {
395 439 public:
396   - DataSourceManager()
  440 + DataSourceManager() : DataSource(500)
397 441 {
398 442 actualSource = NULL;
399 443 }
... ... @@ -414,29 +458,25 @@ public:
414 458  
415 459 bool open(TemplateList & input)
416 460 {
417   - currentIdx = 0;
  461 + current_template_idx = 0;
418 462 templates = input;
419 463  
420   - return open(templates[currentIdx]);
  464 + return DataSource::open(templates[current_template_idx]);
421 465 }
422 466  
423   - bool open(Template & input, int start_index=0)
  467 + bool concreteOpen(Template & input)
424 468 {
425 469 close();
426   - final_frame = -1;
427   - last_issued = -2;
428   - last_received = -3;
429   - next_frame = start_index;
430 470  
431 471 // Input has no matrices? Its probably a video that hasn't been loaded yet
432 472 if (input.empty()) {
433 473 actualSource = new VideoDataSource(0);
434   - actualSource->open(input, next_frame);
  474 + actualSource->concreteOpen(input);
435 475 }
436 476 else {
437 477 // create frame dealer
438 478 actualSource = new TemplateDataSource(0);
439   - actualSource->open(input, next_frame);
  479 + actualSource->concreteOpen(input);
440 480 }
441 481 if (!isOpen()) {
442 482 delete actualSource;
... ... @@ -449,30 +489,47 @@ public:
449 489 bool isOpen() { return !actualSource ? false : actualSource->isOpen(); }
450 490  
451 491 protected:
452   - int currentIdx;
453   - int next_frame;
  492 + // Index of the template in the templatelist we are currently reading from
  493 + int current_template_idx;
  494 +
454 495 TemplateList templates;
455 496 DataSource * actualSource;
456 497 bool getNext(FrameData & output)
457 498 {
458 499 bool res = actualSource->getNext(output);
  500 + output.sequenceNumber = next_sequence_number;
  501 +
459 502 if (res) {
460   - next_frame = output.sequenceNumber+1;
  503 + output.data.last().file.set("FrameNumber", output.sequenceNumber);
  504 + next_sequence_number++;
  505 + if (output.data.last().last().empty())
  506 + qDebug("broken matrix");
461 507 return true;
462 508 }
463 509  
  510 +
464 511 while(!res) {
465   - currentIdx++;
  512 + output.data.clear();
  513 + current_template_idx++;
466 514  
467   - if (currentIdx >= templates.size())
  515 + // No more templates? We're done
  516 + if (current_template_idx >= templates.size())
468 517 return false;
469   - bool open_res = open(templates[currentIdx], next_frame);
  518 +
  519 + // open the next data source
  520 + bool open_res = concreteOpen(templates[current_template_idx]);
470 521 if (!open_res)
471 522 return false;
  523 +
  524 + // get a frame from it
472 525 res = actualSource->getNext(output);
473 526 }
  527 + output.sequenceNumber = next_sequence_number++;
  528 + output.data.last().file.set("FrameNumber", output.sequenceNumber);
  529 +
  530 + if (output.data.last().last().empty())
  531 + qDebug("broken matrix");
474 532  
475   - next_frame = output.sequenceNumber+1;
476 533 return res;
477 534 }
478 535  
... ... @@ -480,9 +537,14 @@ protected:
480 537  
481 538 class ProcessingStage;
482 539  
483   -class BasicLoop : public QRunnable
  540 +class BasicLoop : public QRunnable, public QFutureInterface<void>
484 541 {
485 542 public:
  543 + BasicLoop()
  544 + {
  545 + this->reportStarted();
  546 + }
  547 +
486 548 void run();
487 549  
488 550 QList<ProcessingStage *> * stages;
... ... @@ -508,13 +570,13 @@ public:
508 570 int stage_id;
509 571  
510 572 virtual void reset()=0;
511   -
512 573 protected:
513 574 int thread_count;
514 575  
515 576 SharedBuffer * inputBuffer;
516 577 ProcessingStage * nextStage;
517 578 QList<ProcessingStage *> * stages;
  579 + QThreadPool * threads;
518 580 Transform * transform;
519 581  
520 582 };
... ... @@ -533,6 +595,7 @@ void BasicLoop::run()
533 595 current_idx++;
534 596 current_idx = current_idx % stages->size();
535 597 }
  598 + this->reportFinished();
536 599 }
537 600  
538 601 class MultiThreadStage : public ProcessingStage
... ... @@ -567,7 +630,6 @@ public:
567 630 }
568 631 };
569 632  
570   -
571 633 class SingleThreadStage : public ProcessingStage
572 634 {
573 635 public:
... ... @@ -630,18 +692,20 @@ public:
630 692 lock.unlock();
631 693  
632 694 if (newItem)
633   - {
634   - BasicLoop * next = new BasicLoop();
635   - next->stages = stages;
636   - next->start_idx = this->stage_id;
637   - next->startItem = newItem;
638   -
639   - QThreadPool::globalInstance()->start(next, stages->size() - stage_id);
640   - }
  695 + startThread(newItem);
641 696  
642 697 return input;
643 698 }
644 699  
  700 + void startThread(br::FrameData * newItem)
  701 + {
  702 + BasicLoop * next = new BasicLoop();
  703 + next->stages = stages;
  704 + next->start_idx = this->stage_id;
  705 + next->startItem = newItem;
  706 + this->threads->start(next, stages->size() - stage_id);
  707 + }
  708 +
645 709  
646 710 // Calledfrom a different thread than run.
647 711 bool tryAcquireNextStage(FrameData *& input)
... ... @@ -677,7 +741,7 @@ public:
677 741 };
678 742  
679 743 // No input buffer, instead we draw templates from some data source
680   -// Will be operated by the main thread for the stream
  744 +// Will be operated by the main thread for the stream. starts threads
681 745 class FirstStage : public SingleThreadStage
682 746 {
683 747 public:
... ... @@ -687,44 +751,51 @@ public:
687 751  
688 752 FrameData * run(FrameData * input, bool & should_continue)
689 753 {
690   - // Is there anything on our input buffer? If so we should start a thread with that.
  754 + // Try to get a frame from the datasource
691 755 QWriteLocker lock(&statusLock);
692   - input = dataSource.tryGetFrame();
693   - // Datasource broke?
694   - if (!input)
  756 + bool last_frame = false;
  757 + input = dataSource.tryGetFrame(last_frame);
  758 +
  759 + // Datasource broke, or is currently out of frames?
  760 + if (!input || last_frame)
695 761 {
  762 + // We will just stop and not continue.
696 763 currentStatus = STOPPING;
697   - should_continue = false;
698   - return NULL;
  764 + if (!input) {
  765 + should_continue = false;
  766 + return NULL;
  767 + }
699 768 }
700 769 lock.unlock();
701   -
  770 + // Can we enter the next stage?
702 771 should_continue = nextStage->tryAcquireNextStage(input);
703 772  
704   - BasicLoop * next = new BasicLoop();
705   - next->stages = stages;
706   - next->start_idx = this->stage_id;
707   - next->startItem = NULL;
708   -
709   - QThreadPool::globalInstance()->start(next, stages->size() - stage_id);
  773 + // We are exiting leaving this stage, should we start another
  774 + // thread here? Normally we will always re-queue a thread on
  775 + // the first stage, but if we received the last frame there is
  776 + // no need to.
  777 + if (!last_frame) {
  778 + startThread(NULL);
  779 + }
710 780  
711 781 return input;
712 782 }
713 783  
714   - // Calledfrom a different thread than run.
  784 + // The last stage, trying to access the first stage
715 785 bool tryAcquireNextStage(FrameData *& input)
716 786 {
  787 + // Return the frame, was it the last one?
717 788 bool was_last = dataSource.returnFrame(input);
718 789 input = NULL;
  790 +
  791 + // OK we won't continue.
719 792 if (was_last) {
720 793 return false;
721 794 }
722 795  
723   - if (!dataSource.isOpen())
724   - return false;
725   -
726 796 QReadLocker lock(&statusLock);
727   - // Thread is already running, we should just return
  797 + // A thread is already in the first stage,
  798 + // we should just return
728 799 if (currentStatus == STARTING)
729 800 {
730 801 return false;
... ... @@ -747,6 +818,7 @@ public:
747 818  
748 819 };
749 820  
  821 +// starts threads
750 822 class LastStage : public SingleThreadStage
751 823 {
752 824 public:
... ... @@ -777,11 +849,14 @@ public:
777 849 }
778 850 next_target = input->sequenceNumber + 1;
779 851  
  852 + // add the item to our output buffer
780 853 collectedOutput.append(input->data);
781 854  
  855 + // Can we enter the read stage?
782 856 should_continue = nextStage->tryAcquireNextStage(input);
783 857  
784   - // Is there anything on our input buffer? If so we should start a thread with that.
  858 + // Is there anything on our input buffer? If so we should start a thread
  859 + // in this stage to process that frame.
785 860 QWriteLocker lock(&statusLock);
786 861 FrameData * newItem = inputBuffer->tryGetItem();
787 862 if (!newItem)
... ... @@ -791,23 +866,18 @@ public:
791 866 lock.unlock();
792 867  
793 868 if (newItem)
794   - {
795   - BasicLoop * next = new BasicLoop();
796   - next->stages = stages;
797   - next->start_idx = this->stage_id;
798   - next->startItem = newItem;
799   -
800   - QThreadPool::globalInstance()->start(next, stages->size() - stage_id);
801   - }
  869 + startThread(newItem);
802 870  
803 871 return input;
804 872 }
805 873 };
806 874  
  875 +
807 876 class StreamTransform : public CompositeTransform
808 877 {
809 878 Q_OBJECT
810 879 public:
  880 +
811 881 void train(const TemplateList & data)
812 882 {
813 883 foreach(Transform * transform, transforms) {
... ... @@ -837,23 +907,17 @@ public:
837 907 bool res = readStage->dataSource.open(dst);
838 908 if (!res) return;
839 909  
  910 + // Start the first thread in the stream.
840 911 readStage->currentStatus = SingleThreadStage::STARTING;
  912 + readStage->startThread(NULL);
841 913  
842   - BasicLoop loop;
843   - loop.stages = &this->processingStages;
844   - loop.start_idx = 0;
845   - loop.startItem = NULL;
846   - loop.setAutoDelete(false);
847   -
848   - // Create a thread, then allow it to start by releasing. We queue the thread
849   - // first so that any lower priority threads that are already queued don't start
850   - // instead of the new one.
851   - QThreadPool::globalInstance()->start(&loop, processingStages.size() - processingStages[0]->stage_id);
852   - QThreadPool::globalInstance()->releaseThread();
853   -
854   - // Wait for the end.
  914 + // Wait for the stream to reach the last frame available from
  915 + // the data source.
855 916 readStage->dataSource.waitLast();
856 917  
  918 + // Now that there are no more incoming frames, call finalize
  919 + // on each transform in turn to collect any last templates
  920 + // they wish to issue.
857 921 TemplateList final_output;
858 922  
859 923 // Push finalize through the stages
... ... @@ -869,7 +933,8 @@ public:
869 933 final_output.append(output_set);
870 934 }
871 935  
872   - // dst is set to all output received by the final stage
  936 + // dst is set to all output received by the final stage, along
  937 + // with anything output via the calls to finalize.
873 938 dst = collectionStage->getOutput();
874 939 dst.append(final_output);
875 940  
... ... @@ -881,7 +946,8 @@ public:
881 946 virtual void finalize(TemplateList & output)
882 947 {
883 948 (void) output;
884   - // Not handling this yet -cao
  949 + // Nothing in particular to do here, stream calls finalize
  950 + // on all child transforms as part of projectUpdate
885 951 }
886 952  
887 953 // Create and link stages
... ... @@ -889,6 +955,19 @@ public:
889 955 {
890 956 if (transforms.isEmpty()) return;
891 957  
  958 + // We share a thread pool across streams attached to the same
  959 + // parent tranform, retrieve or create a thread pool based
  960 + // on our parent transform.
  961 + QMutexLocker poolLock(&poolsAccess);
  962 + QHash<QObject *, QThreadPool *>::Iterator it;
  963 + if (!pools.contains(this->parent())) {
  964 + it = pools.insert(this->parent(), new QThreadPool(this));
  965 + it.value()->setMaxThreadCount(Globals->parallelism);
  966 + }
  967 + else it = pools.find(this->parent());
  968 + threads = it.value();
  969 + poolLock.unlock();
  970 +
892 971 stage_variance.reserve(transforms.size());
893 972 foreach (const br::Transform *transform, transforms) {
894 973 stage_variance.append(transform->timeVarying());
... ... @@ -899,6 +978,7 @@ public:
899 978 processingStages.push_back(readStage);
900 979 readStage->stage_id = 0;
901 980 readStage->stages = &this->processingStages;
  981 + readStage->threads = this->threads;
902 982  
903 983 int next_stage_id = 1;
904 984  
... ... @@ -906,9 +986,7 @@ public:
906 986 for (int i =0; i < transforms.size(); i++)
907 987 {
908 988 if (stage_variance[i])
909   - {
910 989 processingStages.append(new SingleThreadStage(prev_stage_variance));
911   - }
912 990 else
913 991 processingStages.append(new MultiThreadStage(Globals->parallelism));
914 992  
... ... @@ -919,6 +997,7 @@ public:
919 997 processingStages[i]->nextStage = processingStages[i+1];
920 998  
921 999 processingStages.last()->stages = &this->processingStages;
  1000 + processingStages.last()->threads = this->threads;
922 1001  
923 1002 processingStages.last()->transform = transforms[i];
924 1003 prev_stage_variance = stage_variance[i];
... ... @@ -928,6 +1007,7 @@ public:
928 1007 processingStages.append(collectionStage);
929 1008 collectionStage->stage_id = next_stage_id;
930 1009 collectionStage->stages = &this->processingStages;
  1010 + collectionStage->threads = this->threads;
931 1011  
932 1012 processingStages[processingStages.size() - 2]->nextStage = collectionStage;
933 1013  
... ... @@ -950,6 +1030,10 @@ protected:
950 1030  
951 1031 QList<ProcessingStage *> processingStages;
952 1032  
  1033 + static QHash<QObject *, QThreadPool *> pools;
  1034 + static QMutex poolsAccess;
  1035 + QThreadPool * threads;
  1036 +
953 1037 void _project(const Template &src, Template &dst) const
954 1038 {
955 1039 (void) src; (void) dst;
... ... @@ -962,6 +1046,9 @@ protected:
962 1046 }
963 1047 };
964 1048  
  1049 +QHash<QObject *, QThreadPool *> StreamTransform::pools;
  1050 +QMutex StreamTransform::poolsAccess;
  1051 +
965 1052 BR_REGISTER(Transform, StreamTransform)
966 1053  
967 1054  
... ...