Threading Model

WinDV runs up to five concurrent threads. This document describes each thread’s role, the CDVQueue ring buffer that connects them, the synchronisation primitives used, and the shutdown sequence that must be respected to avoid deadlocks and data corruption.

Related reading: Architecture for state transitions, DirectShow Pipeline for the pipeline each thread operates on.


Thread Inventory

Thread Owner Priority Notes
Main (UI) CDVToolsDlg HIGH_PRIORITY_CLASS Message pump, timer
DS streaming DS runtime Unmanaged Receive() / Deliver()
Capturing CDV NORMAL Drains queue, writes AVI
Recording CDV NORMAL Drains queue, sends to out
Monitoring CMonitor BELOW_NORMAL Rate-limited preview
Joiner CAVIJoiner NORMAL Sequential file transitions

Only one of CapturingThread or RecordingThread is active at a time (they are mutually exclusive modes). JoinerThread is only active during a record session. MonitoringThread is active whenever CMonitor is alive (both modes). The DirectShow streaming thread is managed entirely by the DirectShow runtime and is not created or joined by WinDV directly.

Why HIGH_PRIORITY_CLASS? DV runs at a fixed 25 fps (PAL) or 29.97 fps (NTSC). A single missed frame cannot be recovered. Raising the process priority class reduces the likelihood that the OS scheduler preempts WinDV during the narrow window between frame arrival and disk write.


CDVQueue Ring Buffer

CDVQueue is a fixed-capacity FIFO ring buffer for raw DV frames. It bridges the DirectShow streaming thread (producer) with the CapturingThread or RecordingThread (consumer).

Memory Layout

m_buffers[]  (single contiguous allocation)
+----------+----------+----------+----------+
| Buffer 0 | Buffer 1 | Buffer 2 |   ...    |
+----------+----------+----------+----------+
  ^                                 ^
  m_queue[0]                        m_queue[N]

struct Buffer {
    REFERENCE_TIME duration;   // frame duration (100-ns units)
    int            len;        // valid bytes in data[]
    BYTE           data[1];    // variable-length; actual size = m_dataSize
};

The queue is constructed with queueSize + 1 slots so that m_load unambiguously represents the number of filled slots. A classic two-pointer ring (head == tail = full OR empty?) requires an extra sentinel; using an explicit m_load counter avoids that ambiguity.

Default parameters in CDV::BuildCapturing() and CDV::BuildRecording():

m_queue = new CDVQueue(100, type.GetSampleSize());
  • 100 slots at PAL frame size (144,000 bytes) = ~13.8 MB total.
  • At 25 fps this represents 4 seconds of buffering.

Producer: CDVQueue::Put(duration, data, len)

Called on the DirectShow streaming thread via CDV::HandleFrame().

data != NULL:
  Acquire m_cs
    if m_load < m_queueSize - 1:
      copy data into m_queue[m_tail]->data
      advance m_tail, increment m_load
      signal m_evGet  (wake consumer if blocked)
    else:
      release m_cs, wait on m_evPut (ring full, backpressure)
      retry

data == NULL (end-of-stream):
  set m_end = true
  signal both m_evGet and m_evPut  (wake any blocked thread)
  return immediately

If the ring is full the producer blocks on m_evPut. This propagates backpressure upstream: the FireWire driver’s own internal buffer absorbs the brief stall. If that buffer also fills, frames are dropped at the driver level and counted by IAMDroppedFrames.

Consumer: CDVQueue::Get(duration, data, len)

Called on CapturingThread or RecordingThread.

Acquire m_cs
  if m_load > 0:
    read m_queue[m_head]
    advance m_head, decrement m_load
    signal m_evPut  (wake producer if blocked)
    return true
  else:
    if m_end: return false  (EOS, ring empty)
    release m_cs, wait on m_evGet
    retry

The returned *data pointer points directly into the ring slot. The caller must consume or copy the data before calling Get() again, because the next Put() may overwrite the same slot. In practice, CapturingThread passes the pointer directly to CAVIWriter::HandleFrame() (which calls CopyMemory internally) before calling Get() again.

Consumer: CDVQueue::GetWithTimeout(duration, data, len, timeout)

Introduced in v1.2.7. Identical to Get() except that instead of blocking indefinitely on m_evGet, it uses WaitForSingleObject(m_evGet, timeout).

Acquire m_cs
  if m_load > 0:
    read m_queue[m_head]
    advance m_head, decrement m_load
    signal m_evPut
    return true
  else:
    if m_end: return false
    release m_cs
    result = WaitForSingleObject(m_evGet, timeout)
    if result == WAIT_TIMEOUT: return false  (signal lost)
    retry

When the wait times out the function returns false with *data == NULL and *len == 0, which CapturingThread distinguishes from a genuine EOS (m_end == true) to detect signal loss. See CapturingThread for how the timeout drives the auto-stop behaviour.


Synchronisation Primitives

Primitive Class Protects
CCritSec m_cs CDVQueue m_head, m_tail, m_load
CEvent m_evGet CDVQueue Signals consumer when a slot is filled
CEvent m_evPut CDVQueue Signals producer when a slot is freed
CEvent m_ev CAVIJoiner Signals JoinerThread on EOS
CEvent m_ev CMonitor Signals MonitoringThread on frame ready
CCritSec m_cs CDV Protects m_captureFilename and m_errorStats
CAutoLock Both RAII guard for CCritSec

m_errorStats (CDV::m_errorStats, ErrorStats) is updated by CapturingThread after every frame via AccumulateErrorStats() under a CAutoLock on CDV::m_cs. The UI timer reads it from the main thread via CDV::GetErrorStats(), which copies the struct under the same lock:

ErrorStats CDV::GetErrorStats()
{
    CAutoLock lock(&m_cs);
    return m_errorStats;
}

This copy-under-lock pattern avoids partial reads of the multi-field struct without needing a reader/writer lock.

CEvent in the DirectShow BaseClasses wraps a Win32 manual-reset event. CSingleLock::Lock() on a CEvent calls WaitForSingleObject(INFINITE). CDVQueue::GetWithTimeout() calls WaitForSingleObject(m_evGet, timeout) directly to support a finite wait without CSingleLock.

CAutoLock is the RAII lock guard for CCritSec. It is used as a local variable:

{
    CAutoLock lock(&m_cs);
    // m_cs held here
}   // m_cs released at scope exit

Thread Lifecycle

All application-managed threads follow the same pattern:

m_thread = AfxBeginThread(
    ThreadProc,          // static UINT __cdecl trampoline
    this,                // parameter (cast to correct type in body)
    THREAD_PRIORITY_xxx,
    0,                   // stack size (0 = default)
    CREATE_SUSPENDED     // start suspended
);
m_thread->m_bAutoDelete = FALSE;   // prevent automatic deletion on exit
m_thread->ResumeThread();          // start running

Why CREATE_SUSPENDED followed by ResumeThread()? m_bAutoDelete = FALSE must be set before the thread runs, otherwise the CWinThread object could be deleted by the MFC framework before the owner has a chance to call WaitForSingleObject() and delete m_thread. The suspended start gives a window to set the flag atomically.

Why m_bAutoDelete = FALSE? The default TRUE causes MFC to delete this when the thread proc returns. With FALSE, ownership stays with the creating object (CDV, CMonitor, CAVIJoiner), which explicitly calls WaitForSingleObject(m_thread->m_hThread, INFINITE) followed by delete m_thread. This guarantees the thread has fully exited before any shared state is freed.


Cross-thread UI Updates

All Win32 window operations must happen on the thread that created the window – the main UI thread.

CDV::CapturingThread() and CDV::RecordingThread() post WM_DV_TIMECHANGE to the parent window whenever the DV recording timestamp changes:

GetParent()->PostMessage(WM_DV_TIMECHANGE, 0, dvTime);

PostMessage is non-blocking: it places the message in the window’s message queue and returns immediately. The main thread processes it in CDVToolsDlg::OnDVTimeChange(), which reads lParam as a time_t and updates m_status2.

WM_DV_TIMECHANGE is defined as WM_USER + 201 and routed via ON_MESSAGE(WM_DV_TIMECHANGE, OnDVTimeChange) in the message map.

Status labels (m_status, m_counter, m_status3) are updated by the 200 ms UI timer (OnTimer), which calls CDV::GetState(), CDV::GetTime(), CDV::GetDropped(), and CDV::GetQueueLoad(). These accessors return int / REFERENCE_TIME values; their reads are not individually lock-protected because they are updated by a single worker thread and consumed by the UI thread at a much lower rate. The only lock-protected cross-thread value is m_captureFilename (guarded by CDV::m_cs).


Preview Throttling Strategy

Preview is deliberately deprioritised relative to the primary data path. The throttling direction differs between the two modes.

Capture Mode (CapturingThread)

if (m_queue->m_load < m_queue->m_queueSize / 2)
    m_monitor->HandleFrame(duration, buffer, len);

Preview is sent only when the queue is below half full. A queue above half capacity means the disk is falling behind; skipping preview prevents the DV decoder and video renderer from competing with disk I/O for CPU time.

Disk Space Monitoring (v1.2.6)

Every ~1500 frames (approximately one minute at 25 fps) CapturingThread calls GetDiskFreeSpaceEx() on the drive root extracted from m_captureFilename. If fewer than 500 MB remain, it posts WM_DV_LOWDISKSPACE (WM_USER + 202) to the parent window. lParam carries the remaining free space in megabytes.

Signal Loss Detection (v1.2.7)

When CDV::m_autoStopTimeout > 0, CapturingThread calls CDVQueue::GetWithTimeout(…, m_autoStopTimeout) instead of the blocking Get(). If GetWithTimeout() returns false without m_end being set (i.e. the wait timed out), the thread treats this as a lost signal: it posts WM_DV_SIGNALLOST (WM_USER + 203) to the parent window, sets the capture state to Finished, and exits. The default timeout is 5000 ms; setting m_autoStopTimeout to 0 disables the feature and restores the original blocking behaviour.

Post-capture Phase (v1.6.0)

After the last frame has been written and the CAVIWriter is deleted (which finalizes the AVI file), CapturingThread performs a sequence of post-capture operations on the completed file before posting to the UI. This phase runs entirely within CapturingThread – it does not require UI interaction:

1. Copy m_errorStats under m_cs lock into capStats.errorStats.
2. CheckAVIIntegrity(m_captureFilename)
      -- full RIFF structural validation (AVICheck.cpp).
      -- result stored in m_lastCheckResult.
3. ComputeFileSHA256(m_captureFilename, capStats.szSHA256)  [if m_enableSHA256]
      -- streams the file in 64 KB blocks through sha256.c.
      -- WriteSHA256Sidecar() writes <filename>.sha256 next to the AVI.
4. WriteCaptureLog(logPath, capStats)
      -- appends one CSV row to WinDV_CaptureLog.csv in the capture directory.
5. PostMessage(WM_DV_CHECK_COMPLETE, bCheckPassed, 0)
      -- CDVToolsDlg::OnDVCheckComplete() reads m_lastCheckResult and
        m_errorStats to build the status-bar summary.

Steps 2-4 are I/O-bound and may take several seconds for large files. They run on CapturingThread, not the UI thread, so the dialog remains responsive throughout.

WM_DV_CHECK_COMPLETE is defined as WM_USER + 204.

SafeAttachConsole: SafeAttachConsole() is declared in StdAfx.h as an inline function. It loads AttachConsole() at runtime via GetProcAddress from kernel32.dll, making the binary link on Windows 2000 (where AttachConsole is not available) while still supporting console attachment on XP and later. This follows the same XP-compatibility pattern used for other newer APIs such as the DPI-awareness functions in WinDV.cpp.

Record Mode (RecordingThread)

if (m_recordPreview) {
    if (m_queue->m_end || m_queue->m_load > m_queue->m_queueSize / 2)
        m_monitor->HandleFrame(duration, buffer, len);
}

Preview is sent only when the queue is above half full (or EOS). During recording, the queue is filled from AVI files on disk; a full queue means the source is reading ahead of the DV device’s consumption. A nearly-empty queue means frames are being consumed as fast as they arrive; skipping preview prevents MonitoringThread from stalling the delivery to the DV device.

MonitoringThread Rate Limiter

Even when the calling thread does offer a frame to CMonitor, MonitoringThread imposes an additional rate limit of approximately 5 fps via a dynamic sleep:

ticks = GetTickCount() - ticks;
Sleep(ticks < 200 ? ticks + 10 : 200);

If less than 200 ms has elapsed since the last delivery, it sleeps the remaining time (plus 10 ms to avoid busy-looping). If more than 200 ms has elapsed, it sleeps the full 200 ms maximum.


Shutdown Sequence

CDV::Destroy() performs a carefully ordered teardown to avoid deadlocks and ensure all data is flushed to disk:

1.  m_state = Idle
    CapturingThread / RecordingThread check m_state at the top of their
    loops; setting Idle causes them to exit on the next iteration.

2.  m_queue->Put(-1, NULL, 0)
    Sends the EOS sentinel.  If the worker thread is blocked in
    CDVQueue::Get(), it will wake up and see m_end = true.

3.  m_aviJoiner->Stop()  or  m_dvInput->Stop()
    Stops the frame source so no new frames enter the queue.
    Stop() for CAVIJoiner sets m_stopping and signals m_ev to unblock
    JoinerThread; it then waits for JoinerThread to exit.

4.  WaitForSingleObject(m_thread->m_hThread, INFINITE)
    Blocks until the worker thread (CapturingThread or RecordingThread)
    has exited cleanly.
    delete m_thread.

5.  delete m_aviJoiner / delete m_dvInput
6.  delete m_aviWriter
    CAVIWriter destructor sends DeliverEndOfStream(), waits up to 5 s
    for EC_COMPLETE, then renames the temp file to its final name.
7.  delete m_dvOutput
    CDVOutput destructor waits up to 5 s for EC_COMPLETE.
8.  delete m_monitor
    CMonitor destructor releases IVideoWindow, signals m_ev to wake
    MonitoringThread, waits for MonitoringThread to exit, then deletes
    it.
9.  delete m_queue
10. Reset m_dropped, m_counter, m_time, m_captureTime.

The ordering in steps 5-8 is critical:

  • m_aviWriter must be deleted before m_monitor because CapturingThread may still be writing to m_aviWriter while consuming from the queue. Step 4 guarantees the thread is done before either is deleted.
  • m_dvOutput and m_monitor both wait internally for their own threads (COutputQueue delivery thread and MonitoringThread), so they are safe to delete after the worker thread has exited.
  • m_queue is deleted last because m_monitor->HandleFrame() may be called by CapturingThread right up to the moment the thread exits.