// A realtime audio glitch detector. // This will be used to make measurements of audio glitches. // Author: Joseph Ballantyne // Date: 11/17/99 // This will first work with DMA based devices. // I will add PCI support as well once the DMA glitch detection is working // properly. #ifdef UNDER_NT #include #include #define IO_NO_INCREMENT 0 HANDLE IoGetCurrentProcess(VOID); #else #include #include #include #endif #include "common.h" #include #include #include "glitch.h" #ifdef UNDER_NT #include "mapview.h" #else #include #include #endif #pragma intrinsic ( strcpy ) // Everything we touch HAS to be locked down. #pragma LOCKED_CODE #pragma LOCKED_DATA // This header file has CODE and DATA in it. It MUST be included after above pragmas. // The code and data in the following header MUST be LOCKED. #include "dma.h" PGLITCHDATA GlitchInfo; #pragma warning ( disable : 4035 ) #define rdtsc __asm _emit 0x0f __asm _emit 0x31 #define rdprf __asm _emit 0x0f __asm _emit 0x33 LONGLONG __inline ReadCycleCounter ( VOID ) { __asm { rdtsc } } ULONGLONG __inline ReadPerformanceCounter ( ULONG index ) { __asm { mov ecx,index rdprf } } ULONG GetCR3 ( VOID ) { __asm mov eax, cr3; } ULONG InB ( ULONG address ) { __asm { mov edx,address xor eax,eax in al,dx } } #pragma PAGEABLE_CODE PVOID MapPhysicalToLinear ( VOID *physicaladdress, ULONG numbytes, ULONG flags ) { #ifndef UNDER_NT __asm push flags __asm push numbytes __asm push physicaladdress VMMCall( _MapPhysToLinear ); __asm add esp,12 #else PHYSICAL_ADDRESS Address; Address.QuadPart=(ULONG_PTR)physicaladdress; return (PVOID)MmMapIoSpace(Address, numbytes, FALSE); #endif } PVOID __cdecl ReservePages ( ULONG page, ULONG npages, ULONG flags ) /*++ Routine Description: Arguments: Return Value: --*/ { #ifndef UNDER_NT __asm { push flags push npages push page VMMCall( _PageReserve ) __asm add esp, 12 } #else return ExAllocatePool( NonPagedPool, npages*PROCPAGESIZE ); #endif } ULONG __cdecl FreePages ( PVOID hmem, ULONG flags ) /*++ Routine Description: Arguments: Return Value: --*/ { #ifndef UNDER_NT __asm { push flags push hmem VMMCall( _PageFree ) __asm add esp, 8 } #else ExFreePool( hmem ); return TRUE; #endif } #ifndef UNDER_NT PVOID __cdecl LockPages ( ULONG page, ULONG npages, ULONG pageoffset, ULONG flags ) { __asm { push flags push pageoffset push npages push page VMMCall( _PageLock ) __asm add esp, 16 } } #endif #pragma warning( default : 4035 ) enum ControlCodes { CHECKLOADED, GETVERSION, GETBASEADDRESS }; #ifndef UNDER_NT DWORD __stdcall GlitchWin32API(PDIOCPARAMETERS p) { switch (p->dwIoControlCode) { case CHECKLOADED: break; case GETVERSION: // Get version. if (!p->lpvOutBuffer || p->cbOutBuffer<4) return ERROR_INVALID_PARAMETER; *(PDWORD)p->lpvOutBuffer=0x0100; if (p->lpcbBytesReturned) *(PDWORD)p->lpcbBytesReturned=4; break; case GETBASEADDRESS: // Get base address. if (!p->lpvOutBuffer || p->cbOutBuffer<4) return ERROR_INVALID_PARAMETER; *(PDWORD)p->lpvOutBuffer=(DWORD)GlitchInfo; if (p->lpcbBytesReturned) *(PDWORD)p->lpcbBytesReturned=4; break; default: return ERROR_INVALID_PARAMETER; } return 0; } #else PVOID MappedBuffer=NULL; NTSTATUS DeviceIoCreate( IN PDEVICE_OBJECT DeviceObject, IN PIRP pIrp ) { NTSTATUS Status=STATUS_SUCCESS; pIrp->IoStatus.Status = Status; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return Status; } NTSTATUS DeviceIoClose( IN PDEVICE_OBJECT DeviceObject, IN PIRP pIrp ) { NTSTATUS Status=STATUS_SUCCESS; PVOID Pointer; // Make sure that we release our mapped buffer view when the user mode // app that opened our section handle closes its handle or goes away. // We screen these calls based on the handle we get to the process that // sucessfully opened a section handle. We only make the unmap call // if we are being called by the same process that made the map call. if (IoGetCurrentProcess()==Process) { if ((Pointer=InterlockedExchangePointer(&MappedBuffer, NULL))!=NULL && UnMapContiguousBufferFromUserModeProcess(Pointer)!=STATUS_SUCCESS) { Trap(); } } pIrp->IoStatus.Status = Status; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return Status; } NTSTATUS DeviceIoControl( IN PDEVICE_OBJECT DeviceObject, IN PIRP pIrp ) { PVOID BaseAddress=NULL; PIO_STACK_LOCATION pIrpStack; NTSTATUS Status=STATUS_SUCCESS; pIrpStack = IoGetCurrentIrpStackLocation(pIrp); switch (pIrpStack->Parameters.DeviceIoControl.IoControlCode) { case 2: if (!pIrp->MdlAddress || pIrp->MdlAddress->ByteCount!=4) { Status=STATUS_INVALID_PARAMETER; break; } Status=MapContiguousBufferToUserModeProcess(GlitchInfo, &BaseAddress); // Remember the address of the mapped buffer. // We depend on the fact that BaseAddress will be NULL for requests // to map the buffer when it is already mapped. if (BaseAddress) { MappedBuffer=BaseAddress; } *(PDWORD_PTR)(MmGetSystemAddressForMdl(pIrp->MdlAddress))=(DWORD_PTR)BaseAddress; break; default: Status=STATUS_INVALID_PARAMETER; break; } pIrp->IoStatus.Status = Status; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return Status; } #endif // All the rest of the code in this file MUST be locked as it is all called from within // the realtime thread. #pragma LOCKED_CODE VOID OutB ( ULONG address, ULONG data ) { __asm { mov edx,address mov eax,data out dx,al } } // This function loads the DMA buffer just played with our starvation fill pattern. // Which is 0xffffffff. This is a nice pattern because for signed 16 bit data it // is a DC value close to zero. Moreover, it is DC for both stereo 16 bit, mono 16 // bit, stereo 8 bit and mono 8 bit. For 8 bit data it is at the max of the data // range - so is pegged to max volume - for most cards. Since most cards do unsigned // 8 bit samples. This value MUST be different from the KMIXER starvation pattern // which is zero. // Note that this routine assumes that CurrentDmaPosition and LastDmaPosition are // multiples of 4 on entry. It also assumes that the DmaBufferSize is a // power of 2 and that DmaBufferSize is greater than 4 bytes. VOID FillDmaBuffer ( ULONG CurrentDmaPosition, ULONG LastDmaPosition, PDMAINFO Context ) { // Make positions relative to start of dma buffer. CurrentDmaPosition-=Context->PhysicalDmaBufferStart; LastDmaPosition-=Context->PhysicalDmaBufferStart; while (LastDmaPosition!=CurrentDmaPosition) { Context->pDmaBuffer[LastDmaPosition/4]=0xffffffff; LastDmaPosition+=4; LastDmaPosition&=Context->DmaBufferSize-1; } } // Glitches start whenever all of the samples in the buffer since we last checked // match our starvation fill pattern. This will happen whenever there is at least // 1 ms of starvation assuming that we run our starvation detection with a 1ms period. // This means that we WILL miss glitches that are for less than 1ms, but it also // means that we won't be prone to false positives. // Note that this routine assumes that CurrentDmaPosition and LastDmaPosition are // multiples of 4 on entry. It also assumes that the DmaBufferSize is a // power of 2 and that DmaBufferSize is greater than 4 bytes. BOOLEAN GlitchStarted ( ULONG CurrentDmaPosition, ULONG LastDmaPosition, PDMAINFO Context ) { if (CurrentDmaPosition==LastDmaPosition) { return FALSE; } // Make positions relative to start of dma buffer. CurrentDmaPosition-=Context->PhysicalDmaBufferStart; LastDmaPosition-=Context->PhysicalDmaBufferStart; while (LastDmaPosition!=CurrentDmaPosition) { if (Context->pDmaBuffer[LastDmaPosition/4]!=0xffffffff) { return FALSE; } LastDmaPosition+=4; LastDmaPosition&=Context->DmaBufferSize-1; } return TRUE; } // Glitches end as soon as any value in the buffer does not match // our starvation fill pattern. // Note that this routine assumes that CurrentDmaPosition and LastDmaPosition are // multiples of 4 on entry. It also assumes that the DmaBufferSize is a // power of 2 and that DmaBufferSize is greater than 4 bytes. BOOLEAN GlitchEnded ( ULONG CurrentDmaPosition, ULONG LastDmaPosition, PDMAINFO Context ) { // Make positions relative to start of dma buffer. CurrentDmaPosition-=Context->PhysicalDmaBufferStart; LastDmaPosition-=Context->PhysicalDmaBufferStart; while (LastDmaPosition!=CurrentDmaPosition) { if (Context->pDmaBuffer[LastDmaPosition/4]!=0xffffffff) { return TRUE; } LastDmaPosition+=4; LastDmaPosition&=Context->DmaBufferSize-1; } return FALSE; } ULONG UnmaskedChannels=0; ULONG DmaBufferRemapCount=0; ULONG gCurrentDmaPosition=0; ULONG gCurrentDmaCount=0; NTSTATUS MapDmaBuffer(PDMAINFO Context, ULONG CurrentDmaPosition, ULONG CurrentDmaCount) { ULONG i; // Whenever I get a non zero DMA position AND a non zero count, I have to // make sure that I have that location properly mapped. if (CurrentDmaPosition!=0 && CurrentDmaCount!=0) { // Check if this position and count are properly mapped. // If not, then we must remap the buffer. if (CurrentDmaPositionPhysicalDmaBufferStart || CurrentDmaPosition>=Context->PhysicalDmaBufferStart+Context->DmaBufferSize || CurrentDmaPosition+CurrentDmaCountPhysicalDmaBufferStart || CurrentDmaPosition+CurrentDmaCount>Context->PhysicalDmaBufferStart+Context->DmaBufferSize ) { // Position or size is outside current mapping. Remap the buffer. DmaBufferRemapCount++; // Recalculate start and size of where the buffer should be. // WARNING: We ASSUME all buffers we map are multiples of 4k bytes in size. // For WDM audio drivers this assumption is currently valid. Context->PhysicalDmaBufferStart=CurrentDmaPosition&(~(PROCPAGESIZE-1)); Context->DmaBufferSize=(CurrentDmaCount+PROCPAGESIZE-1)&(~(PROCPAGESIZE-1)); if (Context->DmaBufferSize>MAXDMABUFFERSIZE) { // Clear start and size since we cannot map them anyway. // Even more important, we must clear them so we will try to remap // again the next time this routine is called. Context->PhysicalDmaBufferStart=0; Context->DmaBufferSize=0; return STATUS_INSUFFICIENT_RESOURCES; } // Walk the pages we have for mapping the DMA buffer and remap them // to our DMA channel buffer. for (i=0; i<(Context->DmaBufferSize/PROCPAGESIZE); i++) { Context->PageTable[i+(((ULONG)(Context->pDmaBuffer)>>12)&1023)]= (Context->PhysicalDmaBufferStart+(i*PROCPAGESIZE))|(PRESENT|WRITABLE|USER|CACHEDISABLED); } // Now flush the TLBs. // If cr3 changes - which it DOES on NT, then I MUST make // sure that we don't get thread switched between the 2 // assembly instructions - otherwise we will corrupt cr3. // Not a good thing. while (((LONG)ReadPerformanceCounter(0)+50)>0) { Trap(); RtYield(0, 0); } __asm mov eax, cr3; __asm mov cr3, eax; } } return STATUS_SUCCESS; } ULONG GetNextPrintPosition ( PDMAINFO Context ) { ULONG PrintLocation, NextLocation; NextLocation=*Context->pPrintLoad; do { PrintLocation=NextLocation; NextLocation=InterlockedCompareExchange((PULONG)Context->pPrintLoad, PrintLocation+PACKETSIZE, PrintLocation); } while (PrintLocation!=NextLocation); // Now we clear out the opposite half of the print buffer. We do this all in kernel mode. // This means that we have data only in 1/2 of the buffer. As we add new data, we // delete the old data. We do the deletion of data in kernel mode so that we only // need to read data from user mode. I do NOT want user mode code to be writing to // this buffer. User mode code can read out of the output buffer, but NOT write into // it. This means we MUST both fill and clear this buffer ourselves. Since user // mode code is dependent on the fact that all slots will be marked as having // NODATA in them until they have been completely loaded with data, at which point // they will be marked with something other than NODATA. We guarantee that // every slot we are loading starts as NODATA by simply clearing the print slots // in kernel mode before we fill them. The easiest way to do this is to start // by marking all entries in the buffer as NODATA, and then by continuing to make // sure that for every print slot we are going to fill with data, we clear the corresponding // print slot halfway around the buffer. // That simple algorithm guarantees that every slot starts out marked as NODATA and // then transitions to some other state after it is filled. ((ULONG *)Context->pPrintBuffer)[((PrintLocation+Context->PrintBufferSize/2)%Context->PrintBufferSize)/sizeof(ULONG)]=NODATA; PrintLocation%=Context->PrintBufferSize; return PrintLocation; } VOID GlitchDetect ( PDMAINFO Context, ThreadStats *Statistics ) { ULONG DmaMask, LastDmaMask; KIRQL OldIrql; ULONG LastDmaPosition; ULONG CurrentDmaPosition; ULONG CurrentDmaCount; ULONGLONG GlitchStart; ULONGLONG LastGlitchStart; ULONG GlitchLength; ULONG PrintLocation; ULONGLONG LastTimesliceStartTime; ULONG Owner; GlitchStart=0; GlitchLength=0; LastDmaMask=0; LastDmaPosition=0; LastGlitchStart=0; LastTimesliceStartTime=0; while (TRUE) { // If any other channels are unmasked. Punt. // Until I figure out what is broken, we only support tracking 1 channel // at a time. if (UnmaskedChannels&(~(1<Channel))) { goto ReleaseCurrentTimeslice; } if (LastTimesliceStartTime) { LastTimesliceStartTime=Statistics->ThisTimesliceStartTime-LastTimesliceStartTime; // At this point LastTimesliceStartTime is really the time between the // last timeslice start time, and the current timeslice start time. } // CR3 DOES change under NT! However, although there are multiple // page directories, they alias the same page tables - at least the // system non paged page tables. (They have to since otherwise the // kernel mode locked code would not work.) #ifndef UNDER_NT if (GetCR3()!=Context->CR3) { Trap(); break; } #endif // Grab the DMA master adapter spinlock. KeAcquireSpinLock(Context->pMasterAdapterSpinLock, &OldIrql); // Check mask state of our channel. DmaMask=ReadDMAMask(); // Unmasked. if ((~DmaMask)&(1<Channel)) { // Lock out glitch tracking on other channels. If this fails, then // someone else is already doing glitch tracking, so release the // spinlock and release the current timeslice. Owner=InterlockedCompareExchange(&UnmaskedChannels, 1<Channel, 0); if (!(Owner==(1<Channel) || Owner==0)) { // Release the DMA master adapter spinlock. KeReleaseSpinLock(Context->pMasterAdapterSpinLock, OldIrql); goto ReleaseCurrentTimeslice; } // Mask the DMA channel. MaskDmaChannel(Context->Channel); // Read the position. ReadDmaPosition(Context->Channel, &CurrentDmaPosition); // Align it. CurrentDmaPosition&=~3; // Read the count. ReadDmaCount(Context->Channel, &CurrentDmaCount); // Align it. CurrentDmaCount+=3; CurrentDmaCount&=~3; // Unmask the DMA channel. UnmaskDmaChannel(Context->Channel); } // Masked. else { #if 0 // Read the position. ReadDmaPosition(Context->Channel, &CurrentDmaPosition); // Align it. CurrentDmaPosition&=~3; // Read the count. ReadDmaCount(Context->Channel, &CurrentDmaCount); // Align it. CurrentDmaCount+=3; CurrentDmaCount&=~3; #else CurrentDmaPosition=0; CurrentDmaCount=0; LastDmaPosition=0; // Release our lockout of glitch tracking on other channels. InterlockedCompareExchange(&UnmaskedChannels, 0, 1<Channel); #endif } // Release the DMA master adapter spinlock. KeReleaseSpinLock(Context->pMasterAdapterSpinLock, OldIrql); // Load globals - so I can see what they are. gCurrentDmaPosition=CurrentDmaPosition; gCurrentDmaCount=CurrentDmaCount; // Now find and map the physical DMA buffer. // Punt and exit thread if we cannot map the buffer. if (MapDmaBuffer(Context, CurrentDmaPosition, CurrentDmaCount)!=STATUS_SUCCESS) { Trap(); break; } // If LastDmaPosition and CurrentDmaPosition are valid and different, // then look for glitches. if (CurrentDmaPosition!=0 && LastDmaPosition!=0 && CurrentDmaPosition!=LastDmaPosition) { // Make sure our position is within our mapped buffer. If not, log that // info and exit. That will kill this thread. if (CurrentDmaPositionPhysicalDmaBufferStart || CurrentDmaPosition>=(Context->PhysicalDmaBufferStart+Context->DmaBufferSize)) { Trap(); break; } // Make sure both current and last dma positions are DWORD aligned. Punt if not. if ((CurrentDmaPosition|LastDmaPosition)&3) { Trap(); break; } // Make sure the dma buffer size is a power of 2. Punt if not. if (Context->DmaBufferSize&(Context->DmaBufferSize-1)) { Trap(); break; } // Check if we see our FLAG value in the DMA buffer. Log glitch start time if so. if (!GlitchStart) { if (GlitchStarted(CurrentDmaPosition, LastDmaPosition, Context)) { GlitchStart=Statistics->PeriodIndex; } } // If we are tracking a glitch, then see if there is valid data now. Log glitch // stop time if so. else { if (GlitchEnded(CurrentDmaPosition, LastDmaPosition, Context)) { GlitchLength=(ULONG)(Statistics->PeriodIndex-GlitchStart); } } // Fill in with our flag value behind the DMA pointer back to previous DMA pointer. FillDmaBuffer(CurrentDmaPosition, LastDmaPosition, Context); } // Print interrupt holdoff time if we have been held off. // We do this only for channels that are unmasked. if ((~DmaMask)&(1<Channel) && LastTimesliceStartTime>=2000*USEC) { PrintLocation=GetNextPrintPosition(Context); // Load the packet type last. When the ring 3 code see's a packet // type that is not NODATA it assumes the rest of the packet has already // been written. ((ULONGLONG *)Context->pPrintBuffer)[1+PrintLocation/sizeof(ULONGLONG)]=LastTimesliceStartTime-MSEC; ((ULONG *)Context->pPrintBuffer)[PrintLocation/sizeof(ULONG)]=HELDOFF|(Context->Channel<<8); } // Print glitch information if any. if (GlitchLength) { PrintLocation=GetNextPrintPosition(Context); // Load the packet type last. When the ring 3 code see's a packet // type that is not NODATA it assumes the rest of the packet has already // been written. // We put the DMA channel in byte 1 of the packet type. ((ULONGLONG *)Context->pPrintBuffer)[1+PrintLocation/sizeof(ULONGLONG)]=GlitchStart-LastGlitchStart; ((ULONG *)Context->pPrintBuffer)[1+PrintLocation/sizeof(ULONG)]=GlitchLength; ((ULONG *)Context->pPrintBuffer)[PrintLocation/sizeof(ULONG)]=GLITCHED|(Context->Channel<<8); LastGlitchStart=GlitchStart; GlitchStart=0; GlitchLength=0; } // Print pause/running state changes. if ((LastDmaMask^DmaMask)&(1<Channel)) { if (DmaMask&(1<Channel)) { PrintLocation=GetNextPrintPosition(Context); // Load the packet type last. When the ring 3 code see's a packet // type that is not NODATA it assumes the rest of the packet has already // been written. // We put the DMA channel in byte 1 of the packet type. ((ULONG *)Context->pPrintBuffer)[PrintLocation/sizeof(ULONG)]=MASKED|(Context->Channel<<8); } else { PrintLocation=GetNextPrintPosition(Context); // Load the packet type last. When the ring 3 code see's a packet // type that is not NODATA it assumes the rest of the packet has already // been written. // We put the DMA channel in byte 1 of the packet type. ((ULONG *)Context->pPrintBuffer)[PrintLocation/sizeof(ULONG)]=UNMASKED|(Context->Channel<<8); } } // Update LastDmaPosition; LastDmaPosition=CurrentDmaPosition; LastDmaMask=DmaMask; ReleaseCurrentTimeslice: LastTimesliceStartTime=Statistics->ThisTimesliceStartTime; // Yield till next ms. RtYield(0, 0); } }