5 #include "../../../C/Alloc.h"
\r
6 #include "../../../C/BwtSort.h"
\r
7 #include "../../../C/HuffEnc.h"
\r
9 #include "BZip2Crc.h"
\r
10 #include "BZip2Encoder.h"
\r
13 namespace NCompress {
\r
16 const int kMaxHuffmanLenForEncoding = 16; // it must be < kMaxHuffmanLen = 20
\r
18 static const UInt32 kBufferSize = (1 << 17);
\r
19 static const int kNumHuffPasses = 4;
\r
21 bool CThreadInfo::Alloc()
\r
23 if (m_BlockSorterIndex == 0)
\r
25 m_BlockSorterIndex = (UInt32 *)::BigAlloc(BLOCK_SORT_BUF_SIZE(kBlockSizeMax) * sizeof(UInt32));
\r
26 if (m_BlockSorterIndex == 0)
\r
32 m_Block = (Byte *)::MidAlloc(kBlockSizeMax * 5 + kBlockSizeMax / 10 + (20 << 10));
\r
35 m_MtfArray = m_Block + kBlockSizeMax;
\r
36 m_TempArray = m_MtfArray + kBlockSizeMax * 2 + 2;
\r
41 void CThreadInfo::Free()
\r
43 ::BigFree(m_BlockSorterIndex);
\r
44 m_BlockSorterIndex = 0;
\r
51 static THREAD_FUNC_DECL MFThread(void *threadCoderInfo)
\r
53 return ((CThreadInfo *)threadCoderInfo)->ThreadFunc();
\r
56 #define RINOK_THREAD(x) { WRes __result_ = (x); if(__result_ != 0) return __result_; }
\r
58 HRESULT CThreadInfo::Create()
\r
60 RINOK_THREAD(StreamWasFinishedEvent.Create());
\r
61 RINOK_THREAD(WaitingWasStartedEvent.Create());
\r
62 RINOK_THREAD(CanWriteEvent.Create());
\r
63 RINOK_THREAD(Thread.Create(MFThread, this));
\r
67 void CThreadInfo::FinishStream(bool needLeave)
\r
69 Encoder->StreamWasFinished = true;
\r
70 StreamWasFinishedEvent.Set();
\r
72 Encoder->CS.Leave();
\r
73 Encoder->CanStartWaitingEvent.Lock();
\r
74 WaitingWasStartedEvent.Set();
\r
77 DWORD CThreadInfo::ThreadFunc()
\r
81 Encoder->CanProcessEvent.Lock();
\r
82 Encoder->CS.Enter();
\r
83 if (Encoder->CloseThreads)
\r
85 Encoder->CS.Leave();
\r
88 if (Encoder->StreamWasFinished)
\r
94 bool needLeave = true;
\r
97 UInt32 blockSize = Encoder->ReadRleBlock(m_Block);
\r
98 m_PackSize = Encoder->m_InStream.GetProcessedSize();
\r
99 m_BlockIndex = Encoder->NextBlockIndex;
\r
100 if (++Encoder->NextBlockIndex == Encoder->NumThreads)
\r
101 Encoder->NextBlockIndex = 0;
\r
102 if (blockSize == 0)
\r
104 FinishStream(true);
\r
107 Encoder->CS.Leave();
\r
109 res = EncodeBlock3(blockSize);
\r
111 catch(const CInBufferException &e) { res = e.ErrorCode; }
\r
112 catch(const COutBufferException &e) { res = e.ErrorCode; }
\r
113 catch(...) { res = E_FAIL; }
\r
116 Encoder->Result = res;
\r
117 FinishStream(needLeave);
\r
125 CEncoder::CEncoder():
\r
127 m_OptimizeNumTables(false),
\r
128 m_BlockSizeMult(kBlockSizeMultMax)
\r
132 m_NumThreadsPrev = 0;
\r
138 CEncoder::~CEncoder()
\r
143 HRESULT CEncoder::Create()
\r
145 RINOK_THREAD(CanProcessEvent.CreateIfNotCreated());
\r
146 RINOK_THREAD(CanStartWaitingEvent.CreateIfNotCreated());
\r
147 if (ThreadsInfo != 0 && m_NumThreadsPrev == NumThreads)
\r
152 MtMode = (NumThreads > 1);
\r
153 m_NumThreadsPrev = NumThreads;
\r
154 ThreadsInfo = new CThreadInfo[NumThreads];
\r
155 if (ThreadsInfo == 0)
\r
156 return E_OUTOFMEMORY;
\r
158 catch(...) { return E_OUTOFMEMORY; }
\r
159 for (UInt32 t = 0; t < NumThreads; t++)
\r
161 CThreadInfo &ti = ThreadsInfo[t];
\r
165 HRESULT res = ti.Create();
\r
177 void CEncoder::Free()
\r
181 CloseThreads = true;
\r
182 CanProcessEvent.Set();
\r
183 for (UInt32 t = 0; t < NumThreads; t++)
\r
185 CThreadInfo &ti = ThreadsInfo[t];
\r
190 delete []ThreadsInfo;
\r
195 UInt32 CEncoder::ReadRleBlock(Byte *buffer)
\r
199 if (m_InStream.ReadByte(prevByte))
\r
201 UInt32 blockSize = m_BlockSizeMult * kBlockSizeStep - 1;
\r
203 buffer[i++] = prevByte;
\r
204 while (i < blockSize) // "- 1" to support RLE
\r
207 if (!m_InStream.ReadByte(b))
\r
211 if (numReps >= kRleModeRepSize)
\r
212 buffer[i++] = (Byte)(numReps - kRleModeRepSize);
\r
219 if (numReps <= kRleModeRepSize)
\r
221 else if (numReps == kRleModeRepSize + 255)
\r
223 buffer[i++] = (Byte)(numReps - kRleModeRepSize);
\r
227 // it's to support original BZip2 decoder
\r
228 if (numReps >= kRleModeRepSize)
\r
229 buffer[i++] = (Byte)(numReps - kRleModeRepSize);
\r
234 void CThreadInfo::WriteBits2(UInt32 value, UInt32 numBits)
\r
235 { m_OutStreamCurrent->WriteBits(value, numBits); }
\r
236 void CThreadInfo::WriteByte2(Byte b) { WriteBits2(b , 8); }
\r
237 void CThreadInfo::WriteBit2(bool v) { WriteBits2((v ? 1 : 0), 1); }
\r
238 void CThreadInfo::WriteCrc2(UInt32 v)
\r
240 for (int i = 0; i < 4; i++)
\r
241 WriteByte2(((Byte)(v >> (24 - i * 8))));
\r
244 void CEncoder::WriteBits(UInt32 value, UInt32 numBits)
\r
245 { m_OutStream.WriteBits(value, numBits); }
\r
246 void CEncoder::WriteByte(Byte b) { WriteBits(b , 8); }
\r
247 void CEncoder::WriteBit(bool v) { WriteBits((v ? 1 : 0), 1); }
\r
248 void CEncoder::WriteCrc(UInt32 v)
\r
250 for (int i = 0; i < 4; i++)
\r
251 WriteByte(((Byte)(v >> (24 - i * 8))));
\r
256 void CThreadInfo::EncodeBlock(const Byte *block, UInt32 blockSize)
\r
258 WriteBit2(false); // Randomised = false
\r
261 UInt32 origPtr = BlockSort(m_BlockSorterIndex, block, blockSize);
\r
262 // if (m_BlockSorterIndex[origPtr] != 0) throw 1;
\r
263 m_BlockSorterIndex[origPtr] = blockSize;
\r
264 WriteBits2(origPtr, kNumOrigBits);
\r
273 for (i = 0; i < 256; i++)
\r
275 for (i = 0; i < 16; i++)
\r
276 inUse16[i] = false;
\r
277 for (i = 0; i < blockSize; i++)
\r
278 inUse[block[i]] = true;
\r
279 for (i = 0; i < 256; i++)
\r
282 inUse16[i >> 4] = true;
\r
283 mtf.Buf[numInUse++] = (Byte)i;
\r
285 for (i = 0; i < 16; i++)
\r
286 WriteBit2(inUse16[i]);
\r
287 for (i = 0; i < 256; i++)
\r
288 if (inUse16[i >> 4])
\r
289 WriteBit2(inUse[i]);
\r
291 int alphaSize = numInUse + 2;
\r
293 Byte *mtfs = m_MtfArray;
\r
294 UInt32 mtfArraySize = 0;
\r
295 UInt32 symbolCounts[kMaxAlphaSize];
\r
297 for (int i = 0; i < kMaxAlphaSize; i++)
\r
298 symbolCounts[i] = 0;
\r
302 UInt32 rleSize = 0;
\r
304 const UInt32 *bsIndex = m_BlockSorterIndex;
\r
308 int pos = mtf.FindAndMove(block[bsIndex[i]]);
\r
313 while (rleSize != 0)
\r
316 mtfs[mtfArraySize++] = (Byte)(rleSize & 1);
\r
317 symbolCounts[rleSize & 1]++;
\r
322 mtfs[mtfArraySize++] = 0xFF;
\r
323 mtfs[mtfArraySize++] = (Byte)(pos - 0xFE);
\r
326 mtfs[mtfArraySize++] = (Byte)(pos + 1);
\r
327 symbolCounts[pos + 1]++;
\r
330 while (++i < blockSize);
\r
332 while (rleSize != 0)
\r
335 mtfs[mtfArraySize++] = (Byte)(rleSize & 1);
\r
336 symbolCounts[rleSize & 1]++;
\r
340 if (alphaSize < 256)
\r
341 mtfs[mtfArraySize++] = (Byte)(alphaSize - 1);
\r
344 mtfs[mtfArraySize++] = 0xFF;
\r
345 mtfs[mtfArraySize++] = (Byte)(alphaSize - 256);
\r
347 symbolCounts[alphaSize - 1]++;
\r
350 UInt32 numSymbols = 0;
\r
352 for (int i = 0; i < kMaxAlphaSize; i++)
\r
353 numSymbols += symbolCounts[i];
\r
356 int bestNumTables = kNumTablesMin;
\r
357 UInt32 bestPrice = 0xFFFFFFFF;
\r
358 UInt32 startPos = m_OutStreamCurrent->GetPos();
\r
359 Byte startCurByte = m_OutStreamCurrent->GetCurByte();
\r
360 for (int nt = kNumTablesMin; nt <= kNumTablesMax + 1; nt++)
\r
364 if(m_OptimizeNumTables)
\r
366 m_OutStreamCurrent->SetPos(startPos);
\r
367 m_OutStreamCurrent->SetCurState((startPos & 7), startCurByte);
\r
368 if (nt <= kNumTablesMax)
\r
371 numTables = bestNumTables;
\r
375 if (numSymbols < 200) numTables = 2;
\r
376 else if (numSymbols < 600) numTables = 3;
\r
377 else if (numSymbols < 1200) numTables = 4;
\r
378 else if (numSymbols < 2400) numTables = 5;
\r
379 else numTables = 6;
\r
382 WriteBits2(numTables, kNumTablesBits);
\r
384 UInt32 numSelectors = (numSymbols + kGroupSize - 1) / kGroupSize;
\r
385 WriteBits2(numSelectors, kNumSelectorsBits);
\r
388 UInt32 remFreq = numSymbols;
\r
393 UInt32 tFreq = remFreq / t;
\r
396 while (aFreq < tFreq) // && ge < alphaSize)
\r
397 aFreq += symbolCounts[ge++];
\r
399 if (ge - 1 > gs && t != numTables && t != 1 && (((numTables - t) & 1) == 1))
\r
400 aFreq -= symbolCounts[--ge];
\r
402 Byte *lens = Lens[t - 1];
\r
405 lens[i] = (i >= gs && i < ge) ? 0 : 1;
\r
406 while (++i < alphaSize);
\r
414 for (int pass = 0; pass < kNumHuffPasses; pass++)
\r
419 memset(Freqs[t], 0, sizeof(Freqs[t]));
\r
420 while(++t < numTables);
\r
428 UInt32 symbols[kGroupSize];
\r
432 UInt32 symbol = mtfs[mtfPos++];
\r
433 if (symbol >= 0xFF)
\r
434 symbol += mtfs[mtfPos++];
\r
435 symbols[i] = symbol;
\r
437 while (++i < kGroupSize && mtfPos < mtfArraySize);
\r
439 UInt32 bestPrice = 0xFFFFFFFF;
\r
443 const Byte *lens = Lens[t];
\r
447 price += lens[symbols[j]];
\r
449 if (price < bestPrice)
\r
451 m_Selectors[g] = (Byte)t;
\r
455 while(++t < numTables);
\r
456 UInt32 *freqs = Freqs[m_Selectors[g++]];
\r
459 freqs[symbols[j]]++;
\r
462 while (mtfPos < mtfArraySize);
\r
468 UInt32 *freqs = Freqs[t];
\r
473 while(++i < alphaSize);
\r
474 Huffman_Generate(freqs, Codes[t], Lens[t], kMaxAlphaSize, kMaxHuffmanLenForEncoding);
\r
476 while(++t < numTables);
\r
480 Byte mtfSel[kNumTablesMax];
\r
484 mtfSel[t] = (Byte)t;
\r
485 while(++t < numTables);
\r
491 Byte sel = m_Selectors[i];
\r
493 for (pos = 0; mtfSel[pos] != sel; pos++)
\r
496 for (; pos > 0; pos--)
\r
497 mtfSel[pos] = mtfSel[pos - 1];
\r
500 while(++i < numSelectors);
\r
507 const Byte *lens = Lens[t];
\r
508 UInt32 len = lens[0];
\r
509 WriteBits2(len, kNumLevelsBits);
\r
513 UInt32 level = lens[i];
\r
514 while (len != level)
\r
530 while (++i < alphaSize);
\r
532 while(++t < numTables);
\r
536 UInt32 groupSize = 0;
\r
537 UInt32 groupIndex = 0;
\r
538 const Byte *lens = 0;
\r
539 const UInt32 *codes = 0;
\r
543 UInt32 symbol = mtfs[mtfPos++];
\r
544 if (symbol >= 0xFF)
\r
545 symbol += mtfs[mtfPos++];
\r
546 if (groupSize == 0)
\r
548 groupSize = kGroupSize;
\r
549 int t = m_Selectors[groupIndex++];
\r
554 m_OutStreamCurrent->WriteBits(codes[symbol], lens[symbol]);
\r
556 while (mtfPos < mtfArraySize);
\r
559 if (!m_OptimizeNumTables)
\r
561 UInt32 price = m_OutStreamCurrent->GetPos() - startPos;
\r
562 if (price <= bestPrice)
\r
564 if (nt == kNumTablesMax)
\r
567 bestNumTables = nt;
\r
573 UInt32 CThreadInfo::EncodeBlockWithHeaders(const Byte *block, UInt32 blockSize)
\r
575 WriteByte2(kBlockSig0);
\r
576 WriteByte2(kBlockSig1);
\r
577 WriteByte2(kBlockSig2);
\r
578 WriteByte2(kBlockSig3);
\r
579 WriteByte2(kBlockSig4);
\r
580 WriteByte2(kBlockSig5);
\r
584 Byte prevByte = block[0];
\r
589 if (numReps == kRleModeRepSize)
\r
592 crc.UpdateByte(prevByte);
\r
605 while (++i < blockSize);
\r
606 UInt32 crcRes = crc.GetDigest();
\r
608 EncodeBlock(block, blockSize);
\r
612 void CThreadInfo::EncodeBlock2(const Byte *block, UInt32 blockSize, UInt32 numPasses)
\r
614 UInt32 numCrcs = m_NumCrcs;
\r
615 bool needCompare = false;
\r
617 UInt32 startBytePos = m_OutStreamCurrent->GetBytePos();
\r
618 UInt32 startPos = m_OutStreamCurrent->GetPos();
\r
619 Byte startCurByte = m_OutStreamCurrent->GetCurByte();
\r
620 Byte endCurByte = 0;
\r
622 if (numPasses > 1 && blockSize >= (1 << 10))
\r
624 UInt32 blockSize0 = blockSize / 2;
\r
625 for (;(block[blockSize0] == block[blockSize0 - 1] ||
\r
626 block[blockSize0 - 1] == block[blockSize0 - 2]) &&
\r
627 blockSize0 < blockSize; blockSize0++);
\r
628 if (blockSize0 < blockSize)
\r
630 EncodeBlock2(block, blockSize0, numPasses - 1);
\r
631 EncodeBlock2(block + blockSize0, blockSize - blockSize0, numPasses - 1);
\r
632 endPos = m_OutStreamCurrent->GetPos();
\r
633 endCurByte = m_OutStreamCurrent->GetCurByte();
\r
634 if ((endPos & 7) > 0)
\r
635 WriteBits2(0, 8 - (endPos & 7));
\r
636 m_OutStreamCurrent->SetCurState((startPos & 7), startCurByte);
\r
637 needCompare = true;
\r
641 UInt32 startBytePos2 = m_OutStreamCurrent->GetBytePos();
\r
642 UInt32 startPos2 = m_OutStreamCurrent->GetPos();
\r
643 UInt32 crcVal = EncodeBlockWithHeaders(block, blockSize);
\r
644 UInt32 endPos2 = m_OutStreamCurrent->GetPos();
\r
648 UInt32 size2 = endPos2 - startPos2;
\r
649 if (size2 < endPos - startPos)
\r
651 UInt32 numBytes = m_OutStreamCurrent->GetBytePos() - startBytePos2;
\r
652 Byte *buffer = m_OutStreamCurrent->GetStream();
\r
653 for (UInt32 i = 0; i < numBytes; i++)
\r
654 buffer[startBytePos + i] = buffer[startBytePos2 + i];
\r
655 m_OutStreamCurrent->SetPos(startPos + endPos2 - startPos2);
\r
656 m_NumCrcs = numCrcs;
\r
657 m_CRCs[m_NumCrcs++] = crcVal;
\r
661 m_OutStreamCurrent->SetPos(endPos);
\r
662 m_OutStreamCurrent->SetCurState((endPos & 7), endCurByte);
\r
667 m_NumCrcs = numCrcs;
\r
668 m_CRCs[m_NumCrcs++] = crcVal;
\r
672 HRESULT CThreadInfo::EncodeBlock3(UInt32 blockSize)
\r
674 CMsbfEncoderTemp outStreamTemp;
\r
675 outStreamTemp.SetStream(m_TempArray);
\r
676 outStreamTemp.Init();
\r
677 m_OutStreamCurrent = &outStreamTemp;
\r
681 EncodeBlock2(m_Block, blockSize, Encoder->NumPasses);
\r
684 if (Encoder->MtMode)
\r
685 Encoder->ThreadsInfo[m_BlockIndex].CanWriteEvent.Lock();
\r
687 for (UInt32 i = 0; i < m_NumCrcs; i++)
\r
688 Encoder->CombinedCrc.Update(m_CRCs[i]);
\r
689 Encoder->WriteBytes(m_TempArray, outStreamTemp.GetPos(), outStreamTemp.GetCurByte());
\r
690 HRESULT res = S_OK;
\r
692 if (Encoder->MtMode)
\r
694 UInt32 blockIndex = m_BlockIndex + 1;
\r
695 if (blockIndex == Encoder->NumThreads)
\r
698 if (Encoder->Progress)
\r
700 UInt64 unpackSize = Encoder->m_OutStream.GetProcessedSize();
\r
701 res = Encoder->Progress->SetRatioInfo(&m_PackSize, &unpackSize);
\r
704 Encoder->ThreadsInfo[blockIndex].CanWriteEvent.Set();
\r
710 void CEncoder::WriteBytes(const Byte *data, UInt32 sizeInBits, Byte lastByte)
\r
712 UInt32 bytesSize = (sizeInBits / 8);
\r
713 for (UInt32 i = 0; i < bytesSize; i++)
\r
714 m_OutStream.WriteBits(data[i], 8);
\r
715 WriteBits(lastByte, (sizeInBits & 7));
\r
719 HRESULT CEncoder::CodeReal(ISequentialInStream *inStream, ISequentialOutStream *outStream,
\r
720 const UInt64 * /* inSize */, const UInt64 * /* outSize */, ICompressProgressInfo *progress)
\r
723 Progress = progress;
\r
725 for (UInt32 t = 0; t < NumThreads; t++)
\r
729 CThreadInfo &ti = ThreadsInfo[t];
\r
732 RINOK(ti.StreamWasFinishedEvent.Reset());
\r
733 RINOK(ti.WaitingWasStartedEvent.Reset());
\r
734 RINOK(ti.CanWriteEvent.Reset());
\r
737 CThreadInfo &ti = ThreadsInfo;
\r
741 ti.m_OptimizeNumTables = m_OptimizeNumTables;
\r
744 return E_OUTOFMEMORY;
\r
748 if (!m_InStream.Create(kBufferSize))
\r
749 return E_OUTOFMEMORY;
\r
750 if (!m_OutStream.Create(kBufferSize))
\r
751 return E_OUTOFMEMORY;
\r
754 m_InStream.SetStream(inStream);
\r
757 m_OutStream.SetStream(outStream);
\r
758 m_OutStream.Init();
\r
760 CFlusher flusher(this);
\r
762 CombinedCrc.Init();
\r
764 NextBlockIndex = 0;
\r
765 StreamWasFinished = false;
\r
766 CloseThreads = false;
\r
767 CanStartWaitingEvent.Reset();
\r
770 WriteByte(kArSig0);
\r
771 WriteByte(kArSig1);
\r
772 WriteByte(kArSig2);
\r
773 WriteByte((Byte)(kArSig3 + m_BlockSizeMult));
\r
779 ThreadsInfo[0].CanWriteEvent.Set();
\r
781 CanProcessEvent.Set();
\r
783 for (t = 0; t < NumThreads; t++)
\r
784 ThreadsInfo[t].StreamWasFinishedEvent.Lock();
\r
785 CanProcessEvent.Reset();
\r
786 CanStartWaitingEvent.Set();
\r
787 for (t = 0; t < NumThreads; t++)
\r
788 ThreadsInfo[t].WaitingWasStartedEvent.Lock();
\r
789 CanStartWaitingEvent.Reset();
\r
803 UInt32 blockSize = ReadRleBlock(ti.m_Block);
\r
804 if (blockSize == 0)
\r
806 RINOK(ti.EncodeBlock3(blockSize));
\r
809 UInt64 packSize = m_InStream.GetProcessedSize();
\r
810 UInt64 unpackSize = m_OutStream.GetProcessedSize();
\r
811 RINOK(progress->SetRatioInfo(&packSize, &unpackSize));
\r
815 WriteByte(kFinSig0);
\r
816 WriteByte(kFinSig1);
\r
817 WriteByte(kFinSig2);
\r
818 WriteByte(kFinSig3);
\r
819 WriteByte(kFinSig4);
\r
820 WriteByte(kFinSig5);
\r
822 WriteCrc(CombinedCrc.GetDigest());
\r
826 STDMETHODIMP CEncoder::Code(ISequentialInStream *inStream, ISequentialOutStream *outStream,
\r
827 const UInt64 *inSize, const UInt64 *outSize, ICompressProgressInfo *progress)
\r
829 try { return CodeReal(inStream, outStream, inSize, outSize, progress); }
\r
830 catch(const CInBufferException &e) { return e.ErrorCode; }
\r
831 catch(const COutBufferException &e) { return e.ErrorCode; }
\r
832 catch(...) { return S_FALSE; }
\r
835 HRESULT CEncoder::SetCoderProperties(const PROPID *propIDs, const PROPVARIANT *props, UInt32 numProps)
\r
837 for(UInt32 i = 0; i < numProps; i++)
\r
839 const PROPVARIANT &prop = props[i];
\r
842 case NCoderPropID::kNumPasses:
\r
844 if (prop.vt != VT_UI4)
\r
845 return E_INVALIDARG;
\r
846 UInt32 numPasses = prop.ulVal;
\r
847 if (numPasses == 0)
\r
849 if (numPasses > kNumPassesMax)
\r
850 numPasses = kNumPassesMax;
\r
851 NumPasses = numPasses;
\r
852 m_OptimizeNumTables = (NumPasses > 1);
\r
855 case NCoderPropID::kDictionarySize:
\r
857 if (prop.vt != VT_UI4)
\r
858 return E_INVALIDARG;
\r
859 UInt32 dictionary = prop.ulVal / kBlockSizeStep;
\r
860 if (dictionary < kBlockSizeMultMin)
\r
861 dictionary = kBlockSizeMultMin;
\r
862 else if (dictionary > kBlockSizeMultMax)
\r
863 dictionary = kBlockSizeMultMax;
\r
864 m_BlockSizeMult = dictionary;
\r
867 case NCoderPropID::kNumThreads:
\r
870 if (prop.vt != VT_UI4)
\r
871 return E_INVALIDARG;
\r
872 NumThreads = prop.ulVal;
\r
873 if (NumThreads < 1)
\r
879 return E_INVALIDARG;
\r
886 STDMETHODIMP CEncoder::SetNumberOfThreads(UInt32 numThreads)
\r
888 NumThreads = numThreads;
\r
889 if (NumThreads < 1)
\r