mux: PrefetchedSectorSource event_fn + delete DiscStream::new_pipeline

* `PrefetchedSectorSource::new_with_events` adds an optional
  `event_fn` callback that fires `BytesRead` after every successful
  batch from the producer thread. The original `new()` becomes a
  thin no-events wrapper. Lets autorip wire the highway and still
  get UI progress events without polling the consumer side.

* `build_iso_pipeline` grows an `event_fn` arg so the autorip
  multipass mux can pipe BytesRead straight through to its progress
  UI.

* Stream trait gains a default `errors() -> u64` method (= 0) so
  Box<dyn Stream> callers (autorip's mux loop) can read the
  skip-on-error counter without downcasting. `DiscStream` overrides
  to return its `errors` field.

* Delete `DiscStream::new_pipeline` and the pipeline-mode fields
  (`demux_thread`, `demux_rx`) plus the `read_pipeline` helper.
  All pipeline construction now goes through
  `PipelinedPesStream` via `build_iso_pipeline`; `DiscStream`
  becomes the single-thread-only inline path used by the drive
  single-pass read.

* `lib.rs` re-exports `build_iso_pipeline`.
This commit is contained in:
MattJackson
2026-05-19 14:19:37 -07:00
parent eeca250f69
commit 4da559e39f
5 changed files with 60 additions and 226 deletions
+36 -1
View File
@@ -41,11 +41,18 @@
//! reading the underlying source directly.
use crate::error::Result;
use crate::event::{Event, EventKind};
use crate::halt::Halt;
use crate::sector::SectorSource;
use crossbeam_channel::{Receiver, Sender, bounded};
use std::thread::JoinHandle;
/// Producer-thread event callback. Fires `BytesRead` after every
/// successful batch read so the consumer side can update a UI
/// progress indicator without polling. `Send + 'static` because the
/// callback runs on the producer thread.
pub type EventFn = Box<dyn Fn(Event) + Send + 'static>;
const PREFETCH_CHANNEL_DEPTH: usize = 2;
/// Smallest sector source the producer will issue per read. AACS
@@ -85,15 +92,33 @@ impl PrefetchedSectorSource {
/// — every byte the producer emits is what the consumer's demux
/// will feed to its codec parsers.
pub fn new<S>(
mut reader: S,
reader: S,
extents: Vec<crate::disc::Extent>,
batch_sectors: u16,
halt: Option<Halt>,
) -> Self
where
S: SectorSource + Send + 'static,
{
Self::new_with_events(reader, extents, batch_sectors, halt, None)
}
/// Same as [`new`] but with a callback fired from the producer
/// thread after each successful batch — used by autorip's mux
/// path to surface `BytesRead` progress to the UI without the
/// consumer thread having to poll.
pub fn new_with_events<S>(
mut reader: S,
extents: Vec<crate::disc::Extent>,
batch_sectors: u16,
halt: Option<Halt>,
event_fn: Option<EventFn>,
) -> Self
where
S: SectorSource + Send + 'static,
{
let total_sectors: u32 = extents.iter().map(|e| e.sector_count).sum();
let bytes_total_extents: u64 = extents.iter().map(|e| e.sector_count as u64 * 2048).sum();
let (tx, rx) = bounded::<Batch>(PREFETCH_CHANNEL_DEPTH);
let (recycle_tx, recycle_rx) = bounded::<Vec<u8>>(PREFETCH_CHANNEL_DEPTH + 1);
let batch_bytes = batch_sectors as usize * 2048;
@@ -111,6 +136,7 @@ impl PrefetchedSectorSource {
.spawn(move || {
let mut ext_idx = 0usize;
let mut offset: u32 = 0;
let mut bytes_read_total: u64 = 0;
while ext_idx < extents.len() {
if halt.as_ref().map(|h| h.is_cancelled()).unwrap_or(false) {
return;
@@ -144,6 +170,15 @@ impl PrefetchedSectorSource {
match reader.read_sectors(lba, sectors, &mut buf[..bytes], false) {
Ok(n) => {
buf.truncate(n);
bytes_read_total = bytes_read_total.saturating_add(n as u64);
if let Some(ref f) = event_fn {
f(Event {
kind: EventKind::BytesRead {
bytes: bytes_read_total,
total: bytes_total_extents,
},
});
}
if tx.send(Ok(buf)).is_err() {
return; // consumer dropped
}