progress: PassProgress carries the located drilldown (emit side)
Add LocatedRange + LocatedProgress to the progress contract and a 'located' field on PassProgress, populated by the sweep + patch emitters from the in-memory bad-range set + title. Move the range->chapter/time annotation (locate_ranges, range_chapter, byte_offset_in_title) into the library so a client renders the disc map + at-risk movie time straight from PassProgress and never reads the mapfile itself — if the mapfile becomes a mapdb, this type and its producer change, clients don't. PassProgress is no longer Copy (located carries a Vec); it's built once per throttled emission and passed by reference. Non-locating phases (verify, extract) emit LocatedProgress::default(). Adds consts::MILLIS_PER_SEC. Consumer-side wiring (autorip drops Mapfile::load) follows.
This commit is contained in:
@@ -20,6 +20,10 @@ pub const SECTOR_BYTES: usize = 2048;
|
||||
/// the workspace reads as `sectors * SECTOR_BYTES_U64` with no per-site cast.
|
||||
pub const SECTOR_BYTES_U64: u64 = SECTOR_BYTES as u64;
|
||||
|
||||
/// Milliseconds per second. For turning a byte count ÷ bytes-per-second into a
|
||||
/// movie-time figure (`bytes / bps * MILLIS_PER_SEC`) without a bare `1000.0`.
|
||||
pub const MILLIS_PER_SEC: f64 = 1_000.0;
|
||||
|
||||
/// Bytes per MPEG-2 transport-stream packet. Common to all MPEG-TS, not just
|
||||
/// Blu-ray — prefixed by the format, not a disc type.
|
||||
pub const TS_PACKET_BYTES: usize = 188;
|
||||
|
||||
@@ -686,6 +686,7 @@ fn report(opts: &ExtractOptions, done: u64, total: u64) -> bool {
|
||||
bytes_bad_in_main_title: 0,
|
||||
main_title_duration_secs: None,
|
||||
main_title_size_bytes: None,
|
||||
located: Default::default(),
|
||||
};
|
||||
p.report(&pp)
|
||||
}
|
||||
|
||||
+106
@@ -588,6 +588,103 @@ pub fn bytes_bad_in_title(title: &DiscTitle, bad_ranges: &[(u64, u64)]) -> u64 {
|
||||
total
|
||||
}
|
||||
|
||||
/// Byte offset of `lba` within `title`'s extents (concatenated in order), or
|
||||
/// `None` if the LBA falls outside every extent. The title is a virtual
|
||||
/// contiguous stream; this maps a disc LBA into that stream so a chapter/time
|
||||
/// lookup can place it. (Moved from autorip — clients must not re-derive it.)
|
||||
fn byte_offset_in_title(lba: u32, title: &DiscTitle) -> Option<u64> {
|
||||
use crate::consts::SECTOR_BYTES_U64;
|
||||
let mut cumulative = 0u64;
|
||||
for ext in &title.extents {
|
||||
if lba >= ext.start_lba && lba < ext.start_lba + ext.sector_count {
|
||||
return Some(cumulative + (lba - ext.start_lba) as u64 * SECTOR_BYTES_U64);
|
||||
}
|
||||
cumulative += ext.sector_count as u64 * SECTOR_BYTES_U64;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The 1-based chapter + movie-time offset an LBA falls in, or `(None, None)`
|
||||
/// if it isn't inside the title.
|
||||
fn range_chapter(lba: u32, title: &DiscTitle) -> (Option<u32>, Option<f64>) {
|
||||
if let Some(byte_offset) = byte_offset_in_title(lba, title) {
|
||||
if let Some((ch, t)) = crate::verify::VerifyResult::chapter_at_offset(
|
||||
&title.chapters,
|
||||
byte_offset,
|
||||
title.duration_secs,
|
||||
title.size_bytes,
|
||||
) {
|
||||
return (Some(ch as u32), Some(t));
|
||||
}
|
||||
}
|
||||
(None, None)
|
||||
}
|
||||
|
||||
/// Annotate raw bad byte-ranges with chapter + movie time, producing the
|
||||
/// rendered drilldown ([`crate::progress::LocatedProgress`]) a client draws.
|
||||
/// `raw` is the mapfile's `(byte_pos, byte_len)` set for whichever statuses the
|
||||
/// caller cares about (the live "Maybe" set during a patch, or terminal
|
||||
/// `Unreadable` for the verdict). The list is sorted largest-movie-time first
|
||||
/// and capped at 50; `truncated` reports the overflow. `bps` (title bytes/sec)
|
||||
/// is derived from the title so callers don't thread it.
|
||||
///
|
||||
/// This is the single place range→chapter/time annotation happens; autorip used
|
||||
/// to own it and read the mapfile to do so. Now the library computes it from
|
||||
/// its in-memory mapfile + title, and the client renders the result verbatim.
|
||||
pub fn locate_ranges(raw: &[(u64, u64)], title: &DiscTitle) -> crate::progress::LocatedProgress {
|
||||
use crate::consts::{MILLIS_PER_SEC, SECTOR_BYTES_U64};
|
||||
use crate::progress::{LocatedProgress, LocatedRange};
|
||||
const MAX_LOCATED: usize = 50;
|
||||
let bps = if title.duration_secs > 0.0 {
|
||||
title.size_bytes as f64 / title.duration_secs
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let num_ranges = raw.len() as u32;
|
||||
let mut ranges: Vec<LocatedRange> = raw
|
||||
.iter()
|
||||
.map(|(pos, size)| {
|
||||
let lba = pos / SECTOR_BYTES_U64;
|
||||
let count = (size / SECTOR_BYTES_U64) as u32;
|
||||
let duration_ms = if bps > 0.0 {
|
||||
(*size as f64) / bps * MILLIS_PER_SEC
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let (chapter, time_offset_secs) = range_chapter(lba as u32, title);
|
||||
LocatedRange {
|
||||
lba,
|
||||
count,
|
||||
duration_ms,
|
||||
chapter,
|
||||
time_offset_secs,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
ranges.sort_by(|a, b| {
|
||||
b.duration_ms
|
||||
.partial_cmp(&a.duration_ms)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let largest_gap_ms = ranges.first().map(|r| r.duration_ms).unwrap_or(0.0);
|
||||
let truncated = ranges.len().saturating_sub(MAX_LOCATED) as u32;
|
||||
ranges.truncate(MAX_LOCATED);
|
||||
// At-risk movie time = duration of the ranges that intersect the title
|
||||
// extents (the others are menus/extras → no movie impact).
|
||||
let main_at_risk_ms = if bps > 0.0 {
|
||||
bytes_bad_in_title(title, raw) as f64 * MILLIS_PER_SEC / bps
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
LocatedProgress {
|
||||
ranges,
|
||||
num_ranges,
|
||||
truncated,
|
||||
main_at_risk_ms,
|
||||
largest_gap_ms,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Display helpers ────────────────────────────────────────────────────────
|
||||
|
||||
impl Codec {
|
||||
@@ -3512,6 +3609,15 @@ impl Disc {
|
||||
bytes_bad_in_main_title: main_title_bad,
|
||||
main_title_duration_secs: main_title.map(|t| t.duration_secs),
|
||||
main_title_size_bytes: main_title.map(|t| t.size_bytes),
|
||||
// Rendered drilldown from the consumer's in-memory
|
||||
// snapshot (bad ranges) + title; empty until the first
|
||||
// snapshot arrives.
|
||||
located: match &cached_snapshot {
|
||||
Some(snap) => main_title
|
||||
.map(|t| locate_ranges(&snap.bad_ranges, t))
|
||||
.unwrap_or_default(),
|
||||
None => crate::progress::LocatedProgress::default(),
|
||||
},
|
||||
};
|
||||
if !reporter.report(&pp) {
|
||||
halt_requested = true;
|
||||
|
||||
@@ -2124,6 +2124,12 @@ impl Disc {
|
||||
bytes_bad_in_main_title: main_title_bad,
|
||||
main_title_duration_secs: main_title.map(|t| t.duration_secs),
|
||||
main_title_size_bytes: main_title.map(|t| t.size_bytes),
|
||||
// The rendered drilldown — located ranges + at-risk movie time —
|
||||
// computed here from the in-memory bad-range set + title so the
|
||||
// client renders it verbatim and never reads the mapfile.
|
||||
located: main_title
|
||||
.map(|t| crate::disc::locate_ranges(&bad_ranges_now, t))
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
!reporter.report(&pp)
|
||||
}
|
||||
|
||||
+56
-1
@@ -35,6 +35,51 @@ pub enum PassKind {
|
||||
Verify,
|
||||
}
|
||||
|
||||
/// One located bad/not-yet-good range, annotated with the chapter and movie
|
||||
/// time it falls in. This is the *rendered* drilldown a client draws: the LBA
|
||||
/// and sector count place it on the disc map, while `chapter` and
|
||||
/// `time_offset_secs` tell the user *what* is affected. Computed by the library
|
||||
/// (which owns the mapfile and title) so no client ever re-derives it. If the
|
||||
/// mapfile becomes a mapdb, this type and its producer change; clients don't.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocatedRange {
|
||||
/// First sector (LBA) of the range.
|
||||
pub lba: u64,
|
||||
/// Length of the range in sectors.
|
||||
pub count: u32,
|
||||
/// Movie time this range spans, in milliseconds (range bytes ÷ title
|
||||
/// bytes/sec). Used to sort the drilldown and size the "largest gap".
|
||||
pub duration_ms: f64,
|
||||
/// 1-based chapter the range falls in, if it lands inside the title.
|
||||
pub chapter: Option<u32>,
|
||||
/// Movie time offset (seconds) where the range begins, if in-title.
|
||||
pub time_offset_secs: Option<f64>,
|
||||
}
|
||||
|
||||
/// The fully-rendered "where is the damage" view for one progress sample: the
|
||||
/// located drilldown plus the derived movie-time figures. A client maps this
|
||||
/// straight to its UI — it never touches the mapfile. `Default` is the empty
|
||||
/// (no-damage / not-applicable) view, used by phases that don't locate ranges
|
||||
/// (verify, extract, mux-label).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LocatedProgress {
|
||||
/// Located not-yet-good ranges, largest-movie-time first, capped (see
|
||||
/// `truncated`).
|
||||
pub ranges: Vec<LocatedRange>,
|
||||
/// Total number of located ranges before the cap (so a client can say
|
||||
/// "N sections").
|
||||
pub num_ranges: u32,
|
||||
/// How many ranges were dropped by the display cap (`ranges.len()` is the
|
||||
/// kept count; this is the "+X more").
|
||||
pub truncated: u32,
|
||||
/// Main-feature movie time still at risk: duration of the not-yet-good
|
||||
/// ranges that intersect the title extents, in milliseconds. `0` when all
|
||||
/// damage is out-of-feature (menus/extras).
|
||||
pub main_at_risk_ms: f64,
|
||||
/// Movie time of the single largest range, in milliseconds.
|
||||
pub largest_gap_ms: f64,
|
||||
}
|
||||
|
||||
/// One progress sample from a pipeline phase.
|
||||
///
|
||||
/// `work_done / work_total` is the per-pass percentage — always 0..=100%
|
||||
@@ -48,7 +93,12 @@ pub enum PassKind {
|
||||
/// - `bytes_good_total` = good + slow + recovered sectors × 2048
|
||||
/// - `bytes_unreadable_total` = bad sectors × 2048
|
||||
/// - `bytes_pending_total` = 0 (verify processes sequentially, nothing pending)
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
///
|
||||
/// NOT `Copy`: `located` carries a `Vec`. Constructed once per (throttled)
|
||||
/// emission and passed by reference to `Progress::report`, so this costs one
|
||||
/// small heap alloc per UI tick — cheap, and it makes `PassProgress` the single
|
||||
/// complete contract a client renders from.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PassProgress {
|
||||
pub kind: PassKind,
|
||||
pub work_done: u64,
|
||||
@@ -72,6 +122,11 @@ pub struct PassProgress {
|
||||
pub main_title_duration_secs: Option<f64>,
|
||||
/// Main title size in bytes (sum of extent sizes).
|
||||
pub main_title_size_bytes: Option<u64>,
|
||||
/// The fully-rendered "where is the damage" drilldown for this sample:
|
||||
/// located ranges + at-risk movie time. Empty (`Default`) for phases that
|
||||
/// don't locate ranges. A client renders the disc map + section list from
|
||||
/// this and NEVER reads the mapfile itself.
|
||||
pub located: LocatedProgress,
|
||||
}
|
||||
|
||||
impl PassProgress {
|
||||
|
||||
@@ -221,6 +221,7 @@ pub fn verify_title(
|
||||
bytes_bad_in_main_title: 0,
|
||||
main_title_duration_secs: Some(title.duration_secs),
|
||||
main_title_size_bytes: Some(total_sectors * 2048),
|
||||
located: Default::default(),
|
||||
};
|
||||
if !cb.report(&pp) {
|
||||
break 'outer;
|
||||
@@ -271,6 +272,7 @@ pub fn verify_title(
|
||||
bytes_bad_in_main_title: 0,
|
||||
main_title_duration_secs: Some(title.duration_secs),
|
||||
main_title_size_bytes: Some(total_sectors * 2048),
|
||||
located: Default::default(),
|
||||
})
|
||||
})
|
||||
});
|
||||
@@ -335,6 +337,7 @@ pub fn verify_title(
|
||||
bytes_bad_in_main_title: 0,
|
||||
main_title_duration_secs: Some(title.duration_secs),
|
||||
main_title_size_bytes: Some(total_sectors * 2048),
|
||||
located: Default::default(),
|
||||
};
|
||||
if !cb.report(&pp) {
|
||||
break 'outer;
|
||||
|
||||
Reference in New Issue
Block a user