Background Inference
Overview
Deep learning batch inference runs on a background QThread so the UI stays responsive during long runs. Results appear progressively in the UI as the model processes frames.
Threading Pattern
The implementation follows the thread confinement + merge model from the Concurrency Architecture:
- DataManager stays single-threaded. All writes happen on the main (Qt) thread.
- The worker uses private data. A cloned
VideoDatagives the worker its own FFmpeg decoder. Non-videoMediaDatais shared (stateless reads). - Results merge on the main thread. A
QTimer(200ms) periodically drains a sharedWriteReservationbuffer and writes results into DataManager. - Qt signals provide cross-thread communication. Progress and completion signals use
Qt::QueuedConnectionautomatically.
Key Components
WriteReservation
Thread-safe buffer connecting the worker thread to the main thread. The worker pushes decoded FrameResult entries via push(), and the main thread drains them via drain(). A std::mutex protects the internal vector.
Worker thread Main thread
───────────── ───────────
decodeFrame() ──→ push(results)
QTimer (200ms) ──→ drain()
decodeFrame() ──→ push(results) ├─ addAtTime(...)
└─ notifyObservers()
decodeFrame() ──→ push(results)
QThread::finished ──→ drain() (final)
└─ cleanup
BatchInferenceWorker
A QThread subclass (defined in an anonymous namespace in DeepLearningPropertiesWidget.cpp) that runs SlotAssembler::runBatchRangeOffline() with a ResultCallback. The callback pushes each frame’s decoded results into the shared WriteReservation.
The worker holds a std::atomic<bool> _cancel_requested flag checked by runBatchRangeOffline() before each frame.
SlotAssembler::ResultCallback
An optional callback parameter on runBatchRangeOffline():
using ResultCallback = std::function<void(std::vector<FrameResult>)>;When provided, decoded results are pushed via the callback per-frame instead of being accumulated in BatchInferenceResult::results. This enables progressive delivery without changing the core inference loop.
When not provided (default nullptr), the old behavior is preserved: results accumulate in the returned BatchInferenceResult.
Data Flow
Startup (_onRunBatch)
- User selects frame range in the dialog
VideoDatais cloned for the worker’s independent FFmpeg decoder- A
WriteReservationis created (shared between worker and main thread) - A
BatchInferenceWorkeris created with the reservation and aResultCallback - A
QTimer(200ms) is started for periodic merging - The worker thread starts
During Inference
- Worker thread: For each frame, assembles inputs, runs forward pass, decodes outputs, pushes
FrameResults intoWriteReservation::push() - Main thread (timer): Every 200ms,
_mergeResults()callsWriteReservation::drain(), writes results to DataManager usingaddAtTime(..., NotifyObservers::No), then callsnotifyObservers()once per affected key. The UI redraws and the user sees results appearing.
Completion (_onBatchFinished)
- Stop the merge timer
- Final
_mergeResults()to pick up any results since the last timer tick - Report success or error
- Clean up worker and reservation
Cancellation
The “Run Batch” button becomes “Cancel Batch” during inference. Clicking it sets _cancel_requested = true on the worker. The inference loop checks this before each frame and exits early. Any results computed before cancellation are still merged.
Why a Separate VideoData?
VideoData wraps a stateful FFmpeg decoder with mutable internal state (seek position, codec context, frame buffer). Two threads cannot read frames from the same VideoData without corrupting each other’s state. Creating a second VideoData at the same file path gives the worker its own independent decoder.
This aligns with ConcurrencyTraits<VideoData>::supports_cheap_clone = true.
Generalizing to Other Components
The same pattern (WriteReservation + QTimer merge) can be applied to DataTransform_Widget or any other long-running computation:
- Create a
WriteReservationshared between worker and main thread - Run computation on a
QThread, pushing results via callback - Merge periodically on the main thread using a
QTimer - Final merge on
QThread::finished