/* * vcworker.c * * 32-bit Video Capture driver * User-mode support library - worker thread functions * * * Geraint Davies, Feb 93 */ #include #include #include #include #include #include #include #include #include "vcupriv.h" /* * see vcuser.c for overview comment. * * We have a worker thread created when streaming starts (during processing * of the StreamInit request. * * We need to track overrun errors (frame skips - ie when the kernel driver * is ready to capture a frame but there is no buffer ready). We issue * a wait-error ioctl asynchronously - it will complete when an overrun * occurs. * * We also issue ioctls for add-buffer requests asynchronously - these * complete when the buffer added has been filled. So that we don't take * up too much page-locked memory, we only have 2 of these outstanding with * the kernel driver at once. The remaining add-buffer requests we queue * ourselves. * * The thread loops waiting for one of the following events: * a request from the calling thread which can be: * - get error (just return the skip count) * - add buffer (queue the buffer - send to kernel if <2 sent) * - reset (free all queued buffers here and in kernel) * - fini (clean up and terminate thread) * * completion of an add-buffer (issue callback and send another buffer) * * completion of a wait-error (callback, incr count, and send another) * * We only queue add-buffer requests to the kernel between stream-start and * stream-stop. This is so that vcuser.c\VC_Frame() can request individual * frames (to show the user where we are before he hits 'start'. The bReset * flag is used to track whether we should be queueing buffers to the kernel. * * * If the add-buffer fails (because the buffer is too large to be pagelocked) * we will set bPartials to indicate that we should use CAP_TO_SYSBUF and * PARTIAL_CAPTURE ioctls instead of ADD_BUFFER. In this case, we replace the * buffer on the queue so that all buffers are now queued here in user mode, and * we post a CAP_TO_SYSBUF. This will complete when a frame has been captured. * During processing of this, we issue as many PARTIAL_CAPTURE requests * as it takes to get the data into the buffer - these are issued synchronously, * since they require just a copy from the kernel-mode buffer. Then the * system buffer is release, and a new CAP_TO_SYSBUF queued for the next frame. * * Once we have set the bPartials flag, we will not unset it until the next * stream init. * */ VOID VC_Callback(PVCCALLBACK pCallback, DWORD msg, DWORD data); VOID VC_ProcessBuffer(VCUSER_HANDLE vh, DWORD Buffer); VOID VC_SendWaitError(VCUSER_HANDLE vh); VOID VC_CompleteWaitError(VCUSER_HANDLE vh); VOID VC_ProcessPartials(VCUSER_HANDLE vh); VOID VC_InitCapToSysBuf(VCUSER_HANDLE vh); VOID VC_ReplaceHead(VCUSER_HANDLE vh, LPVIDEOHDR lpvh); VOID VC_AddTail(VCUSER_HANDLE vh, LPVIDEOHDR lpvh); LPVIDEOHDR VC_RemoveHead(VCUSER_HANDLE vh); /* * initialise the worker thread. Signal the hCompletion event when * all done and ready to receive the first request. * * as a thread start routine it has a 32-bit argument (the VCUSER_HANDLE) * and a 32-bit return value (unused). */ DWORD VC_ThreadInit(DWORD arg) { VCUSER_HANDLE vh = (VCUSER_HANDLE) arg; int i; DWORD dwCount, dwEvent; BOOL bTerminate = FALSE; dprintf2(("worker %d starting", GetCurrentThreadId())); /* initialise event array */ for (i = 0; i < NR_SENT_BUFFERS; i++) { vh->hEvents[i] = CreateEvent(NULL, TRUE, FALSE, NULL); vh->lpBuffers[i] = NULL; } vh->iSentCount = 0; /* create events for wait-error and the sent buffers */ vh->hEvents[NR_SENT_BUFFERS] = CreateEvent(NULL, TRUE, FALSE, NULL); /* we also need this work event to be part of the array so we can * wait for any of them */ vh->hEvents[NR_SENT_BUFFERS+1] = vh->hWorkEvent; /* reset frame-skip count for this session */ vh->SkipCount = 0; /* reset the partial flag */ vh->bPartials = FALSE; /* send the stream-init ioctl */ if (!DeviceIoControl(vh->hDriver, IOCTL_VIDC_STREAM_INIT, (PULONG) &vh->FunctionArg, sizeof(ULONG), NULL, 0, &dwCount, NULL) ) { bTerminate = TRUE; vh->FunctionResult = FALSE; } else { vh->FunctionResult = TRUE; } dprintf2(("init %s", vh->FunctionResult?"ok":"not ok")); vh->bWaitOutstanding = FALSE; /* no buffers sent to kernel until start happens */ vh->bReset = TRUE; VC_Callback(vh->pCallback, MM_DRVM_OPEN, 0); /* signal that init is done */ SetEvent(vh->hCompleteEvent); while (!bTerminate) { if (!vh->bWaitOutstanding) { VC_SendWaitError(vh); } dwEvent = WaitForMultipleObjects( 2 + NR_SENT_BUFFERS, vh->hEvents, FALSE, // wait-any, not all INFINITE); dwEvent -= WAIT_OBJECT_0; dprintf4(("<%d>", dwEvent)); switch (dwEvent) { case NR_SENT_BUFFERS + 1: bTerminate = VC_ProcessFunction(vh, vh->FunctionCode, vh->FunctionArg, &vh->FunctionResult); vh->FunctionCode = InvalidFunction; /* don't signal the event on termination -it will * screw us for the next start-up */ if (!bTerminate) { SetEvent(vh->hCompleteEvent); } break; case NR_SENT_BUFFERS: VC_CompleteWaitError(vh); break; default: /* if we are running in partials mode, this will * be a cap-to-sysbuf completion * * Note that there is a gap after switching to partials mode * before queueing any requests, during which there may be one or * more add-buffers still queued. We wait until these have * completed before queuing the first cap-to-sysbuf request. */ if ((vh->bPartials) && (vh->bCapOutstanding)) { ASSERT(dwEvent == 0); VC_ProcessPartials(vh); } else { dprintf4(("buffer %d done", dwEvent)); if (dwEvent < NR_SENT_BUFFERS) { VC_ProcessBuffer(vh, dwEvent); } } break; } } VC_Callback(vh->pCallback, MM_DRVM_CLOSE, 0); /* clean up events - except the one created by the other thread */ for (i = 0; i <= NR_SENT_BUFFERS; i++) { CloseHandle(vh->hEvents[i]); } return(0); } /* * send a callback to the user. * if callback is null, no callback required. */ VOID VC_Callback(PVCCALLBACK pCallback, DWORD msg, DWORD data) { if (pCallback == NULL) { return; } DriverCallback(pCallback->dwCallback, HIWORD(pCallback->dwFlags), pCallback->hDevice, msg, pCallback->dwUser, data, 0 // not used ); } /* * try to queue another add-buffer request to the driver if not * all of our NR_SENT_BUFFERS is used up, and the local queue of * add-buffers is not empty */ VOID VC_TrySendBuffer(VCUSER_HANDLE vh) { LPVIDEOHDR lpvh; int i; DWORD dwCount; /* don't send any more requests if the device is reset - we are * busy trying to clear the queued requests out of the driver, so it * doesn't help to send them back! */ if (vh->bReset) { return; } /* if in partials mode, then we just q the buffer in user mode * and capture the data when a cap-to-sysbuf completes */ if (vh->bPartials) { /* if there is no outstanding add-buffer in the kernel queue, * then its time to start the first cap-to-sysbuf if not already * started */ if ((vh->iSentCount == 0) && (!vh->bCapOutstanding)) { VC_InitCapToSysBuf(vh); } return; } if (vh->iSentCount >= NR_SENT_BUFFERS) { return; } /* find the free slot */ for (i = 0; i < NR_SENT_BUFFERS; i++) { if (vh->lpBuffers[i] == NULL) { break; } } ASSERT(i < NR_SENT_BUFFERS); if ((lpvh = VC_RemoveHead(vh)) == NULL) { /* we have nothing queued locally to send */ return; } vh->iSentCount++; vh->lpBuffers[i] = lpvh; vh->overlapped[i].hEvent = vh->hEvents[i]; if (!DeviceIoControl(vh->hDriver, IOCTL_VIDC_ADD_BUFFER, lpvh, sizeof(VIDEOHDR), lpvh, sizeof(VIDEOHDR), &dwCount, &vh->overlapped[i]) ) { if (GetLastError() == ERROR_IO_PENDING) { /* request is queued */ dprintf4(("buffer %d queued", i)); return; } else { dprintf(("add-buffer failed %d", GetLastError())); /* the buffer probably failed because of size - * switch to partials mode */ vh->bPartials = TRUE; /* replace the buffer on the queue */ vh->lpBuffers[i] = NULL; vh->iSentCount--; VC_ReplaceHead(vh, lpvh); /* * don't start queueing cap-to-sysbuf until any previous * add-buffers that were queued ok have completed. */ vh->bCapOutstanding = FALSE; if (vh->iSentCount > 0) { return; } else { VC_InitCapToSysBuf(vh); return; } } } else { /* reset the overlapped event */ ResetEvent(vh->hEvents[i]); /* note that the buffer has completed */ dprintf4(("buffer %d done", i)); VC_ProcessBuffer(vh, i); } } /* * one of the buffers has completed. * * callback if necessary, then queue another buffer */ VOID VC_ProcessBuffer(VCUSER_HANDLE vh, DWORD Buffer) { LPVIDEOHDR lpVideoHdr; ResetEvent(vh->hEvents[Buffer]); lpVideoHdr = vh->lpBuffers[Buffer]; ASSERT(lpVideoHdr != NULL); /* clear the flags saying this buffer is still outstanding */ vh->iSentCount--; vh->lpBuffers[Buffer] = NULL; /* * after completing a VC_StreamReset, none of the LPVIDEOHDR pointers * are valid (the user thread may have freed them. * However we may still end up here to process buffers that * were queued in the kernel: we released them by the STREAM_FINI * ioctl, and then completed the VC_StreamReset call before * waiting for them to finish. */ if (!vh->bReset) { /* mark buffer done */ lpVideoHdr->dwFlags |= (VHDR_DONE | VHDR_KEYFRAME); /* callback if necessary */ VC_Callback(vh->pCallback, MM_DRVM_DATA, (DWORD) lpVideoHdr); } /* try to queue another buffer */ VC_TrySendBuffer(vh); } /* * we have got a skip-count back from the driver. Increment our * local count and callback if necessary */ VOID VC_SkipCount(VCUSER_HANDLE vh, DWORD dwSkips) { vh->SkipCount += dwSkips; if (dwSkips > 0) { VC_Callback(vh->pCallback, MM_DRVM_ERROR, vh->SkipCount); } } /* * try to queue another async wait-error requests. Note that * if there have been frame-skips since the previous wait-error * completed, this wait-error will complete immediately. * * If there are skips to report, call VC_SkipCount to increment * our local skip count and callback to app. */ VOID VC_SendWaitError(VCUSER_HANDLE vh) { DWORD dwCount; if ((vh->bWaitOutstanding) || (vh->bReset)) { return; } dprintf4(("sending wait-error")); vh->overlapped[NR_SENT_BUFFERS].hEvent = vh->hEvents[NR_SENT_BUFFERS]; vh->WaitResult = 0; if (!DeviceIoControl(vh->hDriver, IOCTL_VIDC_WAIT_ERROR, NULL, 0, &vh->WaitResult, sizeof(vh->WaitResult), &dwCount, &vh->overlapped[NR_SENT_BUFFERS]) ) { if (GetLastError() == ERROR_IO_PENDING) { /* request is queued */ dprintf3(("wait error queued")); vh->bWaitOutstanding = TRUE; return; } else { dprintf(("wait-error failed")); } } else { /* completed immediately - inc skipcount and callback */ ResetEvent(vh->hEvents[NR_SENT_BUFFERS]); dprintf3(("wait error immediate")); VC_SkipCount(vh, vh->WaitResult); } } /* * an async wait-error has completed. increment our * local skip count, and send a callback if necessary */ VOID VC_CompleteWaitError(VCUSER_HANDLE vh) { DWORD dwCount; vh->bWaitOutstanding = FALSE; /* get the result of the operation */ if (!GetOverlappedResult(vh->hDriver, &vh->overlapped[NR_SENT_BUFFERS], &dwCount, FALSE) ) { if (GetLastError() == ERROR_IO_INCOMPLETE) { dprintf(("wait-error incomplete")); vh->bWaitOutstanding = TRUE; } else { dprintf(("wait-error failed %d", GetLastError())); } } else { ResetEvent(vh->hEvents[NR_SENT_BUFFERS]); /* all ok - increment count and callback */ VC_SkipCount(vh, vh->WaitResult); } } /* * clear out our local queue of add-buffer requests that * have not yet been sent to the driver. * */ VOID VC_ResetQueue(VCUSER_HANDLE vh) { LPVIDEOHDR lpvh; while ( (lpvh = VC_RemoveHead(vh)) != NULL) { /* mark buffer done */ lpvh->dwFlags |= VHDR_DONE; /* callback if necessary */ VC_Callback(vh->pCallback, MM_DRVM_DATA, (DWORD) lpvh); } } /* * process a user request that needs to be handled on the * worker thread * * It returns the result of the operation in pResult, and returns * TRUE if the worker thread loop should terminate. */ BOOL VC_ProcessFunction(VCUSER_HANDLE vh, VCFUNC Function, DWORD Param, LPDWORD pResult) { DWORD dwCount; switch(Function) { case StreamFini: dprintf2(("stream fini")); /* * end streaming request - send a reset first to cancel the * outstanding wait-error request, then pass fini to driver. */ DeviceIoControl(vh->hDriver, IOCTL_VIDC_STREAM_RESET, NULL, 0, NULL, 0, &dwCount, NULL); DeviceIoControl(vh->hDriver, IOCTL_VIDC_STREAM_FINI, NULL, 0, NULL, 0, &dwCount, NULL); /* free up any outstanding buffers in our queues (shouldn't * be any since he should have called streamreset first, * but just in case) */ VC_ResetQueue(vh); /* note that we should terminate this thread now */ return(TRUE); case AddBuffer: dprintf3(("add buffer")); VC_AddTail(vh, (LPVIDEOHDR) Param); VC_TrySendBuffer(vh); /* add-buffer is ok */ *pResult = TRUE; /* we don't want to terminate */ return(FALSE); case GetError: dprintf4(("get error")); /* clear the reset flag to allow more wait-errors to be queued */ vh->bReset = FALSE; *pResult = vh->SkipCount; /* * reset the count after a read */ vh->SkipCount = 0; return FALSE; case StreamReset: dprintf3(("stream reset")); vh->bReset = TRUE; /* send the reset onto the driver */ * (PBOOL) pResult = DeviceIoControl(vh->hDriver, IOCTL_VIDC_STREAM_RESET, NULL, 0, NULL, 0, &dwCount, NULL); /* now release all our queued buffers */ VC_ResetQueue(vh); /* we don't want to terminate */ return(FALSE); case StreamStart: /* note that we have started and its ok to q buffers */ vh->bReset = FALSE; /* try to queue a couple of buffers if possible */ VC_TrySendBuffer(vh); VC_TrySendBuffer(vh); * (PBOOL) pResult = DeviceIoControl(vh->hDriver, IOCTL_VIDC_STREAM_START, NULL, 0, NULL, 0, &dwCount, NULL); return(FALSE); case StreamStop: /* no more sending buffers */ vh->bReset = TRUE; * (PBOOL) pResult = DeviceIoControl(vh->hDriver, IOCTL_VIDC_STREAM_STOP, NULL, 0, NULL, 0, &dwCount, NULL); return(FALSE); default: dprintf(("bad function %d", Function)); return(FALSE); } } /* * Initialise partial-capture: queue a cap-to-sysbuf request * asynchronously. */ VOID VC_InitCapToSysBuf(VCUSER_HANDLE vh) { DWORD dwCount; if (vh->bReset) { return; } if (vh->bCapOutstanding) { return; } vh->overlapped[0].hEvent = vh->hEvents[0]; if (!DeviceIoControl(vh->hDriver, IOCTL_VIDC_CAP_TO_SYSBUF, NULL, 0, NULL, 0, &dwCount, &vh->overlapped[0]) ) { if (GetLastError() == ERROR_IO_PENDING) { /* request is queued */ vh->bCapOutstanding = TRUE; return; } else { /*request failed */ dprintf(("Cap-to-Sysbuf failed %d", GetLastError())); } } else { /* completed straightaway */ /* reset the overlapped event */ ResetEvent(vh->hEvents[0]); VC_ProcessPartials(vh); } } /* * Handle completion of a cap-to-sysbuf request. If all ok, * copy the data to a user buffer using partial-capture ioctls, * and then release the buffer and queue a new capture. * */ VOID VC_ProcessPartials(VCUSER_HANDLE vh) { LPVIDEOHDR lpvh; PCAPTUREBUFFER pCap; DWORD dwCount; ResetEvent(vh->hEvents[0]); vh->bCapOutstanding = FALSE; if ((lpvh = VC_RemoveHead(vh)) != NULL) { /* * now that the buffer is captured into * a system buffer, we can send partial * capture requests to copy windows of it into the * user buffer. As these copies are not done at * interrupt time, the size of the buffer no * longer matters, so (somewhat counter-intuitively) we * send one partial-capture representing the entire buffer. */ pCap = (PCAPTUREBUFFER) lpvh; pCap->dwWindowOffset = 0; pCap->dwWindowLength = pCap->BufferLength; if (!DeviceIoControl(vh->hDriver, IOCTL_VIDC_PARTIAL_CAPTURE, pCap, sizeof(CAPTUREBUFFER), pCap, sizeof(CAPTUREBUFFER), &dwCount, NULL) ) { dprintf(("partial capture failed")); } else { /* mark buffer done */ lpvh->dwFlags |= (VHDR_DONE | VHDR_KEYFRAME); /* release the system buffer */ DeviceIoControl(vh->hDriver, IOCTL_VIDC_FREE_SYSBUF, NULL, 0, NULL, 0, &dwCount, NULL); /* callback if necessary */ VC_Callback(vh->pCallback, MM_DRVM_DATA, (DWORD) lpvh); } } /* release the system buffer */ if (!DeviceIoControl(vh->hDriver, IOCTL_VIDC_FREE_SYSBUF, NULL, 0, NULL, 0, &dwCount, NULL)) { dprintf(("free-sysbuf failed")); } /* try to queue another buffer */ VC_InitCapToSysBuf(vh); } /* * -- buffer-queueing functions ----------------------- * * maintain a single-linked list of LPVIDEOHDR buffer headers for which * we can extract the first one or add to the end. * * we use the dwReserved[0] field as the link. vh->ListHead points to the * first item in the list. vh->ListTail points to the last item in the list * which itself points to NULL. for an empty list, both ListHead and ListTail * are null. */ #define NEXT_LPVH(p) (LPVIDEOHDR)((p)->dwReserved[0]) /* * extract the first item in the list of LPVIDEOHDRs. NULL if none in list */ LPVIDEOHDR VC_RemoveHead(VCUSER_HANDLE vh) { LPVIDEOHDR lpvh; if (vh->ListHead == NULL) { return(NULL); } lpvh = vh->ListHead; vh->ListHead = NEXT_LPVH(lpvh); if (vh->ListHead == NULL) { vh->ListTail = NULL; } return(lpvh); } /* * add an item to the tail of the list */ VOID VC_AddTail(VCUSER_HANDLE vh, LPVIDEOHDR lpvh) { if (vh->ListHead == NULL) { vh->ListHead = lpvh; } else { NEXT_LPVH(vh->ListTail) = lpvh; } NEXT_LPVH(lpvh) = NULL; vh->ListTail = lpvh; } /* * replace an item at the head of the list */ VOID VC_ReplaceHead(VCUSER_HANDLE vh, LPVIDEOHDR lpvh) { if (vh->ListHead == NULL) { VC_AddTail(vh, lpvh); } else { NEXT_LPVH(lpvh) = vh->ListHead; vh->ListHead = lpvh; } }