sl@0: // Copyright (c) 2007-2009 Nokia Corporation and/or its subsidiary(-ies). sl@0: // All rights reserved. sl@0: // This component and the accompanying materials are made available sl@0: // under the terms of the License "Eclipse Public License v1.0" sl@0: // which accompanies this distribution, and is available sl@0: // at the URL "http://www.eclipse.org/legal/epl-v10.html". sl@0: // sl@0: // Initial Contributors: sl@0: // Nokia Corporation - initial contribution. sl@0: // sl@0: // Contributors: sl@0: // sl@0: // Description: sl@0: // sl@0: sl@0: #include "scsiprot.h" sl@0: #include "usbmsshared.h" sl@0: #include "rwdrivethread.h" sl@0: #include "massstoragedebug.h" sl@0: sl@0: // --- sl@0: sl@0: #ifdef PRINT_MSDC_MULTITHREADED_READ_INFO sl@0: #define __MT_READ_PRINT(t) {RDebug::Print(t);} sl@0: #define __MT_READ_PRINT1(t,a) {RDebug::Print(t,a);} sl@0: #define __MT_READ_PRINT2(t,a,b) {RDebug::Print(t,a,b);} sl@0: #else sl@0: #define __MT_READ_PRINT(t) sl@0: #define __MT_READ_PRINT1(t,a) sl@0: #define __MT_READ_PRINT2(t,a,b) sl@0: #endif // PRINT_MSDC_MULTITHREADED_READ_INFO sl@0: sl@0: sl@0: #ifdef MSDC_MULTITHREADED sl@0: sl@0: TBlockDesc::TBlockDesc() sl@0: :iBuf((TUint8 *)NULL,0,0) sl@0: { sl@0: } sl@0: sl@0: void TBlockDesc::SetPtr(TPtr8& aDes) sl@0: { sl@0: iBuf.Set(aDes); sl@0: } sl@0: sl@0: sl@0: TBlockDescBuffer::TBlockDescBuffer() sl@0: { sl@0: iDescReadPtr = &iDesc1; sl@0: iDescWritePtr = &iDesc2; sl@0: } sl@0: sl@0: void TBlockDescBuffer::SetUpReadBuf(TPtr8& aDes1, TPtr8& aDes2) sl@0: { sl@0: iDesc1.SetPtr(aDes1); sl@0: iDesc2.SetPtr(aDes2); sl@0: iDescReadPtr = &iDesc1; sl@0: iDescWritePtr = &iDesc2; sl@0: } sl@0: sl@0: sl@0: //----------------------------------------------- sl@0: sl@0: /** sl@0: Construct a CThreadContext object. sl@0: sl@0: @param aName The name to be assigned to this thread. sl@0: @param aThreadFunction Function to be called when thread is initially scheduled. sl@0: @param aOwner Pointer to the object owning the thread. Used as the parameter to aThreadFunction. sl@0: */ sl@0: CThreadContext* CThreadContext::NewL(const TDesC& aName, sl@0: TThreadFunction aThreadFunction, sl@0: TAny* aOwner) sl@0: { sl@0: __FNLOG("CThreadContext::NewL"); sl@0: CThreadContext* self = new (ELeave) CThreadContext(); sl@0: CleanupStack::PushL(self); sl@0: self->ConstructL(aName, aThreadFunction, aOwner); sl@0: CleanupStack::Pop(); sl@0: return self; sl@0: } sl@0: sl@0: /** sl@0: Construct a CThreadContext object sl@0: sl@0: @param aName The name to be assigned to this thread. sl@0: @param aThreadFunction Function to be called when thread is initially scheduled. sl@0: @param aOwner Pointer to the object owning the thread. Used as the parameter to aThreadFunction. sl@0: */ sl@0: void CThreadContext::ConstructL(const TDesC& aName, sl@0: TThreadFunction aThreadFunction, sl@0: TAny* aOwner) sl@0: { sl@0: __FNLOG("CThreadContext::ConstructL"); sl@0: __PRINT(_L("Creating Critical Section")); sl@0: User::LeaveIfError(iCritSect.CreateLocal()); sl@0: __PRINT(_L("Creating RThread")); sl@0: sl@0: TUint serial(0); // Used to retry creation of a thread in case sl@0: // one with the same name already exists sl@0: sl@0: RBuf threadName; sl@0: threadName.CreateMaxL(aName.Length() + 8); sl@0: CleanupClosePushL(threadName); sl@0: threadName = aName; sl@0: sl@0: TInt err; sl@0: for (;;) sl@0: { sl@0: err = iThread.Create(threadName, aThreadFunction, 0x1000, NULL, aOwner); sl@0: __PRINT2(_L("CThreadContext::ConstructL Created thread %S err=%d"), &threadName, err); sl@0: sl@0: // for a restart wait and retry until old thread is gone sl@0: if (err == KErrAlreadyExists) sl@0: { sl@0: User::After(10 * 1000); // 10 mS sl@0: threadName = aName; sl@0: threadName.AppendNumFixedWidth(serial, EDecimal, 8); sl@0: ++serial; sl@0: } sl@0: else sl@0: { sl@0: break; sl@0: } sl@0: } sl@0: sl@0: User::LeaveIfError(err); sl@0: CleanupStack::Pop(); // threadName sl@0: threadName.Close(); sl@0: sl@0: // set priority sl@0: iThread.SetPriority(EPriorityMore); sl@0: } sl@0: sl@0: sl@0: /** sl@0: Construct a CThreadContext object sl@0: */ sl@0: CThreadContext::CThreadContext() sl@0: : sl@0: iError(KErrNone) sl@0: { sl@0: __FNLOG("CThreadContext::CThreadContext"); sl@0: } sl@0: sl@0: /** sl@0: Destructor sl@0: */ sl@0: CThreadContext::~CThreadContext() sl@0: { sl@0: __FNLOG("CThreadContext::~CThreadContext"); sl@0: __PRINT(_L("Closing Critical Section")); sl@0: iCritSect.Close(); sl@0: __PRINT(_L("Killing ThreadContext")); sl@0: iThread.Kill(0); sl@0: __PRINT(_L("Closing ThreadContext")); sl@0: iThread.Close(); sl@0: } sl@0: sl@0: //----------------------------------------------- sl@0: sl@0: /** sl@0: Construct a CWriteDriveThread object sl@0: */ sl@0: CWriteDriveThread* CWriteDriveThread::NewL() sl@0: { sl@0: __FNLOG("CWriteDriveThread::NewL"); sl@0: CWriteDriveThread* self = new (ELeave) CWriteDriveThread(); sl@0: CleanupStack::PushL(self); sl@0: self->ConstructL(); sl@0: CleanupStack::Pop(); sl@0: return self; sl@0: } sl@0: sl@0: /** sl@0: Construct a CWriteDriveThread object sl@0: */ sl@0: void CWriteDriveThread::ConstructL() sl@0: { sl@0: __FNLOG("CWriteDriveThread::ConstructL"); sl@0: TBuf<16> name = _L("MassStorageWrite"); sl@0: iThreadContext = CThreadContext::NewL(name, ThreadFunction, this); sl@0: // There are two free pointers to start with so initialise the semaphore with 1 sl@0: User::LeaveIfError(iProducerSem.CreateLocal(1)); sl@0: User::LeaveIfError(iConsumerSem.CreateLocal(0)); sl@0: sl@0: iThreadContext->Resume(); sl@0: } sl@0: sl@0: /** sl@0: Construct a CWriteDriveThread object sl@0: */ sl@0: CWriteDriveThread::CWriteDriveThread() sl@0: : iIsCommandWrite10(EFalse) sl@0: { sl@0: __FNLOG("CWriteDriveThread::CWriteDriveThread"); sl@0: } sl@0: sl@0: /** sl@0: Destructor sl@0: */ sl@0: CWriteDriveThread::~CWriteDriveThread() sl@0: { sl@0: __FNLOG("CWriteDriveThread::~CWriteDriveThread"); sl@0: delete iThreadContext; sl@0: } sl@0: sl@0: /** sl@0: This function is called when the thread is initially scheduled. sl@0: sl@0: @param aSelf Pointer to self to facilitate call to member method. sl@0: */ sl@0: TInt CWriteDriveThread::ThreadFunction(TAny* aSelf) sl@0: { sl@0: __FNLOG("CWriteDriveThread::ThreadFunction"); sl@0: CWriteDriveThread* self = static_cast(aSelf); sl@0: return self->WriteToDrive(); sl@0: } sl@0: sl@0: /** sl@0: Writes the data pointed to by iDescWritePtr to the drive. sl@0: */ sl@0: TInt CWriteDriveThread::WriteToDrive() sl@0: { sl@0: __FNLOG("\tCWriteDriveThread::WriteToDrive"); sl@0: sl@0: // One-off convenience variable assignment sl@0: TBlockDesc* &desc = iThreadContext->iBuffer.iDescWritePtr; sl@0: sl@0: for(;;) sl@0: { sl@0: iConsumerSem.Wait(); sl@0: __PRINT(_L("\tWaiting on Write CS...")); sl@0: iThreadContext->iCritSect.Wait(); sl@0: // +++ WRITE CS STARTS HERE +++ sl@0: __PRINT1(_L("\tNow using as write buffer: iBuf%d"), iThreadContext->iBuffer.GetBufferNumber(&desc->iBuf)); sl@0: #ifdef MEASURE_AND_DISPLAY_WRITE_TIME sl@0: RDebug::Print(_L("\tSCSI: writing %d bytes\n"), desc->iBuf.Length()); sl@0: TTime t0, t1; sl@0: t0.HomeTime(); sl@0: #else sl@0: __PRINT1(_L("\tSCSI: writing %d bytes\n"), desc->iBuf.Length()); sl@0: #endif sl@0: // Write buffer to disk sl@0: sl@0: #ifdef INJECT_ERROR sl@0: if (desc->iBuf[0] == '2') sl@0: { sl@0: desc->iBuf[0] = 'x'; sl@0: RDebug::Printf("Injecting error"); sl@0: } sl@0: sl@0: sl@0: RDebug::Printf("%08lx %x [%x] [%x]", desc->iByteOffset, desc->iBuf.Length(), sl@0: desc->iBuf[0], sl@0: desc->iBuf[desc->iBuf.Length()-1]); sl@0: #endif sl@0: sl@0: iThreadContext->iError = iThreadContext->iDrive->Write(desc->iByteOffset, desc->iBuf,iThreadContext->iDrive->IsWholeMediaAccess()); sl@0: #ifdef INJECT_ERROR sl@0: if (desc->iBuf[0] == 'x') sl@0: { sl@0: iThreadContext->iError = KErrUnknown; sl@0: } sl@0: #endif sl@0: sl@0: #ifdef MEASURE_AND_DISPLAY_WRITE_TIME sl@0: t1.HomeTime(); sl@0: const TTimeIntervalMicroSeconds time = t1.MicroSecondsFrom(t0); sl@0: const TUint time_ms = I64LOW(time.Int64() / 1000); sl@0: RDebug::Print(_L("SCSI: write took %d ms\n"), time_ms); sl@0: #endif sl@0: iCallback((TUint8*) (desc->iBuf.Ptr()), iCallbackParameter); sl@0: iWriteCounter--; sl@0: ASSERT(iWriteCounter >= 0); sl@0: sl@0: __PRINT(_L("\tSignalling Write CS")); sl@0: iThreadContext->iCritSect.Signal(); sl@0: // +++ WRITE CS ENDS HERE +++ sl@0: iProducerSem.Signal(); sl@0: } sl@0: } sl@0: sl@0: /** sl@0: Initiates writing data pointed to by iReadBuf to the drive and resumes the thread. Writing sl@0: is completed by the ThreadFunction when the thread is resumed. sl@0: sl@0: @param aDrive Drive to write to. sl@0: @param aOffset Write offset. sl@0: */ sl@0: TInt CWriteDriveThread::WriteDriveData(CMassStorageDrive* aDrive, const TInt64& aOffset, TPtrC8& aDes, ProcessWriteCompleteFunc aFunc, TAny* aPtr) sl@0: { sl@0: // Check error code from previous write sl@0: const TInt r = iThreadContext->iError; sl@0: if (r != KErrNone) sl@0: { sl@0: __PRINT1(_L("Error after previous write = 0x%x \n"), r); sl@0: return KErrAbort; sl@0: } sl@0: sl@0: // Swap the two buffer pointers sl@0: iProducerSem.Wait(); sl@0: __PRINT(_L("Waiting on Write CS...")); sl@0: // +++ WRITE CS STARTS HERE +++ sl@0: iThreadContext->iCritSect.Wait(); sl@0: sl@0: // New DB First read into the iDescReadPtr pointer, sl@0: // then swap,so that write pointer points to correct location, as the ptr pointed to by iDescWritePtr is what is written from in WriteToDrive sl@0: iThreadContext->iBuffer.iDescReadPtr->iBuf.Set((TUint8*)aDes.Ptr(), aDes.Length(), KMaxBufSize ); sl@0: sl@0: iCallback = aFunc; sl@0: iCallbackParameter = aPtr; sl@0: sl@0: iWriteCounter++; sl@0: iThreadContext->iBuffer.SwapDesc(); sl@0: // Prepare variables for next write sl@0: iThreadContext->iDrive = aDrive; sl@0: iThreadContext->iBuffer.iDescWritePtr->iByteOffset = aOffset; sl@0: // +++ WRITE CS ENDS HERE +++ sl@0: __PRINT(_L("Signalling Write CS...")); sl@0: iThreadContext->iCritSect.Signal(); sl@0: sl@0: iConsumerSem.Signal(); sl@0: return KErrNone; sl@0: } sl@0: sl@0: sl@0: void CWriteDriveThread::WaitForWriteEmpty() sl@0: { sl@0: while(iWriteCounter > 0) sl@0: { sl@0: User::After(100); sl@0: } sl@0: } sl@0: sl@0: // Check if the target address range was recently written to, this is to force a sl@0: // cache miss when reading from the same sectors that were just written. sl@0: // Optimisation note: this is only needed if the read precache was started sl@0: // before the write was completed. sl@0: TBool CWriteDriveThread::IsRecentlyWritten(TInt64 aOffset, TInt aLength) sl@0: { sl@0: ASSERT(iWriteCounter == 0); sl@0: if (iIsCommandWrite10) //If the previous command is Write10, then discard pre-read as the same buffers are used and will be over written by Write10 sl@0: return ETrue; sl@0: if(aOffset <= iThreadContext->iBuffer.iDescReadPtr->iByteOffset && sl@0: aOffset + aLength >= iThreadContext->iBuffer.iDescReadPtr->iByteOffset) sl@0: return ETrue; sl@0: if(aOffset >= iThreadContext->iBuffer.iDescReadPtr->iByteOffset && sl@0: aOffset <= iThreadContext->iBuffer.iDescReadPtr->iByteOffset + iThreadContext->iBuffer.iDescReadPtr->iLength) sl@0: return ETrue; sl@0: if(aOffset <= iThreadContext->iBuffer.iDescWritePtr->iByteOffset && sl@0: aOffset + aLength >= iThreadContext->iBuffer.iDescReadPtr->iByteOffset) sl@0: return ETrue; sl@0: if(aOffset >= iThreadContext->iBuffer.iDescWritePtr->iByteOffset && sl@0: aOffset <= iThreadContext->iBuffer.iDescReadPtr->iByteOffset + iThreadContext->iBuffer.iDescReadPtr->iLength) sl@0: return ETrue; sl@0: return EFalse; sl@0: } sl@0: sl@0: //----------------------------------------------- sl@0: sl@0: /** sl@0: Construct a CReadDriveThread object sl@0: */ sl@0: CReadDriveThread* CReadDriveThread::NewL() sl@0: { sl@0: __FNLOG("CReadDriveThread::NewL"); sl@0: CReadDriveThread* self = new (ELeave) CReadDriveThread(); sl@0: CleanupStack::PushL(self); sl@0: self->ConstructL(); sl@0: CleanupStack::Pop(); sl@0: return self; sl@0: } sl@0: sl@0: /** sl@0: Construct a CReadDriveThread object sl@0: sl@0: @param aName The name to be assigned to this thread. sl@0: @pram aThreadFunction Function to be called when thread is initially scheduled. sl@0: */ sl@0: void CReadDriveThread::ConstructL() sl@0: { sl@0: __FNLOG("CReadDriveThread::ConstructL"); sl@0: TBuf<15> name = _L("MassStorageRead"); sl@0: iThreadContext = CThreadContext::NewL(name, ThreadFunction, this); sl@0: } sl@0: sl@0: /** sl@0: Construct a CReadDriveThread object sl@0: */ sl@0: CReadDriveThread::CReadDriveThread() sl@0: : sl@0: iThreadRunning(EFalse) sl@0: { sl@0: __FNLOG("CReadDriveThread::CReadDriveThread"); sl@0: } sl@0: sl@0: /** sl@0: Destructor sl@0: */ sl@0: CReadDriveThread::~CReadDriveThread() sl@0: { sl@0: __FNLOG("CReadDriveThread::~CReadDriveThread"); sl@0: delete iThreadContext; sl@0: } sl@0: sl@0: /** sl@0: This function is called when the thread is initially scheduled. sl@0: sl@0: @param aSelf Pointer to self to facilitate call to member method. sl@0: */ sl@0: TInt CReadDriveThread::ThreadFunction(TAny* aSelf) sl@0: { sl@0: __FNLOG("CReadDriveThread::ThreadFunction"); sl@0: CReadDriveThread* self = static_cast(aSelf); sl@0: return self->ReadFromDrive(); sl@0: } sl@0: sl@0: /** sl@0: Reads data from the drive with iOffset and iReadLength into memory pointer iReadBuffer sl@0: and suspends the thread. sl@0: */ sl@0: TInt CReadDriveThread::ReadFromDrive() sl@0: { sl@0: __FNLOG("\tCReadDriveThread::ReadFromDrive"); sl@0: sl@0: // One-off convenience variable assignment sl@0: TBlockDesc* &desc = iThreadContext->iBuffer.iDescWritePtr; sl@0: sl@0: for (;;) sl@0: { sl@0: __PRINT(_L("\tWaiting on Read CS...")); sl@0: iThreadContext->iCritSect.Wait(); sl@0: // +++ READ CS STARTS HERE +++ sl@0: iThreadRunning = ETrue; sl@0: iCompleted = EFalse; sl@0: sl@0: __PRINT1(_L("\tNow using as read buffer: iBuf%d"), iThreadContext->iBuffer.GetBufferNumber(&desc->iBuf)); sl@0: sl@0: #ifdef MEASURE_AND_DISPLAY_READ_TIME sl@0: RDebug::Print(_L("\tSCSI: reading %d bytes\n"), desc->iBuf.Length()); sl@0: TTime t0, t1; sl@0: t0.HomeTime(); sl@0: #else sl@0: __PRINT1(_L("\tSCSI: reading %d bytes\n"), desc->iBuf.Length()); sl@0: #endif sl@0: // Fill read buffer from disk sl@0: iThreadContext->iError = iThreadContext->iDrive->Read(desc->iByteOffset, sl@0: desc->iLength, sl@0: desc->iBuf, sl@0: iThreadContext->iDrive->IsWholeMediaAccess()); sl@0: sl@0: #ifdef MEASURE_AND_DISPLAY_READ_TIME sl@0: t1.HomeTime(); sl@0: const TTimeIntervalMicroSeconds time = t1.MicroSecondsFrom(t0); sl@0: const TUint time_ms = I64LOW(time.Int64() / 1000); sl@0: RDebug::Print(_L("SCSI: read took %d ms\n"), time_ms); sl@0: #endif sl@0: sl@0: iCompleted = ETrue; sl@0: iThreadRunning = EFalse; sl@0: __PRINT(_L("\tSignalling Read CS")); sl@0: // +++ READ CS ENDS HERE +++ sl@0: iThreadContext->iCritSect.Signal(); sl@0: // Suspend self sl@0: __PRINT(_L("\tSuspending Read Thread")); sl@0: RThread().Suspend(); sl@0: } sl@0: } sl@0: sl@0: /** sl@0: Client read request of a data block from the specified drive. sl@0: If there is no pre-read data that matches the requested Offset and Length then the drive sl@0: is read and the next pre-read is setup. If there is matching pre-read data available then sl@0: the next pre-read is setup. Finishes by resuming the thread and the ThreadFunciton runs. sl@0: sl@0: @param aDrive Drive to read from. sl@0: @param aOffset Read offset sl@0: @param aLength Length sl@0: */ sl@0: TBool CReadDriveThread::ReadDriveData(CMassStorageDrive* aDrive, sl@0: const TInt64& aOffset, sl@0: TUint32 aLength, sl@0: TBool aIgnoreCache) sl@0: { sl@0: __MT_READ_PRINT2(_L("\nRead10: offs %ld len %d"), aOffset, aLength); sl@0: sl@0: __PRINT(_L("Waiting on Read CS...")); sl@0: iThreadContext->iCritSect.Wait(); sl@0: // +++ READ CS STARTS HERE +++ sl@0: __ASSERT_DEBUG(!iThreadRunning, User::Panic(_L("MSDC-THREAD"), 666)); sl@0: sl@0: TBlockDesc* &desc = iThreadContext->iBuffer.iDescReadPtr; sl@0: TBlockDesc* &bgDesc = iThreadContext->iBuffer.iDescWritePtr; sl@0: sl@0: if ((!aIgnoreCache) && sl@0: (iCompleted) && sl@0: (iThreadContext->iError == KErrNone) && sl@0: (iThreadContext->iDrive == aDrive) && sl@0: (bgDesc->iByteOffset == aOffset) && sl@0: (bgDesc->iLength == aLength)) sl@0: { sl@0: // Good: We pre-read the correct data :-) sl@0: __MT_READ_PRINT(_L("Match: Using pre-read data :-) :-) :-) :-)")); sl@0: } sl@0: else sl@0: { sl@0: __MT_READ_PRINT(_L("Not using pre-read data")); sl@0: if (iThreadContext->iError != KErrNone) sl@0: { sl@0: __MT_READ_PRINT1(_L("Pre-read failed: %d"), iThreadContext->iError); sl@0: } sl@0: if (iThreadContext->iDrive != aDrive) sl@0: { sl@0: __MT_READ_PRINT2(_L("Pre-read drive mismatch: pre 0x%08x / act 0x%08x"), sl@0: iThreadContext->iDrive, aDrive); sl@0: } sl@0: if (desc->iByteOffset != aOffset) sl@0: { sl@0: __MT_READ_PRINT2(_L("Pre-read offset mismatch: pre %ld / act %ld"), sl@0: desc->iByteOffset, aOffset); sl@0: } sl@0: if (desc->iLength != aLength) sl@0: { sl@0: __MT_READ_PRINT2(_L("Pre-read length mismatch: pre %d / act %d"), sl@0: desc->iLength, aLength); sl@0: // Potential optimization: If the pre-read was OK but for more data sl@0: // than the host is now asking for, we could still satisfy that sl@0: // request from the pre-read data by shortening the buffer. sl@0: } sl@0: // No valid pre-read data was available - so we have to read it now sl@0: bgDesc->iByteOffset = aOffset; sl@0: bgDesc->iLength = aLength; sl@0: TInt err = aDrive->Read(aOffset, sl@0: aLength, sl@0: bgDesc->iBuf, sl@0: aDrive->IsWholeMediaAccess()); sl@0: if (err != KErrNone) sl@0: { sl@0: __PRINT1(_L("Read failed, err=%d\n"), err); sl@0: // +++ READ CS ENDS HERE +++ sl@0: __PRINT(_L("Signalling Read CS...")); sl@0: iThreadContext->iCritSect.Signal(); sl@0: return EFalse; sl@0: } sl@0: } sl@0: sl@0: // Prepare thread variables for next pre-read attempt by the ReadThread sl@0: const TInt64 offs_new = aOffset + aLength; sl@0: iThreadContext->iDrive = aDrive; // same drive sl@0: desc->iByteOffset = offs_new; // next block sl@0: desc->iLength = aLength; // same length sl@0: iCompleted = EFalse; sl@0: iThreadContext->iBuffer.SwapDesc(); sl@0: sl@0: // +++ READ CS ENDS HERE +++ sl@0: __PRINT(_L("Signalling Read CS...")); sl@0: iThreadContext->iCritSect.Signal(); sl@0: // Start background read sl@0: __PRINT(_L("Resuming Read Thread")); sl@0: iThreadContext->Resume(); sl@0: return ETrue; sl@0: } sl@0: sl@0: /** sl@0: Discard the read buffer. This is used to force a cache miss when reading from sl@0: the same sectors that were just written. sl@0: */ sl@0: void CReadDriveThread::DiscardRead() sl@0: { sl@0: __PRINT(_L("Waiting on Read CS in DiscardRead...")); sl@0: iThreadContext->iCritSect.Wait(); sl@0: // +++ READ CS STARTS HERE +++ sl@0: __PRINT(_L("Discarding pre-read buffer")); sl@0: iCompleted = EFalse; sl@0: iThreadContext->iBuffer.iDescReadPtr->iLength = 0; sl@0: sl@0: // +++ READ CS ENDS HERE +++ sl@0: __PRINT(_L("Signalling Read CS in DiscardRead...")); sl@0: iThreadContext->iCritSect.Signal(); sl@0: } sl@0: #endif // MSDC_MULTITHREADED sl@0: