render: the M1 thread pipeline — one render thread, one decode thread
docs/zh/plans/render-pipeline-threads.md M1: an in-process alternative to the worker-process pool, behind OAK_PIPELINE=threads (processes stays the default and is fully retained). - pipeline.rs: PipelineBackend implements JobDispatch over a single render thread draining a bounded FIFO (cap 8; blocking post with condvar backpressure and a one-ahead exception for re-posts from the render thread itself; shutdown drains with Error::State like the inline dispatcher). The DecodeService is a single decode thread behind a bounded command queue with a real LRU (tick-based eviction), rendezvous requests (None on shutdown -> the caller decodes inline), prefetch gated on render-queue room, and a Sync barrier; it installs into a process-wide slot that eval's footage path consults per frame (no service -> the synchronous decode it always was). - The manager gains RenderBackendChoice::Pipeline; init() reads OAK_PIPELINE (threads -> pipeline, anything else -> the process pool), audio stays deliberately inline. - Present mapping: the UI thread consumes through the ticket completion, unchanged — no fourth thread is invented. - Tests: decode-service unit tests (rendezvous, LRU hit/eviction, error propagation, backpressure gate) plus a six-case integration suite matrixed over inline vs pipeline — consecutive-frame and out-of-order seek pixel equality asserted byte for byte, with decode counters proving the service (not the caller) did the codec work.
This commit is contained in:
@@ -1059,6 +1059,23 @@ const MAX_CACHED_DECODERS: usize = 6;
|
||||
/// LRU tick source for [`DECODERS`].
|
||||
static DECODER_TICK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
/// Count of [`render_footage_frame_inner`] entries, i.e. of real codec work
|
||||
/// on the footage path (frame-cache hits and decode-service LRU hits do not
|
||||
/// count). Visible for the decode-service tests, which use it to tell a
|
||||
/// cached frame from a re-decode.
|
||||
static DECODE_INVOCATIONS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// The number of real footage decodes since the last
|
||||
/// [`reset_decode_invocations`] (or process start).
|
||||
pub fn decode_invocations() -> u64 {
|
||||
DECODE_INVOCATIONS.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Reset the [`decode_invocations`] counter to zero.
|
||||
pub fn reset_decode_invocations() {
|
||||
DECODE_INVOCATIONS.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Cap on decoded FRAMES cached per process (release builds of the graph
|
||||
/// renderer re-decode every frame the pre-render window pulls — each is a
|
||||
/// seek+decode on the shared decoder session, which serializes the graph
|
||||
@@ -1175,6 +1192,12 @@ fn open_decoder(filename: &str, stream_index: i32) -> Result<Arc<dyn oak_codec::
|
||||
|
||||
/// Decode the footage frame at `time` and copy/scale it into an
|
||||
/// oakrender F32 frame of `(w, h)`.
|
||||
///
|
||||
/// While a [`crate::pipeline::DecodeService`] is installed (the pipeline
|
||||
/// backend's decode thread) the decode is a rendezvous with that service,
|
||||
/// which caches frames and keeps the codec calls off the caller's thread;
|
||||
/// with no service installed this is the synchronous decode it has always
|
||||
/// been. Either way the pixels are the same.
|
||||
pub fn render_footage_frame(
|
||||
filename: &str,
|
||||
stream_index: i32,
|
||||
@@ -1183,7 +1206,24 @@ pub fn render_footage_frame(
|
||||
format: PixelFormat,
|
||||
) -> Result<Texture> {
|
||||
let started = std::time::Instant::now();
|
||||
let result = render_footage_frame_inner(filename, stream_index, time, size, format);
|
||||
let result = match crate::pipeline::decode_service() {
|
||||
Some(service) => {
|
||||
let request = crate::pipeline::DecodeRequest {
|
||||
filename: filename.to_string(),
|
||||
stream_index,
|
||||
time,
|
||||
size,
|
||||
format,
|
||||
};
|
||||
match service.request(request) {
|
||||
Some(result) => result,
|
||||
// The service went away (shutdown) mid-flight: decode here
|
||||
// rather than failing the frame.
|
||||
None => render_footage_frame_inner(filename, stream_index, time, size, format),
|
||||
}
|
||||
}
|
||||
None => render_footage_frame_inner(filename, stream_index, time, size, format),
|
||||
};
|
||||
if std::env::var_os("OAK_PERF").is_some() {
|
||||
eprintln!(
|
||||
"[decode] {:?} s{} time {}/{} ({:.3}s) size {:?} -> {:.3}s {:?}",
|
||||
@@ -1200,7 +1240,10 @@ pub fn render_footage_frame(
|
||||
result
|
||||
}
|
||||
|
||||
fn render_footage_frame_inner(
|
||||
/// The synchronous decode behind [`render_footage_frame`] (frame-LRU →
|
||||
/// decoder session → codec → F32 frame). `pub(crate)` because the decode
|
||||
/// service runs exactly this on its own thread.
|
||||
pub(crate) fn render_footage_frame_inner(
|
||||
filename: &str,
|
||||
stream_index: i32,
|
||||
time: Rational,
|
||||
@@ -1224,6 +1267,9 @@ fn render_footage_frame_inner(
|
||||
return Ok(Texture::wrap_frame(frame));
|
||||
}
|
||||
}
|
||||
// Past the frame cache: this call really goes to the codec (the
|
||||
// decode-service tests read this counter to prove it).
|
||||
DECODE_INVOCATIONS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let decoder = open_decoder(filename, stream_index)?;
|
||||
let params = RetrieveVideoParams {
|
||||
stream: CodecStream::with_block(filename.to_string(), stream_index, None),
|
||||
|
||||
Reference in New Issue
Block a user