From 63a2e099a6a26fa22705e77eeb098dd64662a284 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 08:48:55 +0000 Subject: [PATCH 01/19] refactor(hevc): split slicedec functions (S3776) Co-Authored-By: Claude Sonnet 5.5 --- crates/hevc/src/slicedec.rs | 254 +++++++++++++++++++----------------- 1 file changed, 137 insertions(+), 117 deletions(-) diff --git a/crates/hevc/src/slicedec.rs b/crates/hevc/src/slicedec.rs index 48afc3b5..f9cbe9d7 100644 --- a/crates/hevc/src/slicedec.rs +++ b/crates/hevc/src/slicedec.rs @@ -397,6 +397,37 @@ impl<'a> SliceDecoder<'a> { d.decode_data() } + /// Context initialisation / synchronisation at the start of a CTB (9.3.1, 9.3.2.1). + fn init_ctb_contexts(&mut self, layout: &Layout, ts: u32, rs: u32, wctb: u32, first: bool) -> Result<()> { + let ctb = 1i32 << self.sps.log2_ctb; + let first_in_tile = ts == 0 || layout.tile_id[ts as usize] != layout.tile_id[ts as usize - 1]; + let wpp_row_start = self.pps.entropy_coding_sync + && (rs.is_multiple_of(wctb) || layout.tile_id[ts as usize] != layout.tile_id[layout.rs_to_ts[rs as usize - 1] as usize]); + if first_in_tile { + if !first { + init_contexts(&mut self.c.ctx, self.slice_qp, self.init_type); + } + self.last_qp = self.slice_qp; + } else if wpp_row_start { + let (x0, y0) = (self.ctb_x, self.ctb_y); + let avail = self.pic.zavail(x0, y0, x0 + ctb, y0 - ctb, self.slice_addr); + match (&self.pic.wpp_ctx, avail) { + (Some(saved), true) => self.c.ctx = **saved, + _ => init_contexts(&mut self.c.ctx, self.slice_qp, self.init_type), + } + self.last_qp = self.slice_qp; + } else if first && self.sh.dependent { + match &self.pic.ds_ctx { + Some((saved, qp)) => { + self.c.ctx = **saved; + self.last_qp = *qp; + } + None => return invalid("dependent slice segment without stored contexts"), + } + } + Ok(()) + } + fn decode_data(&mut self) -> Result<()> { let layout = self.pic.layout.clone(); let wctb = self.pic.wctb as u32; @@ -412,32 +443,7 @@ impl<'a> SliceDecoder<'a> { self.ctb_y = ry * ctb; self.pic.ctb_slice[rs as usize] = self.slice_idx; self.pic.ctb_addr[rs as usize] = self.slice_addr; - // context initialisation / synchronisation (9.3.1, 9.3.2.1) - let first_in_tile = ts == 0 || layout.tile_id[ts as usize] != layout.tile_id[ts as usize - 1]; - let wpp_row_start = self.pps.entropy_coding_sync - && (rs.is_multiple_of(wctb) || layout.tile_id[ts as usize] != layout.tile_id[layout.rs_to_ts[rs as usize - 1] as usize]); - if first_in_tile { - if !first { - init_contexts(&mut self.c.ctx, self.slice_qp, self.init_type); - } - self.last_qp = self.slice_qp; - } else if wpp_row_start { - let (x0, y0) = (self.ctb_x, self.ctb_y); - let avail = self.pic.zavail(x0, y0, x0 + ctb, y0 - ctb, self.slice_addr); - match (&self.pic.wpp_ctx, avail) { - (Some(saved), true) => self.c.ctx = **saved, - _ => init_contexts(&mut self.c.ctx, self.slice_qp, self.init_type), - } - self.last_qp = self.slice_qp; - } else if first && self.sh.dependent { - match &self.pic.ds_ctx { - Some((saved, qp)) => { - self.c.ctx = **saved; - self.last_qp = *qp; - } - None => return invalid("dependent slice segment without stored contexts"), - } - } + self.init_ctb_contexts(&layout, ts, rs, wctb, first)?; first = false; if self.sh.sao_luma || self.sh.sao_chroma { self.parse_sao(rx, ry, rs)?; @@ -506,56 +512,60 @@ impl<'a> SliceDecoder<'a> { let mut p = SaoParams::default(); for c in 0..3 { let enabled = if c == 0 { self.sh.sao_luma } else { self.sh.sao_chroma }; - if !enabled { - continue; - } - if c == 2 { - p.type_idx[2] = p.type_idx[1]; - p.eo_class[2] = p.eo_class[1]; - } else { - p.type_idx[c] = if self.dd(SAO_TYPE) == 0 { - 0 - } else if self.bypass() == 0 { - 1 - } else { - 2 - }; - } - if p.type_idx[c] == 0 { - continue; - } - let bd = if c == 0 { self.pic.bd_y } else { self.pic.bd_c }; - let cmax = (1u32 << (bd.min(10) - 5)) - 1; - let mut abs = [0i16; 4]; - for a in abs.iter_mut() { - let mut v = 0; - while v < cmax && self.bypass() == 1 { - v += 1; - } - *a = v as i16; - } - if p.type_idx[c] == 1 { - for a in abs.iter_mut() { - if *a != 0 && self.bypass() == 1 { - *a = -*a; - } - } - p.band_pos[c] = self.bypass_bits(5) as u8; - p.offsets[c] = abs; - } else { - p.offsets[c] = [abs[0], abs[1], -abs[2], -abs[3]]; - if c == 0 { - p.eo_class[0] = self.bypass_bits(2) as u8; - } - if c == 1 { - p.eo_class[1] = self.bypass_bits(2) as u8; - } + if enabled { + self.parse_sao_component(&mut p, c); } } self.pic.sao[rs as usize] = p; Ok(()) } + /// SAO syntax of one enabled colour component `c` (7.3.8.3). + fn parse_sao_component(&mut self, p: &mut SaoParams, c: usize) { + if c == 2 { + p.type_idx[2] = p.type_idx[1]; + p.eo_class[2] = p.eo_class[1]; + } else { + p.type_idx[c] = if self.dd(SAO_TYPE) == 0 { + 0 + } else if self.bypass() == 0 { + 1 + } else { + 2 + }; + } + if p.type_idx[c] == 0 { + return; + } + let bd = if c == 0 { self.pic.bd_y } else { self.pic.bd_c }; + let cmax = (1u32 << (bd.min(10) - 5)) - 1; + let mut abs = [0i16; 4]; + for a in abs.iter_mut() { + let mut v = 0; + while v < cmax && self.bypass() == 1 { + v += 1; + } + *a = v as i16; + } + if p.type_idx[c] == 1 { + for a in abs.iter_mut() { + if *a != 0 && self.bypass() == 1 { + *a = -*a; + } + } + p.band_pos[c] = self.bypass_bits(5) as u8; + p.offsets[c] = abs; + } else { + p.offsets[c] = [abs[0], abs[1], -abs[2], -abs[3]]; + if c == 0 { + p.eo_class[0] = self.bypass_bits(2) as u8; + } + if c == 1 { + p.eo_class[1] = self.bypass_bits(2) as u8; + } + } + } + fn coding_quadtree(&mut self, x0: i32, y0: i32, log2: u32, depth: u32) -> Result<()> { let size = 1i32 << log2; let (w, h) = (self.pic.width as i32, self.pic.height as i32); @@ -826,6 +836,34 @@ impl<'a> SliceDecoder<'a> { self.c.init_engine() } + /// Most probable luma intra modes of the block at (xp, yp) (8.4.2). + fn mpm_list(&self, xp: i32, yp: i32) -> [u32; 3] { + let cand_a = if self.pic.zavail(xp, yp, xp - 1, yp, self.slice_addr) { + let b = self.pic.blk_at(xp - 1, yp); + if b.flags & F_INTRA != 0 { b.ipm as u32 } else { 1 } + } else { + 1 + }; + let cand_b = if self.pic.zavail(xp, yp, xp, yp - 1, self.slice_addr) && yp > ((yp >> self.sps.log2_ctb) << self.sps.log2_ctb) { + let b = self.pic.blk_at(xp, yp - 1); + if b.flags & F_INTRA != 0 { b.ipm as u32 } else { 1 } + } else { + 1 + }; + if cand_a == cand_b { + if cand_a < 2 { [0, 1, 26] } else { [cand_a, 2 + ((cand_a + 29) % 32), 2 + ((cand_a - 2 + 1) % 32)] } + } else { + let c = if cand_a != 0 && cand_b != 0 { + 0 + } else if cand_a != 1 && cand_b != 1 { + 1 + } else { + 26 + }; + [cand_a, cand_b, c] + } + } + fn parse_intra_modes(&mut self, x0: i32, y0: i32, n: i32, part: PartMode) -> Result<()> { let nb = if part == PartMode::PNxN { 2 } else { 1 }; let pb = n / nb; @@ -838,31 +876,7 @@ impl<'a> SliceDecoder<'a> { for i in 0..nb { let k = (j * nb + i) as usize; let (xp, yp) = (x0 + i * pb, y0 + j * pb); - // candidate modes (8.4.2) - let cand_a = if self.pic.zavail(xp, yp, xp - 1, yp, self.slice_addr) { - let b = self.pic.blk_at(xp - 1, yp); - if b.flags & F_INTRA != 0 { b.ipm as u32 } else { 1 } - } else { - 1 - }; - let cand_b = if self.pic.zavail(xp, yp, xp, yp - 1, self.slice_addr) && yp > ((yp >> self.sps.log2_ctb) << self.sps.log2_ctb) { - let b = self.pic.blk_at(xp, yp - 1); - if b.flags & F_INTRA != 0 { b.ipm as u32 } else { 1 } - } else { - 1 - }; - let mut list = if cand_a == cand_b { - if cand_a < 2 { [0, 1, 26] } else { [cand_a, 2 + ((cand_a + 29) % 32), 2 + ((cand_a - 2 + 1) % 32)] } - } else { - let c = if cand_a != 0 && cand_b != 0 { - 0 - } else if cand_a != 1 && cand_b != 1 { - 1 - } else { - 26 - }; - [cand_a, cand_b, c] - }; + let mut list = self.mpm_list(xp, yp); let mode = if prev[k] { let idx = if self.bypass() == 0 { 0 @@ -938,33 +952,39 @@ impl<'a> SliceDecoder<'a> { self.transform_unit(x0, y0, xb, yb, log2, blk_idx, cbf_luma, cbf_c) } + /// cu_qp_delta_abs / sign and QP update (7.3.8.14). + fn parse_cu_qp_delta(&mut self) -> Result<()> { + // cu_qp_delta_abs: prefix TU(5) with ctx 0,1,1,1,1, suffix EG0 + let mut v = 0; + while v < 5 && self.dd(CU_QP_DELTA_ABS + if v == 0 { 0 } else { 1 }) == 1 { + v += 1; + } + if v == 5 { + let mut k = 0; + while self.bypass() == 1 { + v += 1 << k; + k += 1; + ensure!(k < 32, "invalid cu_qp_delta_abs"); + } + v += self.bypass_bits(k) as i32; + } + if v != 0 && self.bypass() == 1 { + v = -v; + } + let off = 6 * (self.pic.bd_y as i32 - 8); + ensure!(v >= -(26 + off / 2) && v <= 25 + off / 2, "CuQpDeltaVal out of range"); + self.is_cu_qp_delta_coded = true; + self.cu_qp_delta = v; + self.update_qp(); + Ok(()) + } + fn transform_unit(&mut self, x0: i32, y0: i32, xb: i32, yb: i32, log2: u32, blk_idx: u32, cbf_luma: bool, cbf_c: [bool; 2]) -> Result<()> { let n = 1i32 << log2; let chroma_here = log2 > 2 || blk_idx == 3; let cbf_chroma = cbf_c[0] || cbf_c[1]; if (cbf_luma || cbf_chroma) && self.pps.cu_qp_delta_enabled && !self.is_cu_qp_delta_coded { - // cu_qp_delta_abs: prefix TU(5) with ctx 0,1,1,1,1, suffix EG0 - let mut v = 0; - while v < 5 && self.dd(CU_QP_DELTA_ABS + if v == 0 { 0 } else { 1 }) == 1 { - v += 1; - } - if v == 5 { - let mut k = 0; - while self.bypass() == 1 { - v += 1 << k; - k += 1; - ensure!(k < 32, "invalid cu_qp_delta_abs"); - } - v += self.bypass_bits(k) as i32; - } - if v != 0 && self.bypass() == 1 { - v = -v; - } - let off = 6 * (self.pic.bd_y as i32 - 8); - ensure!(v >= -(26 + off / 2) && v <= 25 + off / 2, "CuQpDeltaVal out of range"); - self.is_cu_qp_delta_coded = true; - self.cu_qp_delta = v; - self.update_qp(); + self.parse_cu_qp_delta()?; } // luma if self.cu_intra { From d3185838671d845c988699cc0b49a5e9233f97d3 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 16:01:43 -0400 Subject: [PATCH 02/19] fix(render): optimize table styles, chart lookups, line trimming, and canvas limits (#1288) * fix(render): optimize table styles, chart lookups, line trimming, and canvas limits * fix(render): give outer-shadow passes their own traversal budget A shadowed group's silhouette pass shared the 100,000-visit limit with normal drawing, so a group with many children could use it up before anything visible was drawn. * perf(render): resolve a table's style once per draw --- crates/render/src/chartex.rs | 47 +++++++++++++++++++------ crates/render/src/images.rs | 14 ++++++-- crates/render/src/lib.rs | 62 +++++++++++++++++++++++++++++---- crates/render/src/table.rs | 42 ++++++++++++++-------- crates/render/src/tests_wild.rs | 9 +++++ crates/render/src/warp.rs | 17 +++++++++ 6 files changed, 158 insertions(+), 33 deletions(-) diff --git a/crates/render/src/chartex.rs b/crates/render/src/chartex.rs index b0f3fffd..55350d5f 100644 --- a/crates/render/src/chartex.rs +++ b/crates/render/src/chartex.rs @@ -170,19 +170,37 @@ pub fn squarify(vals: &[f64], r: Rect) -> Vec { let mut i = 0; while i < order.len() { let short = free.width().min(free.height()); - let worst = |row: &[usize]| { - let areas: Vec = row.iter().map(|&k| vals[k] * scale).collect(); - let sum: f64 = areas.iter().sum(); - let (mx, mn) = (areas.iter().copied().fold(0.0, f64::max), areas.iter().copied().fold(f64::MAX, f64::min)); - ((short * short * mx) / (sum * sum)).max((sum * sum) / (short * short * mn)) - }; + if !short.is_finite() || short <= 1e-9 { + break; + } + let s2 = short * short; + let a0 = vals[order[i]] * scale; + let mut cur_sum = a0; + let mut cur_max = a0; + let mut cur_min = a0; + let mut cur_worst = ((s2 * cur_max) / (cur_sum * cur_sum)).max((cur_sum * cur_sum) / (s2 * cur_min)); let mut end = i + 1; - while end < order.len() && worst(&order[i..=end]) <= worst(&order[i..end]) { - end += 1; + while end < order.len() { + let next_a = vals[order[end]] * scale; + let cand_sum = cur_sum + next_a; + let cand_max = cur_max.max(next_a); + let cand_min = cur_min.min(next_a); + let cand_worst = ((s2 * cand_max) / (cand_sum * cand_sum)).max((cand_sum * cand_sum) / (s2 * cand_min)); + if cand_worst.is_finite() && cand_worst <= cur_worst { + cur_sum = cand_sum; + cur_max = cand_max; + cur_min = cand_min; + cur_worst = cand_worst; + end += 1; + } else { + break; + } } let row = &order[i..end]; - let sum: f64 = row.iter().map(|&k| vals[k] * scale).sum(); - let thick = sum / short; + let thick = cur_sum / short; + if !thick.is_finite() || thick <= 0.0 { + break; + } let mut at = 0.0; for &k in row { let len = vals[k] * scale / thick; @@ -570,4 +588,13 @@ mod tests { assert_eq!(c.waterfall_bars(), [(0.0, 100.0), (100.0, 140.0), (140.0, 115.0), (0.0, 115.0)]); assert_eq!(c.value_range(), (0.0, 140.0)); } + + #[test] + fn squarify_handles_skewed_and_exhausted_rect() { + let vals = vec![1e12, 1e-6, 1e-6, 1e-6]; + let r = Rect::new(0.0, 0.0, 100.0, 100.0); + let tiles = squarify(&vals, r); + assert_eq!(tiles.len(), vals.len()); + assert!(tiles[0].area() > 0.0); + } } diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index 135603b6..0e86be8c 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -47,6 +47,7 @@ const GIF_SIDE: u32 = 1024; const GIF_BUDGET: usize = 64 << 20; const GIF_MAX_FRAMES: usize = 1000; const GIF_MAX_CANVAS: u64 = 4096 * 4096; +const GIF_MAX_TOTAL_CANVAS_PIXELS: u64 = 256 * 1024 * 1024; const GIF_CACHE_BUDGET: usize = 192 << 20; /// The frames of an animated GIF and when each one ends, in seconds from the start of a loop. @@ -394,13 +395,22 @@ fn decode_anim(bytes: &[u8], adj: &PictureAdjust) -> Option { let k = (GIF_SIDE as f64 / w.max(h) as f64).min(1.0); let (nw, nh) = (((w as f64 * k).round() as u32).max(1), ((h as f64 * k).round() as u32).max(1)); let size = nw as usize * nh as usize * 4; + let canvas_pixels = (w as u64) * (h as u64); + let mut total_canvas_pixels: u64 = 0; let (mut frames, mut ends, mut t) = (Vec::new(), Vec::new(), 0.0); - for f in dec.into_frames() { - let Ok(f) = f else { break }; + let mut iter = dec.into_frames(); + loop { if frames.len() >= GIF_MAX_FRAMES || (frames.len() + 1) * size > GIF_BUDGET { log::info!("animated GIF: keeping its first {} frames", frames.len()); break; } + if total_canvas_pixels.saturating_add(canvas_pixels) > GIF_MAX_TOTAL_CANVAS_PIXELS { + log::info!("animated GIF: canvas pixel limit reached"); + break; + } + let Some(f) = iter.next() else { break }; + let Ok(f) = f else { break }; + total_canvas_pixels = total_canvas_pixels.saturating_add(canvas_pixels); let (n, d) = f.delay().numer_denom_ms(); let ms = if d == 0 { 0.0 } else { n as f64 / d as f64 }; // Like browsers: frames of 10 ms or less show for 100 ms. diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index f4435899..1e7c3dca 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -321,6 +321,13 @@ pub struct Renderer { group_fill: Option<(Fill, Affine, Rect)>, /// Their aligned outlines, kept from frame to frame. morphs: morph_path::Cache, + /// Whether we are currently drawing an outer-shadow silhouette pass for an ancestor group. + in_shadow_pass: bool, + /// Cumulative shape traversal counter to prevent nested group amplification DoS. + render_work: usize, + /// Visits spent on outer-shadow silhouette passes, kept apart so they cannot use up the budget + /// of the normal drawing that follows. + shadow_work: usize, } impl Default for Renderer { @@ -333,6 +340,9 @@ impl Default for Renderer { view: Affine::IDENTITY, group_fill: None, morphs: morph_path::Cache::default(), + in_shadow_pass: false, + render_work: 0, + shadow_work: 0, } } } @@ -678,6 +688,9 @@ impl Renderer { } fn slide(&mut self, pres: &Presentation, slide: &Slide, index: usize, opts: &RenderOpts) -> Image { + self.render_work = 0; + self.shadow_work = 0; + self.in_shadow_pass = false; let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, slide)); let f = Frame { pres, opts, fields: Self::fields(pres, index, opts) }; if let Some(rctx) = Ctx::for_slide(pres, slide) { @@ -690,6 +703,9 @@ impl Renderer { } fn slide_part(&mut self, pres: &Presentation, slide: &Slide, index: usize, part: SlidePart, opts: &RenderOpts) -> Image { + self.render_work = 0; + self.shadow_work = 0; + self.in_shadow_pass = false; let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, slide)); let f = Frame { pres, opts, fields: Self::fields(pres, index, opts) }; if let Some(rctx) = Ctx::for_slide(pres, slide) { @@ -721,6 +737,9 @@ impl Renderer { opts: &RenderOpts, ) -> Image { let mix = if mix.is_finite() { mix.clamp(0.0, 1.0) } else { 1.0 }; + self.render_work = 0; + self.shadow_work = 0; + self.in_shadow_pass = false; let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, old.0) || needs_filters(pres, new.0)); let (Some(co), Some(cn)) = (Ctx::for_slide(pres, old.0), Ctx::for_slide(pres, new.0)) else { return self.finish(ctx, w, h) }; // Backdrop graphics never take shape states: their ids may clash with the frame's shapes. @@ -768,6 +787,9 @@ impl Renderer { } fn layout(&mut self, pres: &Presentation, mi: usize, li: Option, opts: &RenderOpts) -> Image { + self.render_work = 0; + self.shadow_work = 0; + self.in_shadow_pass = false; let (w, h) = output_size(pres, opts); let mut ctx = RenderContext::new_with(w, h, vello_cpu::RenderSettings { num_threads: 0, ..Default::default() }); let view = Affine::translate((opts.offset.0, opts.offset.1)) * Affine::scale(opts.scale); @@ -896,6 +918,11 @@ impl Renderer { if depth > 32 || (s.hidden && !f.opts.show_hidden) || f.opts.skip.contains(&s.id) { return; } + let work = if self.in_shadow_pass { &mut self.shadow_work } else { &mut self.render_work }; + *work = work.saturating_add(1); + if *work > 100_000 { + return; + } // Footer placeholders on layouts/masters show only on slides that include them. let st = f.opts.state.map(|g| g(s.id)).unwrap_or_default(); if !st.visible || st.opacity <= 0.001 { @@ -936,7 +963,9 @@ impl Renderer { // A group's outer shadow is cast by everything in it, as PowerPoint draws it: the // children drawn once more under the shadow's offset, scale and skew, recoloured to // the shadow colour and blurred, below the group. - if let Some(sh) = s.effects.as_ref().and_then(|e| e.outer_shadow.as_ref()) { + if !self.in_shadow_pass + && let Some(sh) = s.effects.as_ref().and_then(|e| e.outer_shadow.as_ref()) + { let c = rctx.color(&sh.color, None); let k = scale_of(m); let d = Vec2::new(sh.dir.to_radians().cos(), sh.dir.to_radians().sin()) * sh.dist; @@ -948,9 +977,11 @@ impl Renderer { let shift = pre * m * own * m.inverse(); ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, None, Some((c.a as f32) / 255.0), None, Some(blur(sh.blur / 3.0 * k))); + self.in_shadow_pass = true; for ch in children { self.shape(ctx, f, rctx, ch, shift * gm, depth + 1); } + self.in_shadow_pass = false; ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcIn)), None, None, None); ctx.set_paint(color(c.with_alpha(255), 1.0)); @@ -1958,16 +1989,21 @@ fn trim_ends(path: &BezPath, start: f64, end: f64) -> BezPath { && start > 0.0 { let mut left = start; - while let Some(seg) = segs.first().copied() { + let mut drop_count = 0; + for (i, seg) in segs.iter().enumerate() { let l = seg.arclen(ACC); if l > left { - if let Some(s0) = segs.first_mut() { - *s0 = seg.subsegment(seg.inv_arclen(left, ACC)..1.0); - } + let partial = seg.subsegment(seg.inv_arclen(left, ACC)..1.0); + segs.drain(..i); + segs[0] = partial; + drop_count = 0; break; } left -= l; - segs.remove(0); + drop_count = i + 1; + } + if drop_count > 0 { + segs.drain(..drop_count); } } if let Some((segs, false)) = subs.last_mut() @@ -2276,6 +2312,20 @@ pub(crate) fn fill_deco(ctx: &mut RenderContext, d: &deckcraft_text::Deco) { /// The line of paragraph `para` holding a glyph on baseline `y` (the nearest baseline). fn line_at(l: &deckcraft_text::TextLayout, para: usize, y: f64) -> usize { + if let Some(&(first, count)) = l.para_lines.get(para) { + if count == 0 { + return 0; + } + let end = (first + count).min(l.lines.len()); + if first < end { + return l.lines[first..end] + .iter() + .enumerate() + .min_by(|a, b| (a.1.baseline - y).abs().total_cmp(&(b.1.baseline - y).abs())) + .map(|(i, _)| first + i) + .unwrap_or(0); + } + } l.lines .iter() .enumerate() diff --git a/crates/render/src/table.rs b/crates/render/src/table.rs index ce6f9a86..9c720628 100644 --- a/crates/render/src/table.rs +++ b/crates/render/src/table.rs @@ -138,8 +138,17 @@ fn border_look(ctx: &Ctx, b: &TableBorder) -> (Rgba, f64) { } } +pub fn resolve_style<'a>(ctx: &'a Ctx, t: &Table) -> Option<&'a TableStyleDef> { + ctx.pres.table_styles.iter().find(|d| d.id.eq_ignore_ascii_case(&t.style)) +} + +#[allow(dead_code)] pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook { - if let Some(def) = ctx.pres.table_styles.iter().find(|d| d.id.eq_ignore_ascii_case(&t.style)) { + look_with_def(ctx, resolve_style(ctx, t), t, r, c) +} + +pub fn look_with_def(ctx: &Ctx, def: Option<&TableStyleDef>, t: &Table, r: usize, c: usize) -> CellLook { + if let Some(def) = def { return look_def(ctx, def, t, r, c); } let a = accent(ctx, &t.style); @@ -242,7 +251,11 @@ fn edges(sizes: &[f64]) -> Vec { /// A cell's text as drawn: its margins, anchor and direction, plus the table style's text colour and bold. pub fn cell_body(rctx: &Ctx, t: &Table, r: usize, c: usize, cell: &Cell) -> TextBody { - let lk = look(rctx, t, r, c); + cell_body_with_def(rctx, resolve_style(rctx, t), t, r, c, cell) +} + +pub fn cell_body_with_def(rctx: &Ctx, def: Option<&TableStyleDef>, t: &Table, r: usize, c: usize, cell: &Cell) -> TextBody { + let lk = look_with_def(rctx, def, t, r, c); let mut body = cell.text.clone(); let [ml, mr, mt, mb] = cell.margins.unwrap_or([7.2, 7.2, 3.6, 3.6]); body.body.inset_l = Some(ml); @@ -272,6 +285,11 @@ pub fn cell_body(rctx: &Ctx, t: &Table, r: usize, c: usize, cell: &Cell) -> Text /// text of each of its cells fits, as in PowerPoint. Files from pandoc or python-pptx store 0. /// Cells spanning several rows and vertical text keep the stored height. pub fn row_heights(rctx: &Ctx, t: &Table, fields: &dyn Fields) -> Vec { + row_heights_with_def(rctx, resolve_style(rctx, t), t, fields) +} + +/// [`row_heights`] with the table's style already resolved, for a caller that needs it too. +fn row_heights_with_def(rctx: &Ctx, style_def: Option<&TableStyleDef>, t: &Table, fields: &dyn Fields) -> Vec { let xs = edges(&t.cols); let mut out = Vec::with_capacity(t.rows.len()); for (ri, row) in t.rows.iter().enumerate() { @@ -282,7 +300,7 @@ pub fn row_heights(rctx: &Ctx, t: &Table, fields: &dyn Fields) -> Vec { } let c1 = ci.saturating_add(cell.grid_span.max(1) as usize).min(t.cols.len()); let (Some(x0), Some(x1)) = (xs.get(ci), xs.get(c1)) else { continue }; - let body = cell_body(rctx, t, ri, ci, cell); + let body = cell_body_with_def(rctx, style_def, t, ri, ci, cell); let tmp = Shape::table_cell(body.clone()); let need = deckcraft_text::fit_height(rctx, &tmp, &body, Rect::new(*x0, 0.0, *x1, h.max(1.0)), fields); if need.is_finite() { @@ -347,16 +365,10 @@ pub fn draw( editing: bool, ) -> Option { let _ = pres; - let rects = cell_rects(t, &row_heights(rctx, t, fields)); + let style_def = resolve_style(rctx, t); + let rects = cell_rects(t, &row_heights_with_def(rctx, style_def, t, fields)); let covered = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b)); - // Table backgrounds behind every cell: a file-defined style's, then the table's own fill. - let style_bg = rctx - .pres - .table_styles - .iter() - .find(|d| d.id.eq_ignore_ascii_case(&t.style)) - .and_then(|d| d.background.as_ref()) - .and_then(|b| style_fill(rctx, b)); + let style_bg = style_def.and_then(|d| d.background.as_ref()).and_then(|b| style_fill(rctx, b)); let own_bg = t.background.clone().map(|f| (f, None)); if let Some(all) = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b)) { for (bg, ph) in style_bg.into_iter().chain(own_bg) { @@ -366,7 +378,7 @@ pub fn draw( } for (r, c, rect) in &rects { let Some(cell) = t.cell(*r, *c) else { continue }; - let lk = look(rctx, t, *r, *c); + let lk = look_with_def(rctx, style_def, t, *r, *c); ctx.set_transform(m); match &cell.fill { Some(f) => { @@ -399,7 +411,7 @@ pub fn draw( // Borders: explicit cell borders, else the style's grid. for (r, c, rect) in &rects { let Some(cell) = t.cell(*r, *c) else { continue }; - let lk = look(rctx, t, *r, *c); + let lk = look_with_def(rctx, style_def, t, *r, *c); let edges = [ (rect.x0, rect.y0, rect.x0, rect.y1), (rect.x1, rect.y0, rect.x1, rect.y1), @@ -439,7 +451,7 @@ pub fn draw( if (cell.text.is_empty() && !editing) || text_off_target(cell, *rect, m, (ctx.width(), ctx.height())) { continue; } - let body = cell_body(rctx, t, *r, *c, cell); + let body = cell_body_with_def(rctx, style_def, t, *r, *c, cell); let tmp = Shape::table_cell(body.clone()); #[cfg(test)] TEXT_CELLS_DRAWN.with(|n| n.set(n.get() + 1)); diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index fec3e9e1..fb561346 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -164,6 +164,15 @@ fn trimming_a_path_leaves_its_ends_off_by_arc_length() { assert!(trim_ends(&p, 60.0, 50.0).elements().is_empty()); let r = Rect::new(0.0, 0.0, 10.0, 10.0).to_path(0.1); assert_eq!(trim_ends(&r, 3.0, 3.0).elements().len(), r.elements().len()); + + // Zero-length leading segments trimmed correctly without quadratic copy. + let mut p0 = BezPath::new(); + p0.move_to((0.0, 0.0)); + p0.line_to((0.0, 0.0)); + p0.line_to((0.0, 0.0)); + p0.line_to((50.0, 0.0)); + let t0 = trim_ends(&p0, 10.0, 0.0); + assert_eq!(t0.elements().first(), Some(&PathEl::MoveTo(Point::new(10.0, 0.0)))); } fn radial(focus: [f64; 4]) -> Shape { diff --git a/crates/render/src/warp.rs b/crates/render/src/warp.rs index e14340e8..58caa2ea 100644 --- a/crates/render/src/warp.rs +++ b/crates/render/src/warp.rs @@ -16,6 +16,23 @@ const MAX_POINTS: usize = 40_000; /// The line of `l` a glyph of paragraph `para` on baseline `y` belongs to. fn line_of(l: &TextLayout, para: usize, y: f64) -> Option { + if let Some(&(first, count)) = l.para_lines.get(para) { + if count == 0 { + return None; + } + let end = (first + count).min(l.lines.len()); + if first < end { + let mut best: Option<(usize, f64)> = None; + for (rel_i, li) in l.lines[first..end].iter().enumerate() { + let inside = y >= li.top - 0.01 && y <= li.bottom + 0.01; + let d = if inside { 0.0 } else { (li.baseline - y).abs() }; + if best.is_none_or(|(_, bd)| d < bd) { + best = Some((first + rel_i, d)); + } + } + return best.map(|(i, _)| i); + } + } let mut best: Option<(usize, f64)> = None; for (i, li) in l.lines.iter().enumerate().filter(|(_, li)| li.para == para) { let inside = y >= li.top - 0.01 && y <= li.bottom + 0.01; From 51eef9927fa949e66b5966e0a06efe02ac8a0769 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 17:37:32 -0400 Subject: [PATCH 03/19] build: pin craft-update to craft-libs main (495e597) (#1386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up the update prompt fix that draws its close mark and version pill without glyphs a font may lack, so the prompt no longer takes the app down when a bundled face has no ✕ or →. Part of tracking issue storytold/craft-libs#319 🤖 Generated with Codebuff Co-authored-by: Codebuff --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ca4ec26..29b5df18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,7 +831,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=db19c373c8d59a545972ca340987aecfb3611a4b#db19c373c8d59a545972ca340987aecfb3611a4b" +source = "git+https://github.com/storytold/craft-libs?rev=495e5971bc024aa96e6ed318cb6e7cd3a0103184#495e5971bc024aa96e6ed318cb6e7cd3a0103184" dependencies = [ "chrono", "dunce", diff --git a/Cargo.toml b/Cargo.toml index 21f79187..fb54bd62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ vello_common = { version = "0.2", default-features = false } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] } craft-media = { git = "https://github.com/storytold/craft-libs", rev = "f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4", version = "=0.1.0", default-features = false } craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "db19c373c8d59a545972ca340987aecfb3611a4b", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184", features = ["egui"] } i_overlay = "9" quick-xml = "0.41" zip = { version = "3", default-features = false, features = ["deflate"] } From dbc5e164dd1e0038e9a7d07eb6c52ac56fac2a47 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 17:50:39 -0400 Subject: [PATCH 04/19] build: pin craft-update to craft-libs main (495e597) (#1388) Picks up the startup prompt without the glyphs a font may lack (storytold/craft-libs#343, #344): the close mark is painted and the version pill is ASCII, so apps that panic on a missing glyph no longer take the prompt down. Part of tracking issue storytold/craft-libs#319 --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29b5df18..222379b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1593,7 +1593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2847,7 +2847,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3863,7 +3863,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3920,7 +3920,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4544,7 +4544,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4810,7 +4810,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5461,7 +5461,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] From 13ed8889da1ddd336da50e8f981a36d7236d9295 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 18:18:36 -0400 Subject: [PATCH 05/19] refactor(av1): use method path instead of closure (S1612) (#1269) --- crates/av1/src/transform.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/av1/src/transform.rs b/crates/av1/src/transform.rs index afa649a4..40b1d763 100644 --- a/crates/av1/src/transform.rs +++ b/crates/av1/src/transform.rs @@ -104,7 +104,7 @@ impl Lane for [i32; L] { } #[inline(always)] fn neg(self) -> Self { - self.map(|v| v.wrapping_neg()) + self.map(i32::wrapping_neg) } #[inline(always)] fn mul_round12(self, m: i32) -> Self { From 1bee1857d61741e2777ad2e853996e2193dcb461 Mon Sep 17 00:00:00 2001 From: skylar Date: Sun, 25 Oct 2026 15:48:30 -0600 Subject: [PATCH 06/19] chore(deps): bump serde_json, arboard, winresource, i_overlay --- Cargo.lock | 120 ++++++++++------------------------------------------- 1 file changed, 23 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 222379b0..0986dafc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "arboard" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917" dependencies = [ "clipboard-win", "image", @@ -222,7 +222,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.52.0", "wl-clipboard-rs", "x11rb", ] @@ -1593,7 +1593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -2181,15 +2181,15 @@ dependencies = [ [[package]] name = "i_key_sort" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c6c58d0c60705e66264ce0f788a69a2f21472aeb8188559e7c8c619dbdc10fa" +checksum = "14269ee362009550495e20650ff3c2bca14a8147fe251e216760381ffed90b6f" [[package]] name = "i_overlay" -version = "9.0.0" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a518dce2d7cf7e3758159747e7175c8b86ccdcd8d333dfcb7dbeb3eac2e324fc" +checksum = "913fd692c02408adc76cead636f8e7e07c705f8b995a8b54b454b995e8e197d5" dependencies = [ "i_float", "i_key_sort", @@ -2208,9 +2208,9 @@ dependencies = [ [[package]] name = "i_tree" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9d4a992a9fe83130f41ceacceac3bb116a4355dfc9c8d6ecea4f1e4b4c6caf" +checksum = "048af31de955890c62a8506fec511335a5089e91e1c516537add16f96c507d84" [[package]] name = "iana-time-zone" @@ -2847,7 +2847,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3320,7 +3320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -3863,7 +3863,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -3920,7 +3920,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4077,9 +4077,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -4541,10 +4541,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.5", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4810,7 +4810,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5461,7 +5461,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -5621,15 +5621,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -5663,30 +5654,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -5708,12 +5682,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -5726,12 +5694,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -5744,24 +5706,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -5774,12 +5724,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5792,12 +5736,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -5810,12 +5748,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5828,12 +5760,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winit" version = "0.30.13" @@ -5905,9 +5831,9 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.31" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b" dependencies = [ "toml", "version_check", From b6fd616977751d9ac02912af6611516d1b8fa2ad Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 19:21:54 -0400 Subject: [PATCH 07/19] refactor(h264,hevc): reduce parameter counts (S107) (#1271) Group geometry/size/weight parameters into tuples and a PbGeom struct. --- crates/h264/src/picture.rs | 2 +- crates/h264/src/slicedec.rs | 2 +- crates/hevc/src/inter.rs | 13 +++----- crates/hevc/src/mvpred.rs | 57 +++++++++++++++-------------------- crates/hevc/src/picture.rs | 2 +- crates/hevc/src/slicedec.rs | 59 ++++++++++++++++++------------------- 6 files changed, 59 insertions(+), 76 deletions(-) diff --git a/crates/h264/src/picture.rs b/crates/h264/src/picture.rs index d97fa063..39add6a0 100644 --- a/crates/h264/src/picture.rs +++ b/crates/h264/src/picture.rs @@ -250,7 +250,7 @@ impl Frame { /// Copy the w x h window of chroma component `c` at (x0, y0) (clamped) into `out`. #[inline] - pub fn chroma_window(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, out: &mut [u8], os: usize) { + pub fn chroma_window(&self, c: usize, (x0, y0): (i32, i32), (w, h): (usize, usize), out: &mut [u8], os: usize) { let (cw, ch) = (self.cwidth, self.cheight); for r in 0..h { let yy = (y0 + r as i32).clamp(0, ch as i32 - 1) as usize; diff --git a/crates/h264/src/slicedec.rs b/crates/h264/src/slicedec.rs index 19fa0615..99d448e8 100644 --- a/crates/h264/src/slicedec.rs +++ b/crates/h264/src/slicedec.rs @@ -232,7 +232,7 @@ fn predict_block(f: &Frame, mv: [i16; 2], px: usize, py: usize, bw: usize, bh: u let (fx, fy) = ((mv[0] & 7) as u32, (mv[1] & 7) as u32); for (comp, out) in c.into_iter().enumerate() { let mut w = [0u8; 16 * 9]; - f.chroma_window(comp, cx, cy, cw + 1, ch + 1, &mut w, 16); + f.chroma_window(comp, (cx, cy), (cw + 1, ch + 1), &mut w, 16); inter::mc_chroma_win(&w, 16, fx, fy, cw, ch, out, cs); } } diff --git a/crates/hevc/src/inter.rs b/crates/hevc/src/inter.rs index 8d1b6751..20a4f059 100644 --- a/crates/hevc/src/inter.rs +++ b/crates/hevc/src/inter.rs @@ -128,7 +128,7 @@ pub fn mc_chroma(f: &Frame, c: usize, x: i32, y: i32, mv: [i16; 2], w: usize, h: let yi = y + (mv[1] as i32 >> 3); let shift1 = bd.min(12) - 8; let shift3 = 14 - bd; - f.chroma_window(c, xi - 1, yi - 1, w + 3, h + 3, &mut sc.win, WIN); + f.chroma_window(c, (xi - 1, yi - 1), (w + 3, h + 3), &mut sc.win, WIN); interpolate::<4>(&sc.win, &mut sc.tmp, fx, fy, &CHROMA_FILTER[fx], &CHROMA_FILTER[fy], w, h, shift1, shift3, out); } @@ -158,7 +158,7 @@ pub fn put_bi(p0: &[i16], p1: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u1 } /// Explicit weighted prediction, one list (8-265). `o` is the offset already scaled to the bit depth. -pub fn put_weighted_uni(p: &[i16], w: usize, h: usize, bd: u32, log2wd: u32, wt: i32, o: i32, dst: &mut [u16], ds: usize) { +pub fn put_weighted_uni(p: &[i16], (w, h): (usize, usize), bd: u32, (log2wd, wt, o): (u32, i32, i32), dst: &mut [u16], ds: usize) { let max = (1i32 << bd) - 1; for r in 0..h { for c in 0..w { @@ -173,14 +173,9 @@ pub fn put_weighted_uni(p: &[i16], w: usize, h: usize, bd: u32, log2wd: u32, wt: pub fn put_weighted_bi( p0: &[i16], p1: &[i16], - w: usize, - h: usize, + (w, h): (usize, usize), bd: u32, - log2wd: u32, - w0: i32, - w1: i32, - o0: i32, - o1: i32, + (log2wd, w0, w1, o0, o1): (u32, i32, i32, i32, i32), dst: &mut [u16], ds: usize, ) { diff --git a/crates/hevc/src/mvpred.rs b/crates/hevc/src/mvpred.rs index 3f686cbd..49a37ec6 100644 --- a/crates/hevc/src/mvpred.rs +++ b/crates/hevc/src/mvpred.rs @@ -36,9 +36,23 @@ fn same_motion(a: &MvField, b: &MvField) -> bool { true } +/// Coding block and prediction block geometry (6.4.2): coding block (xc, yc, ncb), prediction block (xp, yp, w, h, part_idx). +#[derive(Clone, Copy)] +pub struct PbGeom { + pub xc: i32, + pub yc: i32, + pub ncb: i32, + pub xp: i32, + pub yp: i32, + pub w: i32, + pub h: i32, + pub part_idx: usize, +} + impl<'a> SliceDecoder<'a> { /// Prediction block availability (6.4.2). - fn pb_avail(&self, xc: i32, yc: i32, ncb: i32, xp: i32, yp: i32, w: i32, h: i32, part_idx: usize, xn: i32, yn: i32) -> bool { + fn pb_avail(&self, g: &PbGeom, xn: i32, yn: i32) -> bool { + let PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx } = *g; let same_cb = xc <= xn && yc <= yn && xc + ncb > xn && yc + ncb > yn; let avail = if !same_cb { self.pic.zavail(xp, yp, xn, yn, self.slice_addr) @@ -59,23 +73,13 @@ impl<'a> SliceDecoder<'a> { } /// Merge mode motion (8.5.3.2.2). - pub fn derive_merge( - &self, - xc: i32, - yc: i32, - ncb: i32, - xp0: i32, - yp0: i32, - w0: i32, - h0: i32, - part_idx0: usize, - part: PartMode, - merge_idx: usize, - ) -> MvField { + pub fn derive_merge(&self, g: PbGeom, part: PartMode, merge_idx: usize) -> MvField { + let PbGeom { xc, yc, ncb, xp: xp0, yp: yp0, w: w0, h: h0, part_idx: part_idx0 } = g; let pml = self.pps.log2_parallel_merge_level; let (xp, yp, w, h, part_idx) = if pml > 2 && ncb == 8 { (xc, yc, ncb, ncb, 0) } else { (xp0, yp0, w0, h0, part_idx0) }; + let g = PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }; let same_mer = |xn: i32, yn: i32| (xp >> pml) == (xn >> pml) && (yp >> pml) == (yn >> pml); - let avail = |xn: i32, yn: i32| self.pb_avail(xc, yc, ncb, xp, yp, w, h, part_idx, xn, yn) && !same_mer(xn, yn); + let avail = |xn: i32, yn: i32| self.pb_avail(&g, xn, yn) && !same_mer(xn, yn); let mut list: [MvField; 5] = [MvField::default(); 5]; let mut n = 0; let max = self.sh.max_num_merge_cand as usize; @@ -237,20 +241,8 @@ impl<'a> SliceDecoder<'a> { } /// AMVP predictor (8.5.3.2.6, 8.5.3.2.7). - pub fn derive_amvp( - &self, - xc: i32, - yc: i32, - ncb: i32, - xp: i32, - yp: i32, - w: i32, - h: i32, - part_idx: usize, - l: usize, - ref_idx: usize, - mvp_flag: usize, - ) -> [i16; 2] { + pub fn derive_amvp(&self, g: PbGeom, l: usize, ref_idx: usize, mvp_flag: usize) -> [i16; 2] { + let PbGeom { xp, yp, w, h, .. } = g; let target = &self.refs[l][ref_idx]; let (tpoc, tlt) = (target.poc, target.long_term); let cur_poc = self.pic.poc; @@ -277,10 +269,7 @@ impl<'a> SliceDecoder<'a> { pick.map(|(mv, r)| if r.poc != tpoc && !r.long_term && !tlt { scale_mv(mv, cur_poc - r.poc, cur_poc - tpoc) } else { mv }) }; let pa = [(xp - 1, yp + h), (xp - 1, yp + h - 1)]; - let av_a = [ - self.pb_avail(xc, yc, ncb, xp, yp, w, h, part_idx, pa[0].0, pa[0].1), - self.pb_avail(xc, yc, ncb, xp, yp, w, h, part_idx, pa[1].0, pa[1].1), - ]; + let av_a = [self.pb_avail(&g, pa[0].0, pa[0].1), self.pb_avail(&g, pa[1].0, pa[1].1)]; let is_scaled = av_a[0] || av_a[1]; let mut a = None; for k in 0..2 { @@ -296,7 +285,7 @@ impl<'a> SliceDecoder<'a> { } } let pb = [(xp + w, yp - 1), (xp + w - 1, yp - 1), (xp - 1, yp - 1)]; - let av_b = [0, 1, 2].map(|k| self.pb_avail(xc, yc, ncb, xp, yp, w, h, part_idx, pb[k].0, pb[k].1)); + let av_b = [0, 1, 2].map(|k| self.pb_avail(&g, pb[k].0, pb[k].1)); let mut b = None; for k in 0..3 { if av_b[k] && b.is_none() { diff --git a/crates/hevc/src/picture.rs b/crates/hevc/src/picture.rs index 1267de80..bca74117 100644 --- a/crates/hevc/src/picture.rs +++ b/crates/hevc/src/picture.rs @@ -125,7 +125,7 @@ impl Frame { } /// Copy a w x h window of chroma component `c` (0 = Cb, 1 = Cr) with edge replication. - pub fn chroma_window(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) { + pub fn chroma_window(&self, c: usize, (x0, y0): (i32, i32), (w, h): (usize, usize), out: &mut [i16], os: usize) { let (cw, ch) = (self.cwidth as i32, self.cheight as i32); let sh = self.log2_ctb - 1; let mask = (1i32 << sh) - 1; diff --git a/crates/hevc/src/slicedec.rs b/crates/hevc/src/slicedec.rs index f9cbe9d7..99a7026f 100644 --- a/crates/hevc/src/slicedec.rs +++ b/crates/hevc/src/slicedec.rs @@ -7,6 +7,7 @@ use crate::cabac::{Cabac, Contexts, init_contexts}; use crate::error::{Result, ensure, invalid}; use crate::inter; use crate::intra::{self, Refs}; +use crate::mvpred::PbGeom; use crate::params::{Layout, Pps, Sps}; use crate::picture::{FrameRef, RefPic}; use crate::slice::{SliceHeader, SliceType}; @@ -678,7 +679,7 @@ impl<'a> SliceDecoder<'a> { self.mark_edges(x0, y0, n, n, E_TU_V | E_PU_V, E_TU_H | E_PU_H); if skip { self.cu_intra = false; - self.prediction_unit(x0, y0, n, x0, y0, n, n, 0, PartMode::P2Nx2N, true)?; + self.prediction_unit(PbGeom { xc: x0, yc: y0, ncb: n, xp: x0, yp: y0, w: n, h: n, part_idx: 0 }, PartMode::P2Nx2N, true)?; self.finish_cu(x0, y0, n); return Ok(()); } @@ -707,7 +708,7 @@ impl<'a> SliceDecoder<'a> { } self.parse_intra_modes(x0, y0, n, part)?; self.max_trafo_depth = self.sps.max_th_depth_intra + self.intra_split as u32; - self.transform_tree(x0, y0, x0, y0, log2, 0, 0, [true, true])?; + self.transform_tree((x0, y0), (x0, y0), log2, 0, 0, [true, true])?; } else { self.intra_split = false; let mut merge2nx2n = false; @@ -724,7 +725,8 @@ impl<'a> SliceDecoder<'a> { PartMode::PNxN => &[(0, 0, h, h), (h, 0, h, h), (0, h, h, h), (h, h, h, h)], }; for (i, &(px, py, pw, ph)) in parts.iter().enumerate() { - let merge = self.prediction_unit(x0, y0, n, x0 + px, y0 + py, pw, ph, i, part, false)?; + let merge = + self.prediction_unit(PbGeom { xc: x0, yc: y0, ncb: n, xp: x0 + px, yp: y0 + py, w: pw, h: ph, part_idx: i }, part, false)?; if part == PartMode::P2Nx2N { merge2nx2n = merge; } @@ -738,7 +740,7 @@ impl<'a> SliceDecoder<'a> { let root_cbf = if !merge2nx2n { self.dd(RQT_ROOT_CBF) == 1 } else { true }; if root_cbf { self.max_trafo_depth = self.sps.max_th_depth_inter; - self.transform_tree(x0, y0, x0, y0, log2, 0, 0, [true, true])?; + self.transform_tree((x0, y0), (x0, y0), log2, 0, 0, [true, true])?; } } self.finish_cu(x0, y0, n); @@ -912,7 +914,7 @@ impl<'a> SliceDecoder<'a> { Ok(()) } - fn transform_tree(&mut self, x0: i32, y0: i32, xb: i32, yb: i32, log2: u32, depth: u32, blk_idx: u32, parent_cbf_c: [bool; 2]) -> Result<()> { + fn transform_tree(&mut self, (x0, y0): (i32, i32), base: (i32, i32), log2: u32, depth: u32, blk_idx: u32, parent_cbf_c: [bool; 2]) -> Result<()> { let split = if log2 <= self.sps.log2_max_tb && log2 > self.sps.log2_min_tb && depth < self.max_trafo_depth && !(self.intra_split && depth == 0) { self.dd(SPLIT_TRANSFORM + (5 - log2 as usize)) == 1 @@ -937,10 +939,10 @@ impl<'a> SliceDecoder<'a> { } if split { let h = 1 << (log2 - 1); - self.transform_tree(x0, y0, x0, y0, log2 - 1, depth + 1, 0, cbf_c)?; - self.transform_tree(x0 + h, y0, x0, y0, log2 - 1, depth + 1, 1, cbf_c)?; - self.transform_tree(x0, y0 + h, x0, y0, log2 - 1, depth + 1, 2, cbf_c)?; - self.transform_tree(x0 + h, y0 + h, x0, y0, log2 - 1, depth + 1, 3, cbf_c)?; + self.transform_tree((x0, y0), (x0, y0), log2 - 1, depth + 1, 0, cbf_c)?; + self.transform_tree((x0 + h, y0), (x0, y0), log2 - 1, depth + 1, 1, cbf_c)?; + self.transform_tree((x0, y0 + h), (x0, y0), log2 - 1, depth + 1, 2, cbf_c)?; + self.transform_tree((x0 + h, y0 + h), (x0, y0), log2 - 1, depth + 1, 3, cbf_c)?; return Ok(()); } let cbf_luma = @@ -949,7 +951,7 @@ impl<'a> SliceDecoder<'a> { if depth > 0 { self.mark_edges(x0, y0, n, n, E_TU_V, E_TU_H); } - self.transform_unit(x0, y0, xb, yb, log2, blk_idx, cbf_luma, cbf_c) + self.transform_unit((x0, y0), base, log2, blk_idx, cbf_luma, cbf_c) } /// cu_qp_delta_abs / sign and QP update (7.3.8.14). @@ -979,7 +981,15 @@ impl<'a> SliceDecoder<'a> { Ok(()) } - fn transform_unit(&mut self, x0: i32, y0: i32, xb: i32, yb: i32, log2: u32, blk_idx: u32, cbf_luma: bool, cbf_c: [bool; 2]) -> Result<()> { + fn transform_unit( + &mut self, + (x0, y0): (i32, i32), + (xb, yb): (i32, i32), + log2: u32, + blk_idx: u32, + cbf_luma: bool, + cbf_c: [bool; 2], + ) -> Result<()> { let n = 1i32 << log2; let chroma_here = log2 > 2 || blk_idx == 3; let cbf_chroma = cbf_c[0] || cbf_c[1]; @@ -1284,7 +1294,7 @@ impl<'a> SliceDecoder<'a> { max_y = max_y.max(yc); } } - self.reconstruct(x0, y0, log2, c, qp, ts, max_x, max_y) + self.reconstruct((x0, y0), log2, c, qp, ts, (max_x, max_y)) } fn coeff_abs_level_remaining(&mut self, rice: u32) -> Result { @@ -1303,7 +1313,7 @@ impl<'a> SliceDecoder<'a> { } /// Scaling, transform and addition to the prediction. - fn reconstruct(&mut self, x0: i32, y0: i32, log2: u32, c: usize, qp: i32, ts: bool, max_x: usize, max_y: usize) -> Result<()> { + fn reconstruct(&mut self, (x0, y0): (i32, i32), log2: u32, c: usize, qp: i32, ts: bool, (max_x, max_y): (usize, usize)) -> Result<()> { let n = 1usize << log2; let bd = if c == 0 { self.pic.bd_y } else { self.pic.bd_c }; let coeffs = &mut self.coeffs[..n * n]; @@ -1355,19 +1365,8 @@ impl<'a> SliceDecoder<'a> { } /// prediction_unit(): parse, derive motion, store it and predict. Returns merge_flag. - fn prediction_unit( - &mut self, - xc: i32, - yc: i32, - ncb: i32, - xp: i32, - yp: i32, - w: i32, - h: i32, - part_idx: usize, - part: PartMode, - skip: bool, - ) -> Result { + fn prediction_unit(&mut self, g: PbGeom, part: PartMode, skip: bool) -> Result { + let PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx } = g; let merge = if skip { true } else { self.dd(MERGE_FLAG) == 1 }; let mvf = if merge { let idx = if self.sh.max_num_merge_cand > 1 { @@ -1382,7 +1381,7 @@ impl<'a> SliceDecoder<'a> { } else { 0 }; - self.derive_merge(xc, yc, ncb, xp, yp, w, h, part_idx, part, idx as usize) + self.derive_merge(PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }, part, idx as usize) } else { let pred_idc = if self.sh.is_b() { if w + h != 12 { @@ -1426,7 +1425,7 @@ impl<'a> SliceDecoder<'a> { if ref_idx[l] < 0 { continue; } - let mvp = self.derive_amvp(xc, yc, ncb, xp, yp, w, h, part_idx, l, ref_idx[l] as usize, mvp_flag[l] as usize); + let mvp = self.derive_amvp(PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }, l, ref_idx[l] as usize, mvp_flag[l] as usize); // mvLX = (mvp + mvd) wrapped to 16 bits (8-94) f.mv[l] = [(mvp[0] as i32 + mvd[l][0]) as i16, (mvp[1] as i32 + mvd[l][1]) as i16]; } @@ -1524,11 +1523,11 @@ impl<'a> SliceDecoder<'a> { if bi { let (w0, o0) = get(0); let (w1, o1) = get(1); - inter::put_weighted_bi(&self.pred0, &self.pred1, bw, bh, bd, log2wd, w0, w1, o0, o1, dst, stride); + inter::put_weighted_bi(&self.pred0, &self.pred1, (bw, bh), bd, (log2wd, w0, w1, o0, o1), dst, stride); } else { let l = if lists[0] { 0 } else { 1 }; let (wt, o) = get(l); - inter::put_weighted_uni(&self.pred0, bw, bh, bd, log2wd, wt, o, dst, stride); + inter::put_weighted_uni(&self.pred0, (bw, bh), bd, (log2wd, wt, o), dst, stride); } } } From 2aa0310e27a41dcbf6f530d158dfd78d1e21cc28 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 19:22:35 -0400 Subject: [PATCH 08/19] refactor(av1): split OBU parsing in decoder (S3776) (#1279) --- crates/av1/src/decoder.rs | 133 ++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 55 deletions(-) diff --git a/crates/av1/src/decoder.rs b/crates/av1/src/decoder.rs index 9f90f2f8..43abce22 100644 --- a/crates/av1/src/decoder.rs +++ b/crates/av1/src/decoder.rs @@ -405,49 +405,35 @@ impl Decoder { r } + /// Whether an OBU belongs to the selected operating point (spec 7.5: OBUs of other layers are dropped). + fn in_operating_point(&self, obu: &ObuUnit) -> bool { + if obu.obu_type != OBU_SEQUENCE_HEADER_T + && obu.obu_type != OBU_TEMPORAL_DELIMITER_T + && obu.ext + && let Some(seq) = &self.seq + && seq.op_idc != 0 + { + let idc = seq.op_idc; + let in_t = (idc >> obu.temporal_id) & 1; + let in_s = (idc >> (obu.spatial_id + 8)) & 1; + if in_t == 0 || in_s == 0 { + return false; + } + } + true + } + fn parse_obus(&mut self, data: &[u8], out: &mut Vec<(Output, usize, u32)>) -> Result<()> { let mut tu = 0usize; let mut pos = 0; while pos < data.len() { - let h = data[pos]; - if h & 0x80 != 0 { - return Err(Error::Invalid("obu_forbidden_bit")); + let obu = parse_obu_unit(data, pos)?; + pos = obu.end; + if !self.in_operating_point(&obu) { + continue; } - let obu_type = ((h >> 3) & 0xf) as u32; - let ext = h & 4 != 0; - let has_size = h & 2 != 0; - let mut p = pos + 1; - let (mut temporal_id, mut spatial_id) = (0, 0); - if ext { - let e = *data.get(p).ok_or(Error::Truncated)?; - temporal_id = (e >> 5) as u32; - spatial_id = ((e >> 3) & 3) as u32; - p += 1; - } - let size = if has_size { - let (v, n) = leb128(data.get(p..).ok_or(Error::Truncated)?)?; - p += n; - v as usize - } else { - data.len().checked_sub(p).ok_or(Error::Truncated)? - }; - let end = p.checked_add(size).filter(|&e| e <= data.len()).ok_or(Error::Truncated)?; - let payload = &data[p..end]; - pos = end; - if obu_type != OBU_SEQUENCE_HEADER_T - && obu_type != OBU_TEMPORAL_DELIMITER_T - && ext - && let Some(seq) = &self.seq - && seq.op_idc != 0 - { - let idc = seq.op_idc; - let in_t = (idc >> temporal_id) & 1; - let in_s = (idc >> (spatial_id + 8)) & 1; - if in_t == 0 || in_s == 0 { - continue; - } - } - match obu_type { + let (payload, spatial_id) = (obu.payload, obu.spatial_id); + match obu.obu_type { OBU_SEQUENCE_HEADER_T => { let s = SequenceHeader::parse(payload)?; self.seq = Some(s); @@ -457,27 +443,11 @@ impl Decoder { tu += 1; } OBU_FRAME_HEADER_T | OBU_REDUNDANT_FRAME_HEADER_T | OBU_FRAME_T => { - if self.seen_frame_header { - // frame_header_copy(): identical to the active header - if obu_type != OBU_FRAME_T { - continue; - } - } - let mut r = BitReader::new(payload); - let shown = self.frame_header(&mut r, temporal_id, spatial_id)?; - if let Some(o) = shown { + if let Some(o) = self.frame_obu(&obu)? { if out.iter().filter(|(o, _, _)| matches!(o, Output::Existing { .. })).count() >= 32 { return Err(Error::Invalid("too many frame outputs in chunk")); } out.push((o, tu, spatial_id)); - continue; - } - if obu_type == OBU_FRAME_T { - r.byte_align(); - let rest = payload.get(r.byte_pos()..).ok_or(Error::Truncated)?; - if let Some(o) = self.tile_group(rest)? { - out.push((o, tu, spatial_id)); - } } } OBU_TILE_GROUP_T => { @@ -492,6 +462,22 @@ impl Decoder { Ok(()) } + /// A frame header or frame OBU: returns the picture it completes, if any. + fn frame_obu(&mut self, obu: &ObuUnit) -> Result> { + // frame_header_copy(): identical to the active header + if self.seen_frame_header && obu.obu_type != OBU_FRAME_T { + return Ok(None); + } + let mut r = BitReader::new(obu.payload); + let shown = self.frame_header(&mut r, obu.temporal_id, obu.spatial_id)?; + if shown.is_some() || obu.obu_type != OBU_FRAME_T { + return Ok(shown); + } + r.byte_align(); + let rest = obu.payload.get(r.byte_pos()..).ok_or(Error::Truncated)?; + self.tile_group(rest) + } + /// frame_header_obu( ): returns the output for show_existing_frame. fn frame_header(&mut self, r: &mut BitReader, temporal_id: u32, spatial_id: u32) -> Result> { let seq = self.seq.clone().ok_or(Error::Invalid("frame before sequence header"))?; @@ -1017,6 +1003,43 @@ mod tests { } } +/// One OBU: its header fields, payload and the position just past it. +struct ObuUnit<'a> { + obu_type: u32, + ext: bool, + temporal_id: u32, + spatial_id: u32, + payload: &'a [u8], + end: usize, +} + +fn parse_obu_unit(data: &[u8], pos: usize) -> Result> { + let h = data[pos]; + if h & 0x80 != 0 { + return Err(Error::Invalid("obu_forbidden_bit")); + } + let obu_type = ((h >> 3) & 0xf) as u32; + let ext = h & 4 != 0; + let has_size = h & 2 != 0; + let mut p = pos + 1; + let (mut temporal_id, mut spatial_id) = (0, 0); + if ext { + let e = *data.get(p).ok_or(Error::Truncated)?; + temporal_id = (e >> 5) as u32; + spatial_id = ((e >> 3) & 3) as u32; + p += 1; + } + let size = if has_size { + let (v, n) = leb128(data.get(p..).ok_or(Error::Truncated)?)?; + p += n; + v as usize + } else { + data.len().checked_sub(p).ok_or(Error::Truncated)? + }; + let end = p.checked_add(size).filter(|&e| e <= data.len()).ok_or(Error::Truncated)?; + Ok(ObuUnit { obu_type, ext, temporal_id, spatial_id, payload: &data[p..end], end }) +} + #[cfg(test)] mod guard_tests { use super::*; From 560f80abe21b918aa5877a9b61a4196b8c5b80a2 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 19:22:58 -0400 Subject: [PATCH 09/19] refactor(h264): split derive_inter_motion and residual_cavlc (S3776) (#1272) --- crates/h264/src/slicedec.rs | 62 +++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/crates/h264/src/slicedec.rs b/crates/h264/src/slicedec.rs index 99d448e8..17fb8ba7 100644 --- a/crates/h264/src/slicedec.rs +++ b/crates/h264/src/slicedec.rs @@ -665,33 +665,38 @@ impl<'a> SliceDecoder<'a> { return Ok(()); } for p in 0..4 { - let sub = self.cur.sub[p]; - let (x8, y8) = ((p & 1) * 2, (p >> 1) * 2); - if sub.direct { - self.derive_direct(1 << p)?; - continue; + self.derive_sub_mb_motion(p, n)?; + } + Ok(()) + } + + /// Motion of one 8x8 sub-macroblock `p` of a P8x8/B8x8 MB. + fn derive_sub_mb_motion(&mut self, p: usize, n: [usize; 2]) -> Result<()> { + let sub = self.cur.sub[p]; + let (x8, y8) = ((p & 1) * 2, (p >> 1) * 2); + if sub.direct { + return self.derive_direct(1 << p); + } + let mut refs = [-1i8; 2]; + for (l, r) in refs.iter_mut().enumerate() { + if sub.pred & (1 << l) != 0 { + let v = self.cur.ref_idx[l][p]; + ensure!((v as usize) < n[l], "ref_idx {v} out of range (list {l}, size {})", n[l]); + *r = v; } - let mut refs = [-1i8; 2]; - for (l, r) in refs.iter_mut().enumerate() { - if sub.pred & (1 << l) != 0 { - let v = self.cur.ref_idx[l][p]; - ensure!((v as usize) < n[l], "ref_idx {v} out of range (list {l}, size {})", n[l]); - *r = v; + } + for j in 0..sub.shape.num_parts() { + let (sx, sy, w, h) = sub.shape.rect(j); + let (x, y) = (x8 + sx, y8 + sy); + let mut mvs = [[0i16; 2]; 2]; + for l in 0..2 { + if refs[l] >= 0 { + let mvp = self.mvp(l, x, y, w, refs[l], 0, 0); + let d = self.cur.mvd[l][p * 4 + j]; + mvs[l] = [mvp[0].wrapping_add(d[0]), mvp[1].wrapping_add(d[1])]; } } - for j in 0..sub.shape.num_parts() { - let (sx, sy, w, h) = sub.shape.rect(j); - let (x, y) = (x8 + sx, y8 + sy); - let mut mvs = [[0i16; 2]; 2]; - for l in 0..2 { - if refs[l] >= 0 { - let mvp = self.mvp(l, x, y, w, refs[l], 0, 0); - let d = self.cur.mvd[l][p * 4 + j]; - mvs[l] = [mvp[0].wrapping_add(d[0]), mvp[1].wrapping_add(d[1])]; - } - } - self.set_motion(x, y, w, h, refs, mvs); - } + self.set_motion(x, y, w, h, refs, mvs); } Ok(()) } @@ -1380,6 +1385,11 @@ impl<'a> SliceDecoder<'a> { cavlc::residual_block(r, &mut lv, 0, 15, 16, nc)?; self.put_luma_dc(&lv); } + self.residual_cavlc_luma(r, cbp, kind, t8)?; + self.residual_cavlc_chroma(r, cbp) + } + + fn residual_cavlc_luma(&mut self, r: &mut BitReader, cbp: u8, kind: MbKind, t8: bool) -> Result<()> { for b8 in 0..4 { let (bx8, by8) = ((b8 & 1) * 2, (b8 >> 1) * 2); if cbp & (1 << b8) == 0 { @@ -1413,6 +1423,10 @@ impl<'a> SliceDecoder<'a> { } } } + Ok(()) + } + + fn residual_cavlc_chroma(&mut self, r: &mut BitReader, cbp: u8) -> Result<()> { let cbp_c = cbp >> 4; if cbp_c & 3 != 0 { for c in 0..2 { From a78d9a9a190013ce96fc46249a87a1167c23646a Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 19:23:04 -0400 Subject: [PATCH 10/19] refactor(av1): split film grain synthesis into stages (S3776) (#1276) --- crates/av1/src/grain.rs | 480 ++++++++++++++++++++++++++-------------- 1 file changed, 313 insertions(+), 167 deletions(-) diff --git a/crates/av1/src/grain.rs b/crates/av1/src/grain.rs index 4222c672..2fa2a6be 100644 --- a/crates/av1/src/grain.rs +++ b/crates/av1/src/grain.rs @@ -55,21 +55,79 @@ impl Rng { } } -pub(crate) fn apply(pic: &mut Picture, g: &FilmGrainParams, seq: &SequenceHeader) { - let _ = seq; - let bd = pic.bit_depth as u32; - let mono = pic.mono_chrome; - let num_planes = if mono { 1 } else { 3 }; - let (sub_x, sub_y) = (pic.subsampling_x as usize, pic.subsampling_y as usize); - let w = pic.width as usize; - let h = pic.height as usize; - let grain_center = 128i32 << (bd - 8); - let grain_min = -grain_center; - let grain_max = (256i32 << (bd - 8)) - 1 - grain_center; +/// Picture-level parameters shared by the grain synthesis stages. +struct GrainCtx<'a> { + g: &'a FilmGrainParams, + bd: u32, + mono: bool, + sub_x: usize, + sub_y: usize, + w: usize, + h: usize, + grain_min: i32, + grain_max: i32, +} - // Generate grain (7.18.3.3). - let mut rng = Rng(g.grain_seed); - let shift = 12 - bd + g.grain_scale_shift; +impl GrainCtx<'_> { + fn num_planes(&self) -> usize { + if self.mono { 1 } else { 3 } + } + + fn grain_shift(&self) -> u32 { + 12 - self.bd + self.g.grain_scale_shift + } + + fn ar_shift(&self) -> u32 { + self.g.ar_coeff_shift_minus_6 + 6 + } + + fn lag(&self) -> i32 { + self.g.ar_coeff_lag as i32 + } + + fn stripe_w(&self) -> usize { + self.w + 64 + } +} + +type GrainTemplate = Vec<[i32; 82]>; + +/// The luma, Cb and Cr grain templates (7.18.3.3). +struct Templates { + luma: GrainTemplate, + cb: GrainTemplate, + cr: GrainTemplate, +} + +impl Templates { + fn plane(&self, plane: usize) -> &GrainTemplate { + match plane { + 0 => &self.luma, + 1 => &self.cb, + _ => &self.cr, + } + } +} + +fn luma_ar_sum(g: &FilmGrainParams, luma: &[[i32; 82]], lag: i32, y: usize, x: usize) -> i32 { + let mut s = 0; + let mut pos = 0; + 'outer: for dr in -lag..=0 { + for dc in -lag..=lag { + if dr == 0 && dc == 0 { + break 'outer; + } + let c = g.ar_coeffs_y_plus_128[pos] as i32 - 128; + s += luma[(y as i32 + dr) as usize][(x as i32 + dc) as usize] * c; + pos += 1; + } + } + s +} + +fn luma_grain(c: &GrainCtx, rng: &mut Rng) -> GrainTemplate { + let g = c.g; + let shift = c.grain_shift(); let mut luma = vec![[0i32; 82]; 73]; for row in luma.iter_mut() { for v in row.iter_mut() { @@ -77,77 +135,89 @@ pub(crate) fn apply(pic: &mut Picture, g: &FilmGrainParams, seq: &SequenceHeader *v = round2(gv, shift); } } - let ar_shift = g.ar_coeff_shift_minus_6 + 6; - let lag = g.ar_coeff_lag as i32; + let ar_shift = c.ar_shift(); + let lag = c.lag(); for y in 3..73 { for x in 3..82 - 3 { - let mut s = 0; - let mut pos = 0; - 'outer: for dr in -lag..=0 { - for dc in -lag..=lag { - if dr == 0 && dc == 0 { - break 'outer; - } - let c = g.ar_coeffs_y_plus_128[pos] as i32 - 128; - s += luma[(y as i32 + dr) as usize][(x as i32 + dc) as usize] * c; - pos += 1; - } - } - luma[y][x] = (luma[y][x] + round2(s, ar_shift)).clamp(grain_min, grain_max); - } - } - let chroma_w = if sub_x != 0 { 44 } else { 82 }; - let chroma_h = if sub_y != 0 { 38 } else { 73 }; - let mut cb = vec![[0i32; 82]; 73]; - let mut cr = vec![[0i32; 82]; 73]; - if !mono { - for (arr, seed, on) in [(&mut cb, 0xb524u32, g.num_cb_points > 0), (&mut cr, 0x49d8, g.num_cr_points > 0)] { - rng.0 = g.grain_seed ^ seed; - for row in arr.iter_mut().take(chroma_h) { - for v in row.iter_mut().take(chroma_w) { - let gv = if on || g.chroma_scaling_from_luma { GAUSSIAN_SEQUENCE[rng.next(11) as usize] as i32 } else { 0 }; - *v = round2(gv, shift); - } - } - } - for y in 3..chroma_h { - for x in 3..chroma_w - 3 { - let mut s0 = 0; - let mut s1 = 0; - let mut pos = 0; - 'outer: for dr in -lag..=0 { - for dc in -lag..=lag { - let c0 = g.ar_coeffs_cb_plus_128[pos] as i32 - 128; - let c1 = g.ar_coeffs_cr_plus_128[pos] as i32 - 128; - if dr == 0 && dc == 0 { - if g.num_y_points > 0 { - let mut l = 0; - let lx = ((x - 3) << sub_x) + 3; - let ly = ((y - 3) << sub_y) + 3; - for i in 0..=sub_y { - for j in 0..=sub_x { - l += luma[ly + i][lx + j]; - } - } - let l = round2(l, (sub_x + sub_y) as u32); - s0 += l * c0; - s1 += l * c1; - } - break 'outer; - } - let (yy, xx) = ((y as i32 + dr) as usize, (x as i32 + dc) as usize); - s0 += cb[yy][xx] * c0; - s1 += cr[yy][xx] * c1; - pos += 1; - } - } - cb[y][x] = (cb[y][x] + round2(s0, ar_shift)).clamp(grain_min, grain_max); - cr[y][x] = (cr[y][x] + round2(s1, ar_shift)).clamp(grain_min, grain_max); - } + let s = luma_ar_sum(g, &luma, lag, y, x); + luma[y][x] = (luma[y][x] + round2(s, ar_shift)).clamp(c.grain_min, c.grain_max); } } + luma +} - // Scaling lookup (7.18.3.4). +/// The luma contribution to the chroma autoregressive filter at chroma position (y, x). +fn chroma_luma_term(c: &GrainCtx, luma: &[[i32; 82]], y: usize, x: usize) -> i32 { + let mut l = 0; + let lx = ((x - 3) << c.sub_x) + 3; + let ly = ((y - 3) << c.sub_y) + 3; + for i in 0..=c.sub_y { + for j in 0..=c.sub_x { + l += luma[ly + i][lx + j]; + } + } + round2(l, (c.sub_x + c.sub_y) as u32) +} + +fn chroma_ar_sums(c: &GrainCtx, t: &Templates, y: usize, x: usize) -> (i32, i32) { + let g = c.g; + let lag = c.lag(); + let mut s0 = 0; + let mut s1 = 0; + let mut pos = 0; + 'outer: for dr in -lag..=0 { + for dc in -lag..=lag { + let c0 = g.ar_coeffs_cb_plus_128[pos] as i32 - 128; + let c1 = g.ar_coeffs_cr_plus_128[pos] as i32 - 128; + if dr == 0 && dc == 0 { + if g.num_y_points > 0 { + let l = chroma_luma_term(c, &t.luma, y, x); + s0 += l * c0; + s1 += l * c1; + } + break 'outer; + } + let (yy, xx) = ((y as i32 + dr) as usize, (x as i32 + dc) as usize); + s0 += t.cb[yy][xx] * c0; + s1 += t.cr[yy][xx] * c1; + pos += 1; + } + } + (s0, s1) +} + +/// Chroma grain templates; zero for monochrome pictures. +fn chroma_grain(c: &GrainCtx, rng: &mut Rng, luma: GrainTemplate) -> Templates { + let g = c.g; + let mut t = Templates { luma, cb: vec![[0i32; 82]; 73], cr: vec![[0i32; 82]; 73] }; + if c.mono { + return t; + } + let shift = c.grain_shift(); + let ar_shift = c.ar_shift(); + let chroma_w = if c.sub_x != 0 { 44 } else { 82 }; + let chroma_h = if c.sub_y != 0 { 38 } else { 73 }; + for (arr, seed, on) in [(&mut t.cb, 0xb524u32, g.num_cb_points > 0), (&mut t.cr, 0x49d8, g.num_cr_points > 0)] { + rng.0 = g.grain_seed ^ seed; + for row in arr.iter_mut().take(chroma_h) { + for v in row.iter_mut().take(chroma_w) { + let gv = if on || g.chroma_scaling_from_luma { GAUSSIAN_SEQUENCE[rng.next(11) as usize] as i32 } else { 0 }; + *v = round2(gv, shift); + } + } + } + for y in 3..chroma_h { + for x in 3..chroma_w - 3 { + let (s0, s1) = chroma_ar_sums(c, &t, y, x); + t.cb[y][x] = (t.cb[y][x] + round2(s0, ar_shift)).clamp(c.grain_min, c.grain_max); + t.cr[y][x] = (t.cr[y][x] + round2(s1, ar_shift)).clamp(c.grain_min, c.grain_max); + } + } + t +} + +/// Scaling lookup tables (7.18.3.4), one per plane. +fn scaling_luts(g: &FilmGrainParams, num_planes: usize) -> [[i32; 256]; 3] { let mut lut = [[0i32; 256]; 3]; for (plane, l) in lut.iter_mut().enumerate().take(num_planes) { let (n, xs, ys) = if plane == 0 || g.chroma_scaling_from_luma { @@ -159,101 +229,141 @@ pub(crate) fn apply(pic: &mut Picture, g: &FilmGrainParams, seq: &SequenceHeader }; *l = scaling_lut(n, xs, ys); } - let scale_lut = |plane: usize, index: i32| -> i32 { - let sh = bd - 8; - let x = index >> sh; - let rem = index - (x << sh); - if bd == 8 || x == 255 { - lut[plane][x as usize] - } else { - let start = lut[plane][x as usize]; - let end = lut[plane][x as usize + 1]; - start + round2((end - start) * rem, sh) - } - }; + lut +} - // Noise stripes (7.18.3.5). - let stripe_w = w + 64; - let n_stripes = (h.div_ceil(2)).div_ceil(16).max(1); - let mut stripes: Vec<[Vec; 3]> = (0..n_stripes).map(|_| [vec![0; 34 * stripe_w], vec![0; 34 * stripe_w], vec![0; 34 * stripe_w]]).collect(); - let overlap = g.overlap_flag; - for (luma_num, stripe) in stripes.iter_mut().enumerate() { - let mut r = g.grain_seed; - r ^= (((luma_num * 37 + 178) & 255) << 8) as u32; - r ^= ((luma_num * 173 + 105) & 255) as u32; - rng.0 = r; - let mut x = 0; - while x < w.div_ceil(2) { - let rand = rng.next(8) as usize; - let off_x = rand >> 4; - let off_y = rand & 15; - for (plane, s) in stripe.iter_mut().enumerate().take(num_planes) { - let psx = if plane > 0 { sub_x } else { 0 }; - let psy = if plane > 0 { sub_y } else { 0 }; - let pox = if psx != 0 { 6 + off_x } else { 9 + off_x * 2 }; - let poy = if psy != 0 { 6 + off_y } else { 9 + off_y * 2 }; - let src = match plane { - 0 => &luma, - 1 => &cb, - _ => &cr, +fn scale_lookup(lut: &[[i32; 256]; 3], bd: u32, plane: usize, index: i32) -> i32 { + let sh = bd - 8; + let x = index >> sh; + let rem = index - (x << sh); + if bd == 8 || x == 255 { + lut[plane][x as usize] + } else { + let start = lut[plane][x as usize]; + let end = lut[plane][x as usize + 1]; + start + round2((end - start) * rem, sh) + } +} + +/// Fills one plane of a noise stripe from the grain template at stripe column `x`. +fn fill_stripe_plane(c: &GrainCtx, src: &GrainTemplate, s: &mut [i32], plane: usize, offsets: (usize, usize), x: usize) { + let (off_x, off_y) = offsets; + let stripe_w = c.stripe_w(); + let overlap = c.g.overlap_flag; + let psx = if plane > 0 { c.sub_x } else { 0 }; + let psy = if plane > 0 { c.sub_y } else { 0 }; + let pox = if psx != 0 { 6 + off_x } else { 9 + off_x * 2 }; + let poy = if psy != 0 { 6 + off_y } else { 9 + off_y * 2 }; + for i in 0..(34 >> psy) { + for j in 0..(34 >> psx) { + let mut gv = src[poy + i][pox + j]; + let (idx, blend) = if psx == 0 { (i * stripe_w + x * 2 + j, j < 2) } else { (i * stripe_w + x + j, j == 0) }; + if blend && overlap && x > 0 { + let old = s[idx]; + gv = if psx != 0 { + old * 23 + gv * 22 + } else if j == 0 { + old * 27 + gv * 17 + } else { + old * 17 + gv * 27 }; - for i in 0..(34 >> psy) { - for j in 0..(34 >> psx) { - let mut gv = src[poy + i][pox + j]; - if psx == 0 { - let idx = i * stripe_w + x * 2 + j; - if j < 2 && overlap && x > 0 { - let old = s[idx]; - gv = if j == 0 { old * 27 + gv * 17 } else { old * 17 + gv * 27 }; - gv = round2(gv, 5).clamp(grain_min, grain_max); - } - s[idx] = gv; - } else { - let idx = i * stripe_w + x + j; - if j == 0 && overlap && x > 0 { - let old = s[idx]; - gv = round2(old * 23 + gv * 22, 5).clamp(grain_min, grain_max); - } - s[idx] = gv; - } - } - } + gv = round2(gv, 5).clamp(c.grain_min, c.grain_max); } - x += 16; + s[idx] = gv; } } - let noise_at = |plane: usize, y: usize, x: usize| -> i32 { - let psy = if plane > 0 { sub_y } else { 0 }; +} + +/// Noise stripes (7.18.3.5). +struct Stripes { + data: Vec<[Vec; 3]>, + stripe_w: usize, + overlap: bool, + sub_y: usize, + grain_min: i32, + grain_max: i32, +} + +impl Stripes { + fn build(c: &GrainCtx, t: &Templates, rng: &mut Rng) -> Stripes { + let g = c.g; + let stripe_w = c.stripe_w(); + let n_stripes = (c.h.div_ceil(2)).div_ceil(16).max(1); + let mut data: Vec<[Vec; 3]> = (0..n_stripes).map(|_| [vec![0; 34 * stripe_w], vec![0; 34 * stripe_w], vec![0; 34 * stripe_w]]).collect(); + for (luma_num, stripe) in data.iter_mut().enumerate() { + let mut r = g.grain_seed; + r ^= (((luma_num * 37 + 178) & 255) << 8) as u32; + r ^= ((luma_num * 173 + 105) & 255) as u32; + rng.0 = r; + let mut x = 0; + while x < c.w.div_ceil(2) { + let rand = rng.next(8) as usize; + let offsets = (rand >> 4, rand & 15); + for (plane, s) in stripe.iter_mut().enumerate().take(c.num_planes()) { + fill_stripe_plane(c, t.plane(plane), s, plane, offsets, x); + } + x += 16; + } + } + Stripes { data, stripe_w, overlap: g.overlap_flag, sub_y: c.sub_y, grain_min: c.grain_min, grain_max: c.grain_max } + } + + fn noise_at(&self, plane: usize, y: usize, x: usize) -> i32 { + let psy = if plane > 0 { self.sub_y } else { 0 }; let luma_num = y >> (5 - psy); let i = y - (luma_num << (5 - psy)); - let mut gv = stripes[luma_num][plane][i * stripe_w + x]; + let mut gv = self.data[luma_num][plane][i * self.stripe_w + x]; if psy == 0 { - if i < 2 && luma_num > 0 && overlap { - let old = stripes[luma_num - 1][plane][(i + 32) * stripe_w + x]; + if i < 2 && luma_num > 0 && self.overlap { + let old = self.data[luma_num - 1][plane][(i + 32) * self.stripe_w + x]; gv = if i == 0 { old * 27 + gv * 17 } else { old * 17 + gv * 27 }; - gv = round2(gv, 5).clamp(grain_min, grain_max); + gv = round2(gv, 5).clamp(self.grain_min, self.grain_max); } - } else if i < 1 && luma_num > 0 && overlap { - let old = stripes[luma_num - 1][plane][(i + 16) * stripe_w + x]; - gv = round2(old * 23 + gv * 22, 5).clamp(grain_min, grain_max); + } else if i < 1 && luma_num > 0 && self.overlap { + let old = self.data[luma_num - 1][plane][(i + 16) * self.stripe_w + x]; + gv = round2(old * 23 + gv * 22, 5).clamp(self.grain_min, self.grain_max); } gv - }; + } +} - // Blend (7.18.3.5). - let (min_value, max_luma, max_chroma) = if g.clip_to_restricted_range { - let ml = 235 << (bd - 8); - (16 << (bd - 8), ml, if pic.matrix_coefficients as usize == MC_IDENTITY { ml } else { 240 << (bd - 8) }) - } else { - let m = (256 << (bd - 8)) - 1; - (0, m, m) - }; - let pix_max = (1i32 << bd) - 1; - let scaling_shift = g.grain_scaling_minus_8 + 8; - if !mono { +/// Output clipping range and scaling shift for the blend step (7.18.3.5). +struct BlendRange { + min_value: i32, + max_luma: i32, + max_chroma: i32, + pix_max: i32, + scaling_shift: u32, +} + +impl BlendRange { + fn new(c: &GrainCtx, matrix_coefficients: usize) -> BlendRange { + let bd = c.bd; + let (min_value, max_luma, max_chroma) = if c.g.clip_to_restricted_range { + let ml = 235 << (bd - 8); + (16 << (bd - 8), ml, if matrix_coefficients == MC_IDENTITY { ml } else { 240 << (bd - 8) }) + } else { + let m = (256 << (bd - 8)) - 1; + (0, m, m) + }; + BlendRange { min_value, max_luma, max_chroma, pix_max: (1i32 << bd) - 1, scaling_shift: c.g.grain_scaling_minus_8 + 8 } + } +} + +struct BlendEnv<'a> { + c: &'a GrainCtx<'a>, + lut: &'a [[i32; 256]; 3], + stripes: &'a Stripes, + range: &'a BlendRange, +} + +impl BlendEnv<'_> { + fn blend_chroma(&self, py: &[u16], pu: &mut [u16], pv: &mut [u16]) { + let c = self.c; + let g = c.g; + let (w, h, sub_x, sub_y, bd) = (c.w, c.h, c.sub_x, c.sub_y, c.bd); let cw = (w + sub_x) >> sub_x; let ch = (h + sub_y) >> sub_y; - let [py, pu, pv] = &mut pic.planes; for y in 0..ch { for x in 0..cw { let lx = x << sub_x; @@ -272,26 +382,62 @@ pub(crate) fn apply(pic: &mut Picture, g: &FilmGrainParams, seq: &SequenceHeader avg } else { let combined = avg * (luma_mult as i32 - 128) + orig * (mult as i32 - 128); - ((combined >> 6) + ((offset as i32 - 256) << (bd - 8))).clamp(0, pix_max) + ((combined >> 6) + ((offset as i32 - 256) << (bd - 8))).clamp(0, self.range.pix_max) }; - let noise = round2(scale_lut(plane, merged) * noise_at(plane, y, x), scaling_shift); - out[y * cw + x] = (orig + noise).clamp(min_value, max_chroma) as u16; + let noise = round2(scale_lookup(self.lut, bd, plane, merged) * self.stripes.noise_at(plane, y, x), self.range.scaling_shift); + out[y * cw + x] = (orig + noise).clamp(self.range.min_value, self.range.max_chroma) as u16; } } } } - if g.num_y_points > 0 { - let py = &mut pic.planes[0]; + + fn blend_luma(&self, py: &mut [u16]) { + let (w, h) = (self.c.w, self.c.h); for y in 0..h { for x in 0..w { let orig = py[y * w + x] as i32; - let noise = round2(scale_lut(0, orig) * noise_at(0, y, x), scaling_shift); - py[y * w + x] = (orig + noise).clamp(min_value, max_luma) as u16; + let noise = round2(scale_lookup(self.lut, self.c.bd, 0, orig) * self.stripes.noise_at(0, y, x), self.range.scaling_shift); + py[y * w + x] = (orig + noise).clamp(self.range.min_value, self.range.max_luma) as u16; } } } } +pub(crate) fn apply(pic: &mut Picture, g: &FilmGrainParams, seq: &SequenceHeader) { + let _ = seq; + let bd = pic.bit_depth as u32; + let grain_center = 128i32 << (bd - 8); + let c = GrainCtx { + g, + bd, + mono: pic.mono_chrome, + sub_x: pic.subsampling_x as usize, + sub_y: pic.subsampling_y as usize, + w: pic.width as usize, + h: pic.height as usize, + grain_min: -grain_center, + grain_max: (256i32 << (bd - 8)) - 1 - grain_center, + }; + + // Generate grain (7.18.3.3). + let mut rng = Rng(g.grain_seed); + let luma = luma_grain(&c, &mut rng); + let templates = chroma_grain(&c, &mut rng, luma); + let lut = scaling_luts(g, c.num_planes()); + let stripes = Stripes::build(&c, &templates, &mut rng); + + // Blend (7.18.3.5). + let range = BlendRange::new(&c, pic.matrix_coefficients as usize); + let env = BlendEnv { c: &c, lut: &lut, stripes: &stripes, range: &range }; + if !c.mono { + let [py, pu, pv] = &mut pic.planes; + env.blend_chroma(py, pu, pv); + } + if g.num_y_points > 0 { + env.blend_luma(&mut pic.planes[0]); + } +} + #[cfg(test)] mod tests { use super::*; From 3418dec9e47c7237c56393c3602dcd5ad095768d Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 19:36:54 -0400 Subject: [PATCH 11/19] refactor(h264): Sonar S3776 dpb helpers (#1275) * refactor(h264): split dpb functions (S3776) * fix(h264): restore the MMCO 6 long-term index range check --- crates/h264/src/dpb.rs | 346 +++++++++++++++++++++++------------------ 1 file changed, 197 insertions(+), 149 deletions(-) diff --git a/crates/h264/src/dpb.rs b/crates/h264/src/dpb.rs index da652b47..a74799d9 100644 --- a/crates/h264/src/dpb.rs +++ b/crates/h264/src/dpb.rs @@ -3,7 +3,7 @@ use crate::error::{Result, ensure}; use crate::picture::{FrameRef, RefPic}; -use crate::slice::{Mmco, SliceHeader, SliceType}; +use crate::slice::{Mmco, RefPicListMod, SliceHeader, SliceType}; use std::sync::Arc; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -174,6 +174,103 @@ impl Dpb { } } + /// Initial reference picture lists (8.2.4.2) as DPB entries. + fn initial_entry_lists<'a>( + sh: &SliceHeader, + cur_poc: i32, + short: &[&'a DpbEntry], + long: &[&'a DpbEntry], + pic_num: impl Fn(&DpbEntry) -> i32, + ) -> [Vec<&'a DpbEntry>; 2] { + let mut entry_lists: [Vec<&'a DpbEntry>; 2] = [Vec::new(), Vec::new()]; + if sh.slice_type != SliceType::B { + let mut s = short.to_vec(); + s.sort_by_key(|e| std::cmp::Reverse(pic_num(e))); + entry_lists[0] = s.into_iter().chain(long.iter().copied()).collect(); + } else { + let mut before: Vec<&'a DpbEntry> = short.iter().copied().filter(|e| e.poc < cur_poc).collect(); + let mut after: Vec<&'a DpbEntry> = short.iter().copied().filter(|e| e.poc > cur_poc).collect(); + before.sort_by_key(|e| std::cmp::Reverse(e.poc)); + after.sort_by_key(|e| e.poc); + entry_lists[0] = before.iter().chain(after.iter()).chain(long.iter()).copied().collect(); + entry_lists[1] = after.iter().chain(before.iter()).chain(long.iter()).copied().collect(); + if entry_lists[1].len() > 1 && entry_lists[0].len() == entry_lists[1].len() { + let same = entry_lists[0].iter().zip(entry_lists[1].iter()).all(|(a, b)| std::ptr::eq(*a, *b)); + if same { + entry_lists[1].swap(0, 1); + } + } + } + entry_lists + } + + /// Reference picture list modification (8.2.4.3) of one list; `n` is num_ref_idx_active. + fn modify_ref_list<'a>( + list: &mut Vec>, + mods: &[RefPicListMod], + n: usize, + cur_fn: u32, + max_frame_num: u32, + short: &[&'a DpbEntry], + long: &[&'a DpbEntry], + ) -> Result<()> { + let max_pic_num = max_frame_num as i32; + let curr_pic_num = cur_fn as i32; + let mut pred = curr_pic_num; + let mut ref_idx = 0usize; + for m in mods { + ensure!(ref_idx < n, "too many ref_pic_list_modification entries"); + list.push(None); // temporarily one longer + let (target, is_long): (Option<&DpbEntry>, bool) = match m.idc { + 0 | 1 => { + let d = m.value as i32 + 1; + let no_wrap = if m.idc == 0 { + if pred - d < 0 { pred - d + max_pic_num } else { pred - d } + } else if pred + d >= max_pic_num { + pred + d - max_pic_num + } else { + pred + d + }; + pred = no_wrap; + let pn = if no_wrap > curr_pic_num { no_wrap - max_pic_num } else { no_wrap }; + (short.iter().copied().find(|e| Self::frame_num_wrap(e, cur_fn, max_frame_num) == pn), false) + } + 2 => (long.iter().copied().find(|e| e.long_term_frame_idx == m.value), true), + _ => continue, + }; + let Some(target) = target else { + // Missing picture: keep the list unchanged (error concealment). + list.pop(); + ref_idx += 1; + continue; + }; + for c in (ref_idx + 1..=n).rev() { + list[c] = list[c - 1]; + } + list[ref_idx] = Some(target); + ref_idx += 1; + let mut nidx = ref_idx; + for c in ref_idx..=n { + let keep = match list[c] { + None => true, + Some(e) => { + if is_long { + !(e.mark == RefMark::Long && e.long_term_frame_idx == target.long_term_frame_idx) + } else { + !(e.mark == RefMark::Short && std::ptr::eq(e, target)) + } + } + }; + if keep { + list[nidx] = list[c]; + nidx += 1; + } + } + list.truncate(n); + } + Ok(()) + } + /// Initial reference picture lists (8.2.4.2) + modification (8.2.4.3) for a frame slice. pub fn build_ref_lists(&self, sh: &SliceHeader, cur_poc: i32, max_frame_num: u32) -> Result<[Vec; 2]> { let mut lists: [Vec; 2] = [Vec::new(), Vec::new()]; @@ -186,25 +283,7 @@ impl Dpb { let mut long: Vec<&DpbEntry> = self.entries.iter().filter(|e| e.mark == RefMark::Long).collect(); long.sort_by_key(|e| e.long_term_frame_idx); let to_ref = |e: &&DpbEntry| RefPic { frame: e.frame.clone(), long_term: e.mark == RefMark::Long }; - let mut entry_lists: [Vec<&DpbEntry>; 2] = [Vec::new(), Vec::new()]; - if sh.slice_type != SliceType::B { - let mut s = short.clone(); - s.sort_by_key(|e| std::cmp::Reverse(pic_num(e))); - entry_lists[0] = s.into_iter().chain(long.iter().copied()).collect(); - } else { - let mut before: Vec<&DpbEntry> = short.iter().copied().filter(|e| e.poc < cur_poc).collect(); - let mut after: Vec<&DpbEntry> = short.iter().copied().filter(|e| e.poc > cur_poc).collect(); - before.sort_by_key(|e| std::cmp::Reverse(e.poc)); - after.sort_by_key(|e| e.poc); - entry_lists[0] = before.iter().chain(after.iter()).chain(long.iter()).copied().collect(); - entry_lists[1] = after.iter().chain(before.iter()).chain(long.iter()).copied().collect(); - if entry_lists[1].len() > 1 && entry_lists[0].len() == entry_lists[1].len() { - let same = entry_lists[0].iter().zip(entry_lists[1].iter()).all(|(a, b)| std::ptr::eq(*a, *b)); - if same { - entry_lists[1].swap(0, 1); - } - } - } + let entry_lists = Self::initial_entry_lists(sh, cur_poc, &short, &long, pic_num); let nlists = if sh.slice_type == SliceType::B { 2 } else { 1 }; for l in 0..nlists { let n = sh.num_ref_idx_active[l] as usize; @@ -212,60 +291,7 @@ impl Dpb { list.truncate(n); list.resize(n, None); if !sh.ref_pic_list_mod[l].is_empty() { - let max_pic_num = max_frame_num as i32; - let curr_pic_num = cur_fn as i32; - let mut pred = curr_pic_num; - let mut ref_idx = 0usize; - for m in &sh.ref_pic_list_mod[l] { - ensure!(ref_idx < n, "too many ref_pic_list_modification entries"); - list.push(None); // temporarily one longer - let (target, is_long): (Option<&DpbEntry>, bool) = match m.idc { - 0 | 1 => { - let d = m.value as i32 + 1; - let no_wrap = if m.idc == 0 { - if pred - d < 0 { pred - d + max_pic_num } else { pred - d } - } else if pred + d >= max_pic_num { - pred + d - max_pic_num - } else { - pred + d - }; - pred = no_wrap; - let pn = if no_wrap > curr_pic_num { no_wrap - max_pic_num } else { no_wrap }; - (short.iter().copied().find(|e| pic_num(e) == pn), false) - } - 2 => (long.iter().copied().find(|e| e.long_term_frame_idx == m.value), true), - _ => continue, - }; - let Some(target) = target else { - // Missing picture: keep the list unchanged (error concealment). - list.pop(); - ref_idx += 1; - continue; - }; - for c in (ref_idx + 1..=n).rev() { - list[c] = list[c - 1]; - } - list[ref_idx] = Some(target); - ref_idx += 1; - let mut nidx = ref_idx; - for c in ref_idx..=n { - let keep = match list[c] { - None => true, - Some(e) => { - if is_long { - !(e.mark == RefMark::Long && e.long_term_frame_idx == target.long_term_frame_idx) - } else { - !(e.mark == RefMark::Short && std::ptr::eq(e, target)) - } - } - }; - if keep { - list[nidx] = list[c]; - nidx += 1; - } - } - list.truncate(n); - } + Self::modify_ref_list(&mut list, &sh.ref_pic_list_mod[l], n, cur_fn, max_frame_num, &short, &long)?; } // Fill missing entries (should not happen in conforming streams) with the first valid entry. let fallback = list.iter().flatten().next().copied().or_else(|| self.entries.last()); @@ -275,91 +301,88 @@ impl Dpb { Ok(lists) } - /// Adaptive memory control (8.2.5.4). Returns true if MMCO 5 was executed. - fn apply_mmcos(&mut self, mmcos: &[Mmco], cur_frame_num: u32, max_frame_num: u32, cur_long: &mut Option) -> bool { + /// One memory management control operation; returns true for MMCO 5. + fn apply_mmco(&mut self, m: &Mmco, cur_frame_num: u32, max_frame_num: u32, cur_long: &mut Option) -> bool { let mut had5 = false; let curr_pic_num = cur_frame_num as i32; - for m in mmcos { - match m.op { - 1 => { - let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1); - for e in &mut self.entries { - if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn { - e.mark = RefMark::Unused; - } + match m.op { + 1 => { + let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1); + for e in &mut self.entries { + if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn { + e.mark = RefMark::Unused; } } - 2 => { - for e in &mut self.entries { - if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_pic_num { - e.mark = RefMark::Unused; - } + } + 2 => { + for e in &mut self.entries { + if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_pic_num { + e.mark = RefMark::Unused; } } - 3 => { - let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1); + } + 3 => { + let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1); + for e in &mut self.entries { + if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx { + e.mark = RefMark::Unused; + } + } + for e in &mut self.entries { + if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn { + e.mark = RefMark::Long; + e.long_term_frame_idx = m.long_term_frame_idx; + } + } + } + 4 => { + let max = m.max_long_term_frame_idx_plus1; + for e in &mut self.entries { + if e.mark == RefMark::Long && (max == 0 || e.long_term_frame_idx > max - 1) { + e.mark = RefMark::Unused; + } + } + self.max_long_term_frame_idx = if max == 0 { None } else { Some(max - 1) }; + } + 5 => { + for e in &mut self.entries { + e.mark = RefMark::Unused; + } + self.max_long_term_frame_idx = None; + had5 = true; + } + 6 => { + // Only an index within the allowed range may be assigned (8.2.5.4.6). + let allowed = match self.max_long_term_frame_idx { + Some(max) => m.long_term_frame_idx <= max, + None => false, + }; + if allowed { for e in &mut self.entries { if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx { e.mark = RefMark::Unused; } } - for e in &mut self.entries { - if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn { - e.mark = RefMark::Long; - e.long_term_frame_idx = m.long_term_frame_idx; - } - } + *cur_long = Some(m.long_term_frame_idx); } - 4 => { - let max = m.max_long_term_frame_idx_plus1; - for e in &mut self.entries { - if e.mark == RefMark::Long && (max == 0 || e.long_term_frame_idx > max - 1) { - e.mark = RefMark::Unused; - } - } - self.max_long_term_frame_idx = if max == 0 { None } else { Some(max - 1) }; - } - 5 => { - for e in &mut self.entries { - e.mark = RefMark::Unused; - } - self.max_long_term_frame_idx = None; - had5 = true; - } - 6 => { - let allowed = match self.max_long_term_frame_idx { - Some(max) => m.long_term_frame_idx <= max, - None => false, - }; - if allowed { - for e in &mut self.entries { - if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx { - e.mark = RefMark::Unused; - } - } - *cur_long = Some(m.long_term_frame_idx); - } - } - _ => {} } + _ => {} + } + had5 + } + + /// Adaptive memory control (8.2.5.4). Returns true if MMCO 5 was executed. + fn apply_mmcos(&mut self, mmcos: &[Mmco], cur_frame_num: u32, max_frame_num: u32, cur_long: &mut Option) -> bool { + let mut had5 = false; + for m in mmcos { + had5 |= self.apply_mmco(m, cur_frame_num, max_frame_num, cur_long); } self.prune(); had5 } - /// Reference marking for the just-decoded picture and insertion into the DPB with output bumping. - /// Returns pictures ready for output. - #[allow(clippy::too_many_arguments)] - pub fn store_picture( - &mut self, - sh: &SliceHeader, - frame: FrameRef, - poc: i32, - max_frame_num: u32, - max_num_ref_frames: usize, - meta: Arc, - out: &mut Vec, - ) { + /// Reference marking of the current picture (8.2.5); returns (mark, long-term idx, MMCO 5 executed). + fn mark_current(&mut self, sh: &SliceHeader, max_frame_num: u32, max_num_ref_frames: usize) -> (RefMark, u32, bool) { let mut mark = RefMark::Unused; let mut long_idx = 0; let mut had5 = false; @@ -391,12 +414,12 @@ impl Dpb { } } } - if had5 { - // All earlier pictures are output before the current one (C.4.4). - self.flush(out); - } - let frame_num = if had5 { 0 } else { sh.frame_num }; - // Bumping until there is space (C.4.5.1 / C.4.5.2). + (mark, long_idx, had5) + } + + /// Bumping until there is space (C.4.5.1 / C.4.5.2). Returns true when the (non-reference) current + /// picture has the smallest POC and is to be output directly instead of being stored. + fn bump_for_space(&mut self, mark: RefMark, poc: i32, out: &mut Vec) -> bool { if mark == RefMark::Unused { loop { let waiting = self.entries.len(); @@ -406,8 +429,7 @@ impl Dpb { // non-reference picture with the smallest POC is output directly let min_poc = self.entries.iter().filter(|e| e.needed_for_output).map(|e| e.poc).min(); if min_poc.is_none_or(|m| poc < m) { - out.push(Output { frame, poc, meta }); - return; + return true; } if !self.bump(out) { break; @@ -425,6 +447,32 @@ impl Dpb { } } } + false + } + + /// Reference marking for the just-decoded picture and insertion into the DPB with output bumping. + /// Returns pictures ready for output. + #[allow(clippy::too_many_arguments)] + pub fn store_picture( + &mut self, + sh: &SliceHeader, + frame: FrameRef, + poc: i32, + max_frame_num: u32, + max_num_ref_frames: usize, + meta: Arc, + out: &mut Vec, + ) { + let (mark, long_idx, had5) = self.mark_current(sh, max_frame_num, max_num_ref_frames); + if had5 { + // All earlier pictures are output before the current one (C.4.4). + self.flush(out); + } + let frame_num = if had5 { 0 } else { sh.frame_num }; + if self.bump_for_space(mark, poc, out) { + out.push(Output { frame, poc, meta }); + return; + } self.entries.push(DpbEntry { frame, poc, From 230b6485d91a8fc5e171082ed4dd9e7d8ea39ac1 Mon Sep 17 00:00:00 2001 From: Sofia Alves Figueredo Date: Sun, 25 Oct 2026 21:23:43 -0300 Subject: [PATCH 12/19] fix(windows): keep the shortcut icon after an update (#1114) (#1391) * fix(windows): keep the shortcut icon after an update (#1114) The MSI's Start menu shortcut took its icon from the WiX , which Windows Installer caches under %APPDATA%\Microsoft\Installer\{ProductCode}\. Every update is a major upgrade with a new ProductCode, so the old folder is deleted. A taskbar pin is a copy of the shortcut that the update never touches, so it kept pointing at the deleted file and lost its icon. Drop the Icon from the shortcut so it shows the icon embedded in deckcraft.exe, whose path survives updates. The stays for ARPPRODUCTICON. A test keeps shortcuts from naming an MSI icon again. Co-Authored-By: Claude Sonnet 5.5 * contributors: credit Sofia Alves Figueredo by real name Co-Authored-By: Claude Sonnet 5.5 * fix(windows): make the Start menu shortcut non-advertised (#1114) Addresses the review comment: without an MSI Icon, a non-advertised shortcut targets the installed exe directly and takes its embedded icon. Co-Authored-By: Claude Sonnet 5.5 * fix(windows): package.ps1 rejects a deckcraft.exe with no embedded icon (#1114) The shortcuts now take the exe's own icon, and -SkipBuild packages an existing exe that may come from a build without DECKCRAFT_REQUIRE_WINRES=1. Count its icons with ExtractIconEx and stop before WiX when there are none. Co-Authored-By: Claude Sonnet 5.5 --------- Co-authored-by: Claude Sonnet 5.5 --- ROADMAP.md | 2 ++ apps/deckcraft/src/main.rs | 23 +++++++++++++++++++++++ contributors/people.toml | 3 +++ packaging/windows/deckcraft.wxs | 10 ++++++++-- packaging/windows/package.ps1 | 9 +++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b52abe28..b04c4b69 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -137,6 +137,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI ``, so a taskbar pin keeps its icon after an update (#1114); a pin that already lost it has to be unpinned and pinned again once | | 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off | | 2026-10-24 | Tables: while the caret is in a cell, dragging into another cell selects that range of cells (shaded, merged cells taken in whole), and Shift+arrow grows or shrinks it; formatting applies to the selected cells only, and Delete clears their text and keeps the table | | 2026-10-24 | Preferences ▸ Measurement units: inches or centimetres. The Format pane's Size and Position fields, Shape Format ▸ Size on the ribbon, the Slide Size dialog and the ruler show and take the chosen unit; the deck still stores points (#1326) | @@ -293,6 +294,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-25 | trivial | Progress log: Windows shortcut icon survives updates (#1114). Scores and milestone names unchanged | | 2026-10-24 | trivial | Progress log: selecting a range of table cells by dragging. Scores and milestone names unchanged | | 2026-10-24 | trivial | Progress log: measurement units in centimetres (#1326). Scores unchanged | | 2026-10-22 | trivial | Progress log: Animation Pane ▸ Play From. Scores unchanged | diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs index aa7e2611..605dc84a 100644 --- a/apps/deckcraft/src/main.rs +++ b/apps/deckcraft/src/main.rs @@ -430,4 +430,27 @@ mod tests { assert_eq!(mac, win, "macOS and Windows offer different file types"); assert_eq!(mac, linux, "macOS and Linux offer different file types"); } + + /// #1114: a shortcut that names an MSI `` shows a blank icon once an update deletes the + /// cached copy of it, and a taskbar pin (a copy of the shortcut) is never repaired. Shortcuts + /// take the icon embedded in `deckcraft.exe`; only `ARPPRODUCTICON` uses the MSI ``. + #[test] + fn windows_shortcuts_use_the_icon_embedded_in_the_exe() { + let wxs = include_str!("../../../packaging/windows/deckcraft.wxs"); + let shortcuts: Vec<&str> = wxs.split("").next()).collect(); + assert!(!shortcuts.is_empty(), "deckcraft.wxs has no shortcut"); + for shortcut in shortcuts { + assert!(!shortcut.contains("Icon="), "a shortcut names an MSI :{shortcut}"); + } + assert!(wxs.contains(" + + Description="DeckCraft presentations and slide shows" WorkingDirectory="INSTALLFOLDER" Advertise="no" /> diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index 7ebbcbbe..6023a722 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -83,6 +83,15 @@ foreach ($check in @(@('deckcraft.exe', 2), @('deckcraft-cli.exe', 3))) { if ($h.Subsystem -ne $check[1]) { throw "$($check[0]) has PE subsystem $($h.Subsystem), expected $($check[1])" } Write-Output "ok $($check[0]): $Arch, PE subsystem $($h.Subsystem)" } + +# The Start menu shortcut is not advertised and names no MSI icon, so it shows the icon embedded in +# deckcraft.exe (#1114). -SkipBuild takes whatever binary is already there, which may come from a +# build without DECKCRAFT_REQUIRE_WINRES=1 whose resource compilation failed: refuse an exe with no +# icon instead of shipping shortcuts with a generic one. ExtractIconEx with index -1 counts them. +Add-Type -Namespace DeckCraft -Name Shell -MemberDefinition '[DllImport("shell32.dll", CharSet = CharSet.Unicode)] public static extern uint ExtractIconEx(string file, int index, IntPtr[] large, IntPtr[] small, uint icons);' +$Icons = [DeckCraft.Shell]::ExtractIconEx((Join-Path $Bin 'deckcraft.exe'), -1, $null, $null, 0) +if ($Icons -eq 0) { throw 'deckcraft.exe has no embedded icon: rebuild it with DECKCRAFT_REQUIRE_WINRES=1 (omit -SkipBuild)' } +Write-Output "ok deckcraft.exe: $Icons embedded icon group(s)" $Stage = Join-Path $TargetDir "windows-package\$Arch" Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $Stage | Out-Null From 580b577cf2970b4f9697ad880af208a7075eb5b3 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 20:26:28 -0400 Subject: [PATCH 13/19] fix(h264): use Option::as_ref instead of closure (S1612) (#1268) --- crates/h264/src/params.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/h264/src/params.rs b/crates/h264/src/params.rs index 8b36b3ab..2a3b2e3d 100644 --- a/crates/h264/src/params.rs +++ b/crates/h264/src/params.rs @@ -575,7 +575,7 @@ impl Pps { ensure!(sps_id < 32, "seq_parameter_set_id out of range"); let sps = spss .get(sps_id as usize) - .and_then(|s| s.as_ref()) + .and_then(Option::as_ref) .ok_or_else(|| crate::Error::MissingParameterSet(format!("SPS {sps_id} for PPS {id}")))?; let entropy_coding_mode = r.read_flag()?; let bottom_field_pic_order_in_frame_present = r.read_flag()?; From edfee403926f0d291df9c384fdb26ee310ef49bb Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 20:26:33 -0400 Subject: [PATCH 14/19] refactor(av1): split inverse transform into stages and passes (S3776) (#1277) --- crates/av1/src/transform.rs | 50 ++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/crates/av1/src/transform.rs b/crates/av1/src/transform.rs index 40b1d763..65db0b04 100644 --- a/crates/av1/src/transform.rs +++ b/crates/av1/src/transform.rs @@ -165,6 +165,15 @@ pub(crate) fn inverse_dct(t: &mut [T], n: u32, r: u32) { for i in 0..len { t[i] = copy[brev(n, i)]; } + inverse_dct_stage1(t, n, r); + inverse_dct_stage2(t, n, r); + inverse_dct_stage3(t, n, r); + inverse_dct_stage4(t, n, r); + inverse_dct_stage5(t, n, r); + inverse_dct_stage6(t, n, r); +} + +fn inverse_dct_stage1(t: &mut [T], n: u32, r: u32) { if n == 6 { for i in 0..16 { bf(t, 32 + i, 63 - i, 63 - 4 * brev(4, i) as i32, false); @@ -190,6 +199,9 @@ pub(crate) fn inverse_dct(t: &mut [T], n: u32, r: u32) { hd(t, 16 + 2 * i, 17 + 2 * i, i & 1 == 1, r); } } +} + +fn inverse_dct_stage2(t: &mut [T], n: u32, r: u32) { if n == 6 { for i in 0..4 { for j in 0..2 { @@ -221,6 +233,9 @@ pub(crate) fn inverse_dct(t: &mut [T], n: u32, r: u32) { } } } +} + +fn inverse_dct_stage3(t: &mut [T], n: u32, r: u32) { for i in 0..2 { bf(t, 2 * i, 2 * i + 1, 32 + 16 * i as i32, i == 0); } @@ -248,6 +263,9 @@ pub(crate) fn inverse_dct(t: &mut [T], n: u32, r: u32) { } } } +} + +fn inverse_dct_stage4(t: &mut [T], n: u32, r: u32) { for i in 0..2 { hd(t, i, 3 - i, false, r); } @@ -273,6 +291,9 @@ pub(crate) fn inverse_dct(t: &mut [T], n: u32, r: u32) { } } } +} + +fn inverse_dct_stage5(t: &mut [T], n: u32, r: u32) { if n >= 3 { for i in 0..4 { hd(t, i, 7 - i, false, r); @@ -300,6 +321,9 @@ pub(crate) fn inverse_dct(t: &mut [T], n: u32, r: u32) { hd(t, i, 15 - i, false, r); } } +} + +fn inverse_dct_stage6(t: &mut [T], n: u32, r: u32) { if n >= 5 { for i in 0..4 { bf(t, 27 - i, 20 + i, 32, true); @@ -531,10 +555,9 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz: let col_clamp = (bit_depth + 6).max(16); let rk = row_kind(tx_type); let ck = col_kind(tx_type); - let mut t = [0i32; 64]; let rect2 = log2w.abs_diff(log2h) == 1; + let p = Tx2d { w, h, log2w, log2h, row_shift, col_shift, row_clamp, col_clamp, rk, ck, rect2 }; if !lossless && bit_depth <= 10 { - let p = Tx2d { w, h, log2w, log2h, row_shift, col_shift, row_clamp, col_clamp, rk, ck, rect2 }; if w >= 8 && h >= 8 { transform_lanes::<8>(dequant, residual, &p); } else { @@ -542,6 +565,13 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz: } return; } + scalar_row_pass(dequant, residual, &p, lossless); + scalar_col_pass(residual, &p, lossless); +} + +fn scalar_row_pass(dequant: &[i32], residual: &mut [i32], p: &Tx2d, lossless: bool) { + let (w, h, log2w, row_shift, row_clamp, col_clamp, rk, rect2) = (p.w, p.h, p.log2w, p.row_shift, p.row_clamp, p.col_clamp, p.rk, p.rect2); + let mut t = [0i32; 64]; let in_w = w.min(32); for i in 0..h { // Every 1D transform maps an all-zero row to zeros (each step is linear with @@ -572,6 +602,11 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz: residual[i * w + j] = round2(t[j], row_shift).clamp(lo, hi); } } +} + +fn scalar_col_pass(residual: &mut [i32], p: &Tx2d, lossless: bool) { + let (w, h, log2h, col_shift, col_clamp, ck) = (p.w, p.h, p.log2h, p.col_shift, p.col_clamp, p.ck); + let mut t = [0i32; 64]; for j in 0..w { for i in 0..h { t[i] = residual[i * w + j]; @@ -617,11 +652,16 @@ fn transform_1d(t: &mut [T], kind: Kind1d, n: u32, r: u32) { /// The 2D inverse transform with `L` rows (row pass) or columns (column pass) at a time; /// bit-identical to the one-at-a-time process for bit depths up to 10. fn transform_lanes(dequant: &[i32], residual: &mut [i32], p: &Tx2d) { + let mut t = [[0i32; L]; 64]; + lanes_row_pass(dequant, residual, p, &mut t); + lanes_col_pass(residual, p, &mut t); +} + +fn lanes_row_pass(dequant: &[i32], residual: &mut [i32], p: &Tx2d, t: &mut [[i32; L]; 64]) { let (w, h) = (p.w, p.h); let in_w = w.min(32); let lo = -(1i32 << (p.col_clamp - 1)); let hi = (1i32 << (p.col_clamp - 1)) - 1; - let mut t = [[0i32; L]; 64]; let mut i0 = 0; while i0 < h { // Rows without coefficients (and rows 32..63) transform to zeros. @@ -649,6 +689,10 @@ fn transform_lanes(dequant: &[i32], residual: &mut [i32], p: &Tx } i0 += L; } +} + +fn lanes_col_pass(residual: &mut [i32], p: &Tx2d, t: &mut [[i32; L]; 64]) { + let (w, h) = (p.w, p.h); let mut j0 = 0; while j0 < w { for i in 0..h { From aa3a1baa012cb7d92745e3b3816dae427a7a9a3e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 20:26:38 -0400 Subject: [PATCH 15/19] refactor(av1): reduce complexity in cdef and loop restoration (S3776) (#1278) --- crates/av1/src/cdef.rs | 189 ++++++++++++++++++++------------- crates/av1/src/restoration.rs | 191 ++++++++++++++++++++-------------- 2 files changed, 228 insertions(+), 152 deletions(-) diff --git a/crates/av1/src/cdef.rs b/crates/av1/src/cdef.rs index 0a919351..3ea3ee9b 100644 --- a/crates/av1/src/cdef.rs +++ b/crates/av1/src/cdef.rs @@ -45,7 +45,7 @@ fn cdef_block(fs: &FrameState, out: &mut FrameBuf, r: usize, c: usize, idx: usiz let var_str = if (var >> 6) != 0 { (31 - ((var >> 6) as u32).leading_zeros()).min(12) as i32 } else { 0 }; pri = if var != 0 { (pri * (4 + var_str) + 8) >> 4 } else { 0 }; let damping = cp.damping as i32 + coeff_shift as i32; - filter(fs, out, 0, r, c, pri, sec, damping, dir); + filter(fs, out, 0, (r, c), (pri, sec), damping, dir); if fs.num_planes == 1 { return true; } @@ -53,8 +53,8 @@ fn cdef_block(fs: &FrameState, out: &mut FrameBuf, r: usize, c: usize, idx: usiz let sec = (cp.uv_sec[idx] << coeff_shift) as i32; let dir = if pri == 0 { 0 } else { CDEF_UV_DIR[fs.ssx][fs.ssy][y_dir] as usize }; let damping = cp.damping as i32 + coeff_shift as i32 - 1; - filter(fs, out, 1, r, c, pri, sec, damping, dir); - filter(fs, out, 2, r, c, pri, sec, damping, dir); + filter(fs, out, 1, (r, c), (pri, sec), damping, dir); + filter(fs, out, 2, (r, c), (pri, sec), damping, dir); true } @@ -124,33 +124,74 @@ fn constrain(diff: i32, threshold: i32, adj: u32) -> i32 { if diff < 0 { -mag } else { mag } } -#[allow(clippy::too_many_arguments)] -fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, r: usize, c: usize, pri: i32, sec: i32, damping: i32, dir: usize) { - if pri == 0 && sec == 0 { - // Every tap contributes 0 and the clamp to [min, max] keeps x: CdefFrame = CurrFrame. - return; +/// Tap offsets, weights and strengths of one block. +struct Taps { + pri: i32, + sec: i32, + pri_adj: u32, + sec_adj: u32, + pri_off: [isize; 2], + sec_off: [[isize; 2]; 2], + pri_taps: [i32; 2], + sec_taps: [i32; 2], +} + +impl Taps { + fn new(coeff_shift: u32, (pri, sec): (i32, i32), damping: i32, dir: usize) -> Taps { + let pri_tap = ((pri >> coeff_shift) & 1) as usize; + let off = |d: usize, k: usize| CDEF_DIRECTIONS[d][k][0] as isize * PAD_STRIDE as isize + CDEF_DIRECTIONS[d][k][1] as isize; + let pri_off = [off(dir, 0), off(dir, 1)]; + let d_lo = (dir + 6) & 7; + let d_hi = (dir + 2) & 7; + let sec_off = [[off(d_lo, 0), off(d_hi, 0)], [off(d_lo, 1), off(d_hi, 1)]]; + let pri_taps = [CDEF_PRI_TAPS[pri_tap][0] as i32, CDEF_PRI_TAPS[pri_tap][1] as i32]; + let sec_taps = [CDEF_SEC_TAPS[pri_tap][0] as i32, CDEF_SEC_TAPS[pri_tap][1] as i32]; + let adj = |t: i32| if t == 0 { 0 } else { (damping - (31 - (t as u32).leading_zeros()) as i32).max(0) as u32 }; + Taps { pri, sec, pri_adj: adj(pri), sec_adj: adj(sec), pri_off, sec_off, pri_taps, sec_taps } } - let coeff_shift = fs.bit_depth - 8; + + /// The 12 (offset, weight, strength, adjustment) taps in filtering order. + fn tap_list(&self) -> [(isize, i32, i32, u32); 12] { + let mut taps = [(0isize, 0i32, 0i32, 0u32); 12]; + let mut n = 0; + for k in 0..2 { + for sign in [-1isize, 1] { + taps[n] = (sign * self.pri_off[k], self.pri_taps[k], self.pri, self.pri_adj); + taps[n + 1] = (sign * self.sec_off[k][0], self.sec_taps[k], self.sec, self.sec_adj); + taps[n + 2] = (sign * self.sec_off[k][1], self.sec_taps[k], self.sec, self.sec_adj); + n += 3; + } + } + taps + } +} + +/// Position and size of a block within its plane. +#[derive(Clone, Copy)] +struct BlockRect { + x0: usize, + y0: usize, + w: usize, + h: usize, +} + +/// The block with two samples of padding on every side, plus whether every sample is available +/// (CdefAvailable: its 4x4 block is inside the frame). +fn padded_block(fs: &FrameState, plane: usize, src: &Plane, rect: BlockRect) -> ([i32; PAD_STRIDE * PAD_STRIDE], bool) { let (sub_x, sub_y) = if plane > 0 { (fs.ssx, fs.ssy) } else { (0, 0) }; - let x0 = (c * 4) >> sub_x; - let y0 = (r * 4) >> sub_y; - let w = 8 >> sub_x; - let h = 8 >> sub_y; - let src = &fs.cur.planes[plane]; let (mi_rows, mi_cols) = (fs.fh.mi_rows as isize, fs.fh.mi_cols as isize); - // CdefAvailable per sample: its 4x4 block is inside the frame. let mut buf = [CDEF_NA; PAD_STRIDE * PAD_STRIDE]; let mut all_avail = true; - for i in 0..h + 4 { - let y = (y0 + i) as isize - 2; + for i in 0..rect.h + 4 { + let y = (rect.y0 + i) as isize - 2; let cand_r = (y << sub_y) >> 2; if y < 0 || cand_r >= mi_rows { all_avail = false; continue; } let row = src.row(y as usize); - for j in 0..w + 4 { - let x = (x0 + j) as isize - 2; + for j in 0..rect.w + 4 { + let x = (rect.x0 + j) as isize - 2; let cand_c = (x << sub_x) >> 2; if x >= 0 && cand_c < mi_cols { buf[i * PAD_STRIDE + j] = row[x as usize] as i32; @@ -159,70 +200,70 @@ fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, r: usize, c: usize, } } } - let pri_tap = ((pri >> coeff_shift) & 1) as usize; - let off = |d: usize, k: usize| CDEF_DIRECTIONS[d][k][0] as isize * PAD_STRIDE as isize + CDEF_DIRECTIONS[d][k][1] as isize; - let pri_off = [off(dir, 0), off(dir, 1)]; - let d_lo = (dir + 6) & 7; - let d_hi = (dir + 2) & 7; - let sec_off = [[off(d_lo, 0), off(d_hi, 0)], [off(d_lo, 1), off(d_hi, 1)]]; - let pri_taps = [CDEF_PRI_TAPS[pri_tap][0] as i32, CDEF_PRI_TAPS[pri_tap][1] as i32]; - let sec_taps = [CDEF_SEC_TAPS[pri_tap][0] as i32, CDEF_SEC_TAPS[pri_tap][1] as i32]; - let adj = |t: i32| if t == 0 { 0 } else { (damping - (31 - (t as u32).leading_zeros()) as i32).max(0) as u32 }; - let (pri_adj, sec_adj) = (adj(pri), adj(sec)); + (buf, all_avail) +} + +/// Filters one sample whose taps may be unavailable (CDEF_NA). +fn filter_sample(buf: &[i32; PAD_STRIDE * PAD_STRIDE], taps: &Taps, ci: isize, x: i32) -> u16 { + let mut sum = 0i32; + let mut max = x; + let mut min = x; + for k in 0..2 { + for sign in [-1isize, 1] { + let p = buf[(ci + sign * taps.pri_off[k]) as usize]; + if p != CDEF_NA { + if taps.pri != 0 { + sum += taps.pri_taps[k] * constrain(p - x, taps.pri, taps.pri_adj); + } + max = max.max(p); + min = min.min(p); + } + for s in 0..2 { + let q = buf[(ci + sign * taps.sec_off[k][s]) as usize]; + if q != CDEF_NA { + if taps.sec != 0 { + sum += taps.sec_taps[k] * constrain(q - x, taps.sec, taps.sec_adj); + } + max = max.max(q); + min = min.min(q); + } + } + } + } + (x + ((8 + sum - (sum < 0) as i32) >> 4)).clamp(min, max) as u16 +} + +fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, (r, c): (usize, usize), strengths: (i32, i32), damping: i32, dir: usize) { + let (pri, sec) = strengths; + if pri == 0 && sec == 0 { + // Every tap contributes 0 and the clamp to [min, max] keeps x: CdefFrame = CurrFrame. + return; + } + let coeff_shift = fs.bit_depth - 8; + let (sub_x, sub_y) = if plane > 0 { (fs.ssx, fs.ssy) } else { (0, 0) }; + let rect = BlockRect { x0: (c * 4) >> sub_x, y0: (r * 4) >> sub_y, w: 8 >> sub_x, h: 8 >> sub_y }; + let src = &fs.cur.planes[plane]; + let (buf, all_avail) = padded_block(fs, plane, src, rect); + let taps = Taps::new(coeff_shift, strengths, damping, dir); let dst = &mut out.planes[plane]; if all_avail { // Every tap is available: whole rows at a time (constrain() with threshold 0 is 0). - let mut taps = [(0isize, 0i32, 0i32, 0u32); 12]; - let mut n = 0; - for k in 0..2 { - for sign in [-1isize, 1] { - taps[n] = (sign * pri_off[k], pri_taps[k], pri, pri_adj); - taps[n + 1] = (sign * sec_off[k][0], sec_taps[k], sec, sec_adj); - taps[n + 2] = (sign * sec_off[k][1], sec_taps[k], sec, sec_adj); - n += 3; - } - } - let rows = (y0, x0, h); - match w { - 8 => filter_rows::<8>(&buf, &taps, dst, rows), - _ => filter_rows::<4>(&buf, &taps, dst, rows), + let list = taps.tap_list(); + let rows = (rect.y0, rect.x0, rect.h); + match rect.w { + 8 => filter_rows::<8>(&buf, &list, dst, rows), + _ => filter_rows::<4>(&buf, &list, dst, rows), } return; } - for i in 0..h { + for i in 0..rect.h { // The centre sample is read even where CdefAvailable is 0 (8x8 blocks straddling an odd // MiRows / MiCols edge). - let srow = &src.row(y0 + i)[x0..x0 + w]; - let drow = &mut dst.row_mut(y0 + i)[x0..x0 + w]; - for j in 0..w { + let srow = &src.row(rect.y0 + i)[rect.x0..rect.x0 + rect.w]; + let drow = &mut dst.row_mut(rect.y0 + i)[rect.x0..rect.x0 + rect.w]; + for j in 0..rect.w { let ci = ((i + 2) * PAD_STRIDE + j + 2) as isize; - let x = srow[j] as i32; - let mut sum = 0i32; - let mut max = x; - let mut min = x; - for k in 0..2 { - for sign in [-1isize, 1] { - let p = buf[(ci + sign * pri_off[k]) as usize]; - if p != CDEF_NA { - if pri != 0 { - sum += pri_taps[k] * constrain(p - x, pri, pri_adj); - } - max = max.max(p); - min = min.min(p); - } - for s in 0..2 { - let q = buf[(ci + sign * sec_off[k][s]) as usize]; - if q != CDEF_NA { - if sec != 0 { - sum += sec_taps[k] * constrain(q - x, sec, sec_adj); - } - max = max.max(q); - min = min.min(q); - } - } - } - } - drow[j] = (x + ((8 + sum - (sum < 0) as i32) >> 4)).clamp(min, max) as u16; + drow[j] = filter_sample(&buf, &taps, ci, srow[j] as i32); } } } diff --git a/crates/av1/src/restoration.rs b/crates/av1/src/restoration.rs index 0b829733..b31f982b 100644 --- a/crates/av1/src/restoration.rs +++ b/crates/av1/src/restoration.rs @@ -73,57 +73,70 @@ impl LrCtx<'_> { } } +/// Position and size of a (at most 4x4) restoration block within its plane. +#[derive(Clone, Copy)] +struct LrBlock { + x: i32, + y: i32, + w: i32, + h: i32, +} + /// Loop restoration (7.17): returns LrFrame given UpscaledCurrFrame and UpscaledCdefFrame. pub(crate) fn loop_restoration(fs: &FrameState, cur: &FrameBuf, cdef: &FrameBuf) -> FrameBuf { - let fh = &fs.fh; let mut lr = cdef.clone(); - let up_w = fh.upscaled_width as i32; - let fr_h = fh.frame_height as i32; - let bd = fs.bit_depth; for plane in 0..fs.num_planes { - let rtype_frame = fh.lr.frame_restoration_type[plane]; - if rtype_frame == RESTORE_NONE as u8 { + if fs.fh.lr.frame_restoration_type[plane] == RESTORE_NONE as u8 { continue; } - let (sub_x, sub_y) = if plane > 0 { (fs.ssx as i32, fs.ssy as i32) } else { (0, 0) }; - let unit_size = fh.lr.loop_restoration_size[plane] as i32; - let unit_rows = fs.lr_unit_rows[plane] as i32; - let unit_cols = fs.lr_unit_cols[plane] as i32; - let plane_end_x = ((up_w + sub_x) >> sub_x) - 1; - let plane_end_y = ((fr_h + sub_y) >> sub_y) - 1; - let mut row = 0; - while row * 4 < fr_h { - let luma_y = row * 4; - let stripe_num = (luma_y + 8) / 64; - let stripe_start_y = (-8 + stripe_num * 64) >> sub_y; - let stripe_end_y = stripe_start_y + (64 >> sub_y) - 1; - let ctx = LrCtx { cur: &cur.planes[plane], cdef: &cdef.planes[plane], plane_end_x, plane_end_y, stripe_start_y, stripe_end_y }; - let unit_row = (unit_rows - 1).min(((row * 4 + 8) >> sub_y) / unit_size); - let mut col = 0; - while col * 4 < up_w { - let unit_col = (unit_cols - 1).min(((col * 4) >> sub_x) / unit_size); - let x = (col * 4) >> sub_x; - let y = (row * 4) >> sub_y; - let w = (4 >> sub_x).min(plane_end_x - x + 1); - let h = (4 >> sub_y).min(plane_end_y - y + 1); - if w > 0 && h > 0 { - let ui = (unit_row * unit_cols + unit_col) as usize; - match fs.lr.lr_type[plane][ui] { - t if t == RESTORE_WIENER as u8 => wiener(fs, &ctx, &mut lr.planes[plane], plane, ui, x, y, w, h, bd), - t if t == RESTORE_SGRPROJ as u8 => self_guided(fs, &ctx, &mut lr.planes[plane], plane, ui, x, y, w, h, bd), - _ => {} - } - } - col += 1; - } - row += 1; - } + restore_plane(fs, cur, cdef, &mut lr.planes[plane], plane); } lr } -#[allow(clippy::too_many_arguments)] -fn wiener(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, plane: usize, ui: usize, x: i32, y: i32, w: i32, h: i32, bd: u32) { +fn restore_plane(fs: &FrameState, cur: &FrameBuf, cdef: &FrameBuf, out: &mut Plane, plane: usize) { + let fh = &fs.fh; + let up_w = fh.upscaled_width as i32; + let fr_h = fh.frame_height as i32; + let bd = fs.bit_depth; + let (sub_x, sub_y) = if plane > 0 { (fs.ssx as i32, fs.ssy as i32) } else { (0, 0) }; + let unit_size = fh.lr.loop_restoration_size[plane] as i32; + let unit_rows = fs.lr_unit_rows[plane] as i32; + let unit_cols = fs.lr_unit_cols[plane] as i32; + let plane_end_x = ((up_w + sub_x) >> sub_x) - 1; + let plane_end_y = ((fr_h + sub_y) >> sub_y) - 1; + let mut row = 0; + while row * 4 < fr_h { + let luma_y = row * 4; + let stripe_num = (luma_y + 8) / 64; + let stripe_start_y = (-8 + stripe_num * 64) >> sub_y; + let stripe_end_y = stripe_start_y + (64 >> sub_y) - 1; + let ctx = LrCtx { cur: &cur.planes[plane], cdef: &cdef.planes[plane], plane_end_x, plane_end_y, stripe_start_y, stripe_end_y }; + let unit_row = (unit_rows - 1).min(((row * 4 + 8) >> sub_y) / unit_size); + let mut col = 0; + while col * 4 < up_w { + let unit_col = (unit_cols - 1).min(((col * 4) >> sub_x) / unit_size); + let x = (col * 4) >> sub_x; + let y = (row * 4) >> sub_y; + let w = (4 >> sub_x).min(plane_end_x - x + 1); + let h = (4 >> sub_y).min(plane_end_y - y + 1); + if w > 0 && h > 0 { + let ui = (unit_row * unit_cols + unit_col) as usize; + let blk = LrBlock { x, y, w, h }; + match fs.lr.lr_type[plane][ui] { + t if t == RESTORE_WIENER as u8 => wiener(fs, &ctx, out, (plane, ui), blk, bd), + t if t == RESTORE_SGRPROJ as u8 => self_guided(fs, &ctx, out, (plane, ui), blk, bd), + _ => {} + } + } + col += 1; + } + row += 1; + } +} + +fn wiener(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, (plane, ui): (usize, usize), blk: LrBlock, bd: u32) { + let LrBlock { x, y, w, h } = blk; let round0: u32 = if bd == 12 { 5 } else { 3 }; let round1: u32 = if bd == 12 { 9 } else { 11 }; let coef = &fs.lr.lr_wiener[plane][ui]; @@ -166,13 +179,13 @@ fn wiener(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, plane: usize, ui: usize } } -#[allow(clippy::too_many_arguments)] -fn self_guided(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, plane: usize, ui: usize, x: i32, y: i32, w: i32, h: i32, bd: u32) { +fn self_guided(fs: &FrameState, ctx: &LrCtx, out: &mut Plane, (plane, ui): (usize, usize), blk: LrBlock, bd: u32) { + let LrBlock { x, y, w, h } = blk; let set = fs.lr.lr_sgr_set[plane][ui] as usize; let mut flt0 = [[0i32; 4]; 4]; let mut flt1 = [[0i32; 4]; 4]; - box_filter(ctx, x, y, w, h, set, 0, bd, &mut flt0); - box_filter(ctx, x, y, w, h, set, 1, bd, &mut flt1); + box_filter(ctx, blk, (set, 0), bd, &mut flt0); + box_filter(ctx, blk, (set, 1), bd, &mut flt1); let xqd = fs.lr.lr_sgr_xqd[plane][ui]; let w0 = xqd[0] as i32; let w1 = xqd[1] as i32; @@ -198,8 +211,56 @@ fn round2_64(x: i64, n: u32) -> i64 { if n == 0 { x } else { (x + (1i64 << (n - 1))) >> n } } -#[allow(clippy::too_many_arguments)] -fn box_filter(ctx: &LrCtx, x: i32, y: i32, w: i32, h: i32, set: usize, pass: usize, bd: u32, f: &mut [[i32; 4]; 4]) { +/// Box sums of the samples within `r` of (x, y): (sum of squares, sum). +fn box_sums(ctx: &LrCtx, x: i32, y: i32, r: i32) -> (i64, i64) { + let mut a = 0i64; + let mut b = 0i64; + for dy in -r..=r { + for dx in -r..=r { + let c = ctx.sample(x + dx, y + dy) as i64; + a += c * c; + b += c; + } + } + (a, b) +} + +/// Self-guided filter constants of one pass. +struct SgrConsts { + n: i64, + s: i64, + one_over_n: i64, +} + +/// (A, B) coefficients of one sample from its box sums. +fn sgr_coefficients(a: i64, b: i64, k: &SgrConsts, bd: u32) -> (i64, i64) { + let a = round2_64(a, 2 * (bd - 8)); + let d = round2_64(b, bd - 8); + let p = (a * k.n - d * d).max(0); + let z = round2_64(p * k.s, SGRPROJ_MTABLE_BITS as u32); + let a2 = if z >= 255 { + 256 + } else if z == 0 { + 1 + } else { + ((z << SGRPROJ_SGR_BITS) + z / 2) / (z + 1) + }; + let b2 = ((1i64 << SGRPROJ_SGR_BITS) - a2) * b * k.one_over_n; + (a2, round2_64(b2, SGRPROJ_RECIP_BITS as u32)) +} + +fn box_weight(pass: usize, i: i32, dx: i32, dy: i32) -> i64 { + if pass == 0 { + if ((i + dy) & 1) != 0 { if dx == 0 { 6 } else { 5 } } else { 0 } + } else if dx == 0 || dy == 0 { + 4 + } else { + 3 + } +} + +fn box_filter(ctx: &LrCtx, blk: LrBlock, (set, pass): (usize, usize), bd: u32, f: &mut [[i32; 4]; 4]) { + let LrBlock { x, y, w, h } = blk; let r = SGR_PARAMS[set][pass * 2] as i32; if r == 0 { return; @@ -207,35 +268,15 @@ fn box_filter(ctx: &LrCtx, x: i32, y: i32, w: i32, h: i32, set: usize, pass: usi let eps = SGR_PARAMS[set][pass * 2 + 1] as i64; let n = ((2 * r + 1) * (2 * r + 1)) as i64; let n2e = n * n * eps; - let s = ((1i64 << SGRPROJ_MTABLE_BITS) + n2e / 2) / n2e; - let one_over_n = ((1i64 << SGRPROJ_RECIP_BITS) + n / 2) / n; + let k = SgrConsts { n, s: ((1i64 << SGRPROJ_MTABLE_BITS) + n2e / 2) / n2e, one_over_n: ((1i64 << SGRPROJ_RECIP_BITS) + n / 2) / n }; let mut a_arr = [[0i64; 6]; 6]; let mut b_arr = [[0i64; 6]; 6]; for i in -1..h + 1 { for j in -1..w + 1 { - let mut a = 0i64; - let mut b = 0i64; - for dy in -r..=r { - for dx in -r..=r { - let c = ctx.sample(x + j + dx, y + i + dy) as i64; - a += c * c; - b += c; - } - } - let a = round2_64(a, 2 * (bd - 8)); - let d = round2_64(b, bd - 8); - let p = (a * n - d * d).max(0); - let z = round2_64(p * s, SGRPROJ_MTABLE_BITS as u32); - let a2 = if z >= 255 { - 256 - } else if z == 0 { - 1 - } else { - ((z << SGRPROJ_SGR_BITS) + z / 2) / (z + 1) - }; - let b2 = ((1i64 << SGRPROJ_SGR_BITS) - a2) * b * one_over_n; + let (a, b) = box_sums(ctx, x + j, y + i, r); + let (a2, b2) = sgr_coefficients(a, b, &k, bd); a_arr[(i + 1) as usize][(j + 1) as usize] = a2; - b_arr[(i + 1) as usize][(j + 1) as usize] = round2_64(b2, SGRPROJ_RECIP_BITS as u32); + b_arr[(i + 1) as usize][(j + 1) as usize] = b2; } } for i in 0..h { @@ -245,13 +286,7 @@ fn box_filter(ctx: &LrCtx, x: i32, y: i32, w: i32, h: i32, set: usize, pass: usi let mut b = 0i64; for dy in -1..=1i32 { for dx in -1..=1i32 { - let weight = if pass == 0 { - if ((i + dy) & 1) != 0 { if dx == 0 { 6 } else { 5 } } else { 0 } - } else if dx == 0 || dy == 0 { - 4 - } else { - 3 - }; + let weight = box_weight(pass, i, dx, dy); a += weight * a_arr[(i + dy + 1) as usize][(j + dx + 1) as usize]; b += weight * b_arr[(i + dy + 1) as usize][(j + dx + 1) as usize]; } From c93e0a0bacd5122ecf408360c707f92043bc9a13 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 20:55:30 -0400 Subject: [PATCH 16/19] refactor(rtf): split the outline reader into per-construct methods (S3776) (#1399) --- crates/format/src/rtf.rs | 341 +++++++++++++++++++++++---------------- 1 file changed, 201 insertions(+), 140 deletions(-) diff --git a/crates/format/src/rtf.rs b/crates/format/src/rtf.rs index f3edcb97..4800aba2 100644 --- a/crates/format/src/rtf.rs +++ b/crates/format/src/rtf.rs @@ -126,155 +126,216 @@ fn advance(rtf: &str, i: usize, n: usize) -> usize { if rtf.is_char_boundary(i + n) { i + n } else { i + rtf.get(i..).and_then(|r| r.chars().next()).map_or(1, char::len_utf8) } } +/// Group state: whether the text is being skipped, and the `\uc` fallback count. +#[derive(Clone, Copy)] +struct St { + skip: bool, + uc: usize, +} + +/// Parser state for [`rtf_to_outline`]: the cursor, the group stack and the outline so far. +struct Outline<'a> { + rtf: &'a str, + b: &'a [u8], + i: usize, + stack: Vec, + st: St, + level: Option, + indent: i64, + pending_skip: usize, + line: String, + out: String, +} + /// Read an RTF outline into tab-indented outline text: a paragraph's outline level (or its left /// indent in 0.25" steps) becomes its number of tabs. pub fn rtf_to_outline(rtf: &str) -> String { - #[derive(Clone, Copy)] - struct St { - skip: bool, - uc: usize, - } - let b = rtf.as_bytes(); - let mut stack: Vec = vec![]; - let mut st = St { skip: false, uc: 1 }; - let (mut level, mut indent): (Option, i64) = (None, 0); - let mut line = String::new(); - let mut out = String::new(); - let mut pending_skip = 0usize; - let mut i = 0; - let flush = |line: &mut String, level: Option, indent: i64, out: &mut String| { - let text = line.trim(); - if !text.is_empty() { - let n = level.map(|l| l as i64).unwrap_or(indent.saturating_add(180) / 360).clamp(0, 9) as usize; - out.push_str(&"\t".repeat(n)); - out.push_str(text); - out.push('\n'); - } - line.clear(); + let mut p = Outline { + rtf, + b: rtf.as_bytes(), + i: 0, + stack: vec![], + st: St { skip: false, uc: 1 }, + level: None, + indent: 0, + pending_skip: 0, + line: String::new(), + out: String::new(), }; - while i < b.len() { - let c = b[i]; - match c { - b'{' => { - stack.push(st); - i += 1; - // `{\*\dest …}`: an ignorable destination. - if b[i..].starts_with(b"\\*") { - st.skip = true; - } - } - b'}' => { - st = stack.pop().unwrap_or(St { skip: false, uc: 1 }); - i += 1; - } - b'\\' => { - i += 1; - let Some(&n) = b.get(i) else { break }; - if n.is_ascii_alphabetic() { - let s = i; - while i < b.len() && b[i].is_ascii_alphabetic() { - i += 1; - } - let word = &rtf[s..i]; - let ns = i; - if i < b.len() && (b[i] == b'-' || b[i].is_ascii_digit()) { - i += 1; - while i < b.len() && b[i].is_ascii_digit() { - i += 1; - } - } - let arg: Option = rtf[ns..i].parse().ok(); - if i < b.len() && b[i] == b' ' { - i += 1; - } - if pending_skip > 0 { - pending_skip -= 1; - continue; - } - if SKIP.contains(&word) { - st.skip = true; - } - if st.skip { - continue; - } - match word { - "par" => { - flush(&mut line, level, indent, &mut out); - } - "pard" => { - level = None; - indent = 0; - } - "outlinelevel" => level = arg.map(|a| a.clamp(0, 9) as u32), - "li" => indent = arg.unwrap_or(0), - "tab" | "line" => line.push(' '), - "uc" => st.uc = arg.unwrap_or(1).clamp(0, 8) as usize, - "u" => { - let v = arg.unwrap_or(0); - // RTF writes UTF-16 units as signed 16-bit values. - push_utf16(&mut line, (v & 0xffff) as u32); - pending_skip = 0; - // Skip the fallback characters. - let mut k = st.uc; - while k > 0 && i < b.len() { - if b[i] == b'\\' && b.get(i + 1) == Some(&b'\'') { - i = advance(rtf, i, 4); - } else if b[i] == b'{' || b[i] == b'}' || b[i] == b'\\' { - break; - } else { - i = advance(rtf, i, 1); - } - k -= 1; - } - } - "emdash" => line.push('—'), - "endash" => line.push('–'), - "bullet" => line.push('•'), - "lquote" => line.push('‘'), - "rquote" => line.push('’'), - "ldblquote" => line.push('“'), - "rdblquote" => line.push('”'), - _ => {} - } - } else if n == b'\'' { - let hex = rtf.get(i + 1..i + 3).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u8::from_str_radix(h, 16).ok()); - i = advance(rtf, i, 3); - if !st.skip - && let Some(h) = hex - && h != 0 - { - line.push(cp1252(h)); - } - } else { - i += 1; - if st.skip { - continue; - } - match n { - b'\\' | b'{' | b'}' => line.push(n as char), - b'~' => line.push('\u{a0}'), - b'_' => line.push('\u{2011}'), - b'\n' | b'\r' => flush(&mut line, level, indent, &mut out), - _ => {} + p.run(); + p.flush(); + finish_surrogates(p.out) +} + +impl Outline<'_> { + fn run(&mut self) { + while self.i < self.b.len() { + match self.b[self.i] { + b'{' => self.open_group(), + b'}' => self.close_group(), + b'\\' => { + self.i += 1; + if !self.escape() { + break; } } - } - b'\r' | b'\n' => i += 1, - _ => { - let s = i; - while i < b.len() && !matches!(b[i], b'{' | b'}' | b'\\' | b'\r' | b'\n') { - i += 1; - } - if !st.skip - && let Some(t) = rtf.get(s..i) - { - line.push_str(&t.replace('\u{0}', "")); - } + b'\r' | b'\n' => self.i += 1, + _ => self.text(), } } } - flush(&mut line, level, indent, &mut out); - finish_surrogates(out) + + /// `{`: push the group; `{\*\dest …}` is an ignorable destination. + fn open_group(&mut self) { + self.stack.push(self.st); + self.i += 1; + if self.b[self.i..].starts_with(b"\\*") { + self.st.skip = true; + } + } + + fn close_group(&mut self) { + self.st = self.stack.pop().unwrap_or(St { skip: false, uc: 1 }); + self.i += 1; + } + + /// The character after a backslash: a control word, a hex escape or a symbol. False at the + /// end of the input. + fn escape(&mut self) -> bool { + let Some(&n) = self.b.get(self.i) else { return false }; + if n.is_ascii_alphabetic() { + self.control_word(); + } else if n == b'\'' { + self.hex_escape(); + } else { + self.symbol(n); + } + true + } + + fn control_word(&mut self) { + let rtf = self.rtf; + let s = self.i; + while self.i < self.b.len() && self.b[self.i].is_ascii_alphabetic() { + self.i += 1; + } + let word = &rtf[s..self.i]; + let ns = self.i; + if self.i < self.b.len() && (self.b[self.i] == b'-' || self.b[self.i].is_ascii_digit()) { + self.i += 1; + while self.i < self.b.len() && self.b[self.i].is_ascii_digit() { + self.i += 1; + } + } + let arg: Option = rtf[ns..self.i].parse().ok(); + if self.i < self.b.len() && self.b[self.i] == b' ' { + self.i += 1; + } + if self.pending_skip > 0 { + self.pending_skip -= 1; + return; + } + if SKIP.contains(&word) { + self.st.skip = true; + } + if !self.st.skip { + self.apply(word, arg); + } + } + + fn apply(&mut self, word: &str, arg: Option) { + match word { + "par" => self.flush(), + "pard" => { + self.level = None; + self.indent = 0; + } + "outlinelevel" => self.level = arg.map(|a| a.clamp(0, 9) as u32), + "li" => self.indent = arg.unwrap_or(0), + "tab" | "line" => self.line.push(' '), + "uc" => self.st.uc = arg.unwrap_or(1).clamp(0, 8) as usize, + "u" => self.unicode(arg.unwrap_or(0)), + "emdash" => self.line.push('—'), + "endash" => self.line.push('–'), + "bullet" => self.line.push('•'), + "lquote" => self.line.push('‘'), + "rquote" => self.line.push('’'), + "ldblquote" => self.line.push('“'), + "rdblquote" => self.line.push('”'), + _ => {} + } + } + + /// `\uN`: one UTF-16 unit, then the `\uc` fallback characters, which are skipped. + fn unicode(&mut self, v: i64) { + // RTF writes UTF-16 units as signed 16-bit values. + push_utf16(&mut self.line, (v & 0xffff) as u32); + self.pending_skip = 0; + let mut k = self.st.uc; + while k > 0 && self.i < self.b.len() { + if self.b[self.i] == b'\\' && self.b.get(self.i + 1) == Some(&b'\'') { + self.i = advance(self.rtf, self.i, 4); + } else if matches!(self.b[self.i], b'{' | b'}' | b'\\') { + break; + } else { + self.i = advance(self.rtf, self.i, 1); + } + k -= 1; + } + } + + /// `\'hh`: one cp1252 byte. + fn hex_escape(&mut self) { + let hex = + self.rtf.get(self.i + 1..self.i + 3).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u8::from_str_radix(h, 16).ok()); + self.i = advance(self.rtf, self.i, 3); + if !self.st.skip + && let Some(h) = hex + && h != 0 + { + self.line.push(cp1252(h)); + } + } + + /// A one-character control symbol (`\\`, `\{`, `\~`, ...). + fn symbol(&mut self, n: u8) { + self.i += 1; + if self.st.skip { + return; + } + match n { + b'\\' | b'{' | b'}' => self.line.push(n as char), + b'~' => self.line.push('\u{a0}'), + b'_' => self.line.push('\u{2011}'), + b'\n' | b'\r' => self.flush(), + _ => {} + } + } + + /// Plain text up to the next special character. + fn text(&mut self) { + let s = self.i; + while self.i < self.b.len() && !matches!(self.b[self.i], b'{' | b'}' | b'\\' | b'\r' | b'\n') { + self.i += 1; + } + if !self.st.skip + && let Some(t) = self.rtf.get(s..self.i) + { + self.line.push_str(&t.replace('\u{0}', "")); + } + } + + /// Ends the current line: a non-empty line becomes one outline entry, indented by its level. + fn flush(&mut self) { + let text = self.line.trim(); + if !text.is_empty() { + let n = self.level.map(|l| l as i64).unwrap_or(self.indent.saturating_add(180) / 360).clamp(0, 9) as usize; + self.out.push_str(&"\t".repeat(n)); + self.out.push_str(text); + self.out.push('\n'); + } + self.line.clear(); + } } /// Push a UTF-16 unit; surrogate halves are parked as private markers (NUL plus four hex digits) From 430d0a91c0f4c82f4fe27346968568c59307c7bc Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 20:55:35 -0400 Subject: [PATCH 17/19] refactor(hevc): split prediction_unit into merge, AMVP and store helpers (S3776) (#1400) --- crates/hevc/src/slicedec.rs | 150 +++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 64 deletions(-) diff --git a/crates/hevc/src/slicedec.rs b/crates/hevc/src/slicedec.rs index 99a7026f..5bf9f07b 100644 --- a/crates/hevc/src/slicedec.rs +++ b/crates/hevc/src/slicedec.rs @@ -1366,84 +1366,106 @@ impl<'a> SliceDecoder<'a> { /// prediction_unit(): parse, derive motion, store it and predict. Returns merge_flag. fn prediction_unit(&mut self, g: PbGeom, part: PartMode, skip: bool) -> Result { - let PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx } = g; let merge = if skip { true } else { self.dd(MERGE_FLAG) == 1 }; let mvf = if merge { - let idx = if self.sh.max_num_merge_cand > 1 { - let mut i = 0; - if self.dd(MERGE_IDX) == 1 { - i = 1; - while i < self.sh.max_num_merge_cand - 1 && self.bypass() == 1 { - i += 1; - } - } - i - } else { - 0 - }; - self.derive_merge(PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }, part, idx as usize) + let idx = self.merge_idx(); + self.derive_merge(g, part, idx as usize) } else { - let pred_idc = if self.sh.is_b() { - if w + h != 12 { - let depth = self.pic.blk_at(xc, yc).depth as usize; - if self.dd(INTER_PRED_IDC + depth) == 1 { 2 } else { self.dd(INTER_PRED_IDC + 4) } - } else { - self.dd(INTER_PRED_IDC + 4) - } - } else { - 0 - }; - let mut ref_idx = [-1i8; 2]; - let mut mvd = [[0i32; 2]; 2]; - let mut mvp_flag = [0u32; 2]; - for l in 0..2 { - let used = if l == 0 { pred_idc != 1 } else { pred_idc != 0 }; - if !used { - continue; - } - let nref = self.sh.num_ref_idx[l]; - let mut ri = 0; - if nref > 1 { - while ri < nref - 1 { - let bin = if ri < 2 { self.dd(REF_IDX + ri as usize) } else { self.bypass() }; - if bin == 0 { - break; - } - ri += 1; - } - } - ref_idx[l] = ri as i8; - if l == 1 && self.sh.mvd_l1_zero && pred_idc == 2 { - mvd[1] = [0, 0]; - } else { - mvd[l] = self.mvd_coding()?; - } - mvp_flag[l] = self.dd(MVP_FLAG); - } - let mut f = MvField { mv: [[0; 2]; 2], ref_idx }; - for l in 0..2 { - if ref_idx[l] < 0 { - continue; - } - let mvp = self.derive_amvp(PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }, l, ref_idx[l] as usize, mvp_flag[l] as usize); - // mvLX = (mvp + mvd) wrapped to 16 bits (8-94) - f.mv[l] = [(mvp[0] as i32 + mvd[l][0]) as i16, (mvp[1] as i32 + mvd[l][1]) as i16]; - } - f + self.amvp_motion(g)? }; for l in 0..2 { if mvf.ref_idx[l] >= 0 { ensure!((mvf.ref_idx[l] as usize) < self.refs[l].len(), "reference index out of range"); } } - // store motion + self.store_motion(g.xp, g.yp, g.w, g.h, mvf); + self.motion_compensate(g.xp, g.yp, g.w as usize, g.h as usize, &mvf); + Ok(merge) + } + + /// merge_idx(): the merge candidate index, truncated unary (0 when there is one candidate). + fn merge_idx(&mut self) -> u32 { + if self.sh.max_num_merge_cand <= 1 { + return 0; + } + let mut i = 0; + if self.dd(MERGE_IDX) == 1 { + i = 1; + while i < self.sh.max_num_merge_cand - 1 && self.bypass() == 1 { + i += 1; + } + } + i + } + + /// inter_pred_idc: 0 = list 0, 1 = list 1, 2 = both. + fn inter_pred_idc(&mut self, xc: i32, yc: i32, w: i32, h: i32) -> u32 { + if !self.sh.is_b() { + return 0; + } + if w + h != 12 { + let depth = self.pic.blk_at(xc, yc).depth as usize; + if self.dd(INTER_PRED_IDC + depth) == 1 { 2 } else { self.dd(INTER_PRED_IDC + 4) } + } else { + self.dd(INTER_PRED_IDC + 4) + } + } + + /// ref_idx_lX: truncated unary, the first two bins context-coded. + fn ref_idx(&mut self, l: usize) -> u32 { + let nref = self.sh.num_ref_idx[l]; + let mut ri = 0; + if nref > 1 { + while ri < nref - 1 { + let bin = if ri < 2 { self.dd(REF_IDX + ri as usize) } else { self.bypass() }; + if bin == 0 { + break; + } + ri += 1; + } + } + ri + } + + /// The AMVP branch: parse each used list's reference index, mvd and predictor flag, then add + /// every mvd to its predictor. + fn amvp_motion(&mut self, g: PbGeom) -> Result { + let pred_idc = self.inter_pred_idc(g.xc, g.yc, g.w, g.h); + let mut ref_idx = [-1i8; 2]; + let mut mvd = [[0i32; 2]; 2]; + let mut mvp_flag = [0u32; 2]; + for l in 0..2 { + let used = if l == 0 { pred_idc != 1 } else { pred_idc != 0 }; + if !used { + continue; + } + ref_idx[l] = self.ref_idx(l) as i8; + if l == 1 && self.sh.mvd_l1_zero && pred_idc == 2 { + mvd[1] = [0, 0]; + } else { + mvd[l] = self.mvd_coding()?; + } + mvp_flag[l] = self.dd(MVP_FLAG); + } + let mut f = MvField { mv: [[0; 2]; 2], ref_idx }; + for l in 0..2 { + if ref_idx[l] < 0 { + continue; + } + let mvp = self.derive_amvp(g, l, ref_idx[l] as usize, mvp_flag[l] as usize); + // mvLX = (mvp + mvd) wrapped to 16 bits (8-94) + f.mv[l] = [(mvp[0] as i32 + mvd[l][0]) as i16, (mvp[1] as i32 + mvd[l][1]) as i16]; + } + Ok(f) + } + + /// Writes a prediction block's motion into the picture's 4x4 motion field. + fn store_motion(&mut self, xp: i32, yp: i32, w: i32, h: i32, mvf: MvField) { let w4 = self.pic.w4; for by in (yp >> 2)..((yp + h) >> 2) { let o = by as usize * w4; self.pic.mvf[o + (xp >> 2) as usize..o + ((xp + w) >> 2) as usize].fill(mvf); } - self.motion_compensate(xp, yp, w as usize, h as usize, &mvf); - Ok(merge) } fn mvd_coding(&mut self) -> Result<[i32; 2]> { From 16fe4ebc65c2ef191aa21e42442c5dd39b3a8a80 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 20:55:39 -0400 Subject: [PATCH 18/19] refactor(hevc): split derive_merge into spatial, temporal, combined and zero stages (S3776) (#1401) --- crates/hevc/src/mvpred.rs | 85 ++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/crates/hevc/src/mvpred.rs b/crates/hevc/src/mvpred.rs index 49a37ec6..236c14de 100644 --- a/crates/hevc/src/mvpred.rs +++ b/crates/hevc/src/mvpred.rs @@ -74,16 +74,39 @@ impl<'a> SliceDecoder<'a> { /// Merge mode motion (8.5.3.2.2). pub fn derive_merge(&self, g: PbGeom, part: PartMode, merge_idx: usize) -> MvField { - let PbGeom { xc, yc, ncb, xp: xp0, yp: yp0, w: w0, h: h0, part_idx: part_idx0 } = g; + let (w0, h0) = (g.w, g.h); + // With a parallel merge level above 4x4, an 8x8 coding block shares one candidate set. + let g = if self.pps.log2_parallel_merge_level > 2 && g.ncb == 8 { + PbGeom { xp: g.xc, yp: g.yc, w: g.ncb, h: g.ncb, part_idx: 0, ..g } + } else { + g + }; + let max = self.sh.max_num_merge_cand as usize; + let (list, n) = self.spatial_merge_candidates(&g, part); + if merge_idx < n { + return self.finish_merge(list[merge_idx], w0, h0); + } + let mut cands: Vec = list[..n].to_vec(); + cands.extend(self.temporal_merge_candidate(g.xp, g.yp, g.w, g.h)); + if merge_idx < cands.len() { + return self.finish_merge(cands[merge_idx], w0, h0); + } + self.combined_merge_candidates(&mut cands, max); + if merge_idx < cands.len() { + return self.finish_merge(cands[merge_idx], w0, h0); + } + self.zero_merge_candidates(&mut cands, merge_idx); + self.finish_merge(cands[merge_idx], w0, h0) + } + + /// The spatial candidates A1, B1, B0, A0 and B2 (8.5.3.2.3): the candidates, and how many. + fn spatial_merge_candidates(&self, g: &PbGeom, part: PartMode) -> ([MvField; 5], usize) { + let PbGeom { xp, yp, w, h, part_idx, .. } = *g; let pml = self.pps.log2_parallel_merge_level; - let (xp, yp, w, h, part_idx) = if pml > 2 && ncb == 8 { (xc, yc, ncb, ncb, 0) } else { (xp0, yp0, w0, h0, part_idx0) }; - let g = PbGeom { xc, yc, ncb, xp, yp, w, h, part_idx }; let same_mer = |xn: i32, yn: i32| (xp >> pml) == (xn >> pml) && (yp >> pml) == (yn >> pml); - let avail = |xn: i32, yn: i32| self.pb_avail(&g, xn, yn) && !same_mer(xn, yn); + let avail = |xn: i32, yn: i32| self.pb_avail(g, xn, yn) && !same_mer(xn, yn); let mut list: [MvField; 5] = [MvField::default(); 5]; let mut n = 0; - let max = self.sh.max_num_merge_cand as usize; - // spatial candidates (8.5.3.2.3) let pa1 = (xp - 1, yp + h - 1); let av_a1 = avail(pa1.0, pa1.1) && !(matches!(part, PartMode::PNx2N | PartMode::PnLx2N | PartMode::PnRx2N) && part_idx == 1); let m_a1 = if av_a1 { self.mv_at(pa1.0, pa1.1) } else { MvField::default() }; @@ -130,10 +153,11 @@ impl<'a> SliceDecoder<'a> { list[n] = self.mv_at(pb2.0, pb2.1); n += 1; } - if merge_idx < n { - return self.finish_merge(list[merge_idx], w0, h0); - } - // temporal candidate + (list, n) + } + + /// The temporal candidate (8.5.3.2.8): list 0 from the collocated picture, and list 1 for B slices. + fn temporal_merge_candidate(&self, xp: i32, yp: i32, w: i32, h: i32) -> Option { let mut col = MvField::default(); let mut col_avail = false; if let Some(mv) = self.temporal(xp, yp, w, h, 0, 0) { @@ -148,30 +172,28 @@ impl<'a> SliceDecoder<'a> { col.ref_idx[1] = 0; col_avail = true; } - let mut cands: Vec = list[..n].to_vec(); - if col_avail { - cands.push(col); - } - if merge_idx < cands.len() { - return self.finish_merge(cands[merge_idx], w0, h0); - } - // combined bi-predictive candidates (8.5.3.2.4) + col_avail.then_some(col) + } + + /// Combined bi-predictive candidates (8.5.3.2.4) from pairs of the original candidates. + fn combined_merge_candidates(&self, cands: &mut Vec, max: usize) { let num_orig = cands.len(); - if self.sh.is_b() && num_orig > 1 && num_orig < max { - let mut comb = 0; - while comb < num_orig * (num_orig - 1) && cands.len() < max { - let (i0, i1) = COMB[comb]; - let (c0, c1) = (cands[i0], cands[i1]); - if c0.pred(0) && c1.pred(1) && (self.ref_pic(0, c0.ref_idx[0]).poc != self.ref_pic(1, c1.ref_idx[1]).poc || c0.mv[0] != c1.mv[1]) { - cands.push(MvField { mv: [c0.mv[0], c1.mv[1]], ref_idx: [c0.ref_idx[0], c1.ref_idx[1]] }); - } - comb += 1; + if !(self.sh.is_b() && num_orig > 1 && num_orig < max) { + return; + } + let mut comb = 0; + while comb < num_orig * (num_orig - 1) && cands.len() < max { + let (i0, i1) = COMB[comb]; + let (c0, c1) = (cands[i0], cands[i1]); + if c0.pred(0) && c1.pred(1) && (self.ref_pic(0, c0.ref_idx[0]).poc != self.ref_pic(1, c1.ref_idx[1]).poc || c0.mv[0] != c1.mv[1]) { + cands.push(MvField { mv: [c0.mv[0], c1.mv[1]], ref_idx: [c0.ref_idx[0], c1.ref_idx[1]] }); } + comb += 1; } - if merge_idx < cands.len() { - return self.finish_merge(cands[merge_idx], w0, h0); - } - // zero candidates (8.5.3.2.5) + } + + /// Zero candidates (8.5.3.2.5) up to and including `merge_idx`. + fn zero_merge_candidates(&self, cands: &mut Vec, merge_idx: usize) { let num_ref = if self.sh.is_b() { self.sh.num_ref_idx[0].min(self.sh.num_ref_idx[1]) } else { self.sh.num_ref_idx[0] } as usize; let mut zero_idx = 0; while cands.len() <= merge_idx { @@ -179,7 +201,6 @@ impl<'a> SliceDecoder<'a> { cands.push(MvField { mv: [[0; 2]; 2], ref_idx: [r, if self.sh.is_b() { r } else { -1 }] }); zero_idx += 1; } - self.finish_merge(cands[merge_idx], w0, h0) } fn finish_merge(&self, mut f: MvField, w: i32, h: i32) -> MvField { From aaf6c1c38aa6c59beda8110bd014cc6c96c6ec74 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 21:13:50 -0400 Subject: [PATCH 19/19] refactor(show): split the key handler into grid, navigation and tool keys (S3776) (#1402) --- crates/ui-egui/src/show.rs | 188 ++++++++++++++++++++++--------------- 1 file changed, 114 insertions(+), 74 deletions(-) diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 7055f9b2..a2658391 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -739,84 +739,19 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64, let mut keep = true; let mut actions = vec![]; for (k, m) in keys { - use egui::Key::*; - let presenter_grid = presenter && show.presenter_grid; - let grid_open = if presenter { show.presenter_grid } else { show.grid }; - if grid_open && !matches!(k, Escape | Minus) { + let handled = if grid_open(show, presenter) && !matches!(k, egui::Key::Escape | egui::Key::Minus) { // See All Slides takes no other keys until a slide is chosen. - if k == G && plain(m) { - if presenter_grid { - show.presenter_grid = false; - } else { - show.grid = false; - } + if k == egui::Key::G && plain(m) { + close_grid(show, presenter); + true } else { - continue; + false } } else { - match k { - Escape | Minus => { - let cancelled = k == Escape && show.clear_goto(); - if !cancelled && !show.back_out(presenter) { - keep = false; - } - } - ArrowRight | ArrowDown | Space | PageDown | N => { - if show.ended { - keep = false; - } else { - actions.push(show.state.next(doc)); - } - } - Enter => { - if !show.goto.is_empty() { - if let Some(i) = show.take_goto() - && i < doc.slides.len() - { - actions.push(show.state.goto(doc, i)); - } - } else if show.ended { - keep = false; - } else { - actions.push(show.state.next(doc)); - } - } - Backspace if !show.goto.is_empty() => { - show.goto.pop(); - } - ArrowLeft | ArrowUp | Backspace | PageUp | P if !(k == P && m.command) => { - show.ended = false; - actions.push(show.state.prev(doc)); - } - P if m.command => show.set_pen(!show.pen), - L if m.command => show.set_laser(!show.laser), - A if m.command => show.arrow(), - Home => { - if let Some(i) = show.state.first_index(doc) { - actions.push(show.state.goto(doc, i)); - } - } - End => { - if let Some(i) = show.state.last_index(doc) { - actions.push(show.state.goto(doc, i)); - } - } - B | Period => toggle_blank(show, Color32::BLACK), - W | Comma => toggle_blank(show, Color32::WHITE), - E => show.strokes.clear(), - G if plain(m) => show.open_grid(presenter), - Z if plain(m) => show.zoom_key(), - T if plain(m) => show.timer.toggle(now), - R if plain(m) => show.timer.reset(now), - OpenBracket if plain(m) => show.nudge_notes(-NOTES_STEP), - CloseBracket if plain(m) => show.nudge_notes(NOTES_STEP), - Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9 => { - if let Some(d) = k.name().chars().next() { - show.push_slide_number(d, now); - } - } - _ => continue, - } + navigation_key(show, doc, k, m, presenter, &mut keep, &mut actions) || tool_key(show, k, m, now, presenter) + }; + if !handled { + continue; } // The same key must not also activate a focused presenter button or be handled again // by an embedded presenter viewport. @@ -831,6 +766,111 @@ fn keyboard(ctx: &egui::Context, show: &mut Show, doc: &Presentation, now: f64, keep } +/// Whether the grid that the presenter key G opens is showing in this viewport. +fn grid_open(show: &Show, presenter: bool) -> bool { + if presenter { show.presenter_grid } else { show.grid } +} + +/// Closes the grid that G opens in this viewport. +fn close_grid(show: &mut Show, presenter: bool) { + if presenter && show.presenter_grid { + show.presenter_grid = false; + } else { + show.grid = false; + } +} + +/// Keys that move through the show or end it. False when `k` isn't one of them. +fn navigation_key( + show: &mut Show, + doc: &Presentation, + k: egui::Key, + m: egui::Modifiers, + presenter: bool, + keep: &mut bool, + actions: &mut Vec, +) -> bool { + use egui::Key::*; + match k { + Escape | Minus => { + let cancelled = k == Escape && show.clear_goto(); + if !cancelled && !show.back_out(presenter) { + *keep = false; + } + } + ArrowRight | ArrowDown | Space | PageDown | N => { + if show.ended { + *keep = false; + } else { + actions.push(show.state.next(doc)); + } + } + Enter => { + if !show.goto.is_empty() { + if let Some(i) = show.take_goto() + && i < doc.slides.len() + { + actions.push(show.state.goto(doc, i)); + } + } else if show.ended { + *keep = false; + } else { + actions.push(show.state.next(doc)); + } + } + Backspace if !show.goto.is_empty() => { + show.goto.pop(); + } + ArrowLeft | ArrowUp | Backspace | PageUp => { + show.ended = false; + actions.push(show.state.prev(doc)); + } + P if m.command => show.set_pen(!show.pen), + P => { + show.ended = false; + actions.push(show.state.prev(doc)); + } + Home => { + if let Some(i) = show.state.first_index(doc) { + actions.push(show.state.goto(doc, i)); + } + } + End => { + if let Some(i) = show.state.last_index(doc) { + actions.push(show.state.goto(doc, i)); + } + } + _ => return false, + } + true +} + +/// Pen, laser, blanking, zoom, the timer, note size and typed slide numbers. False when `k` +/// isn't one of them. +fn tool_key(show: &mut Show, k: egui::Key, m: egui::Modifiers, now: f64, presenter: bool) -> bool { + use egui::Key::*; + match k { + L if m.command => show.set_laser(!show.laser), + A if m.command => show.arrow(), + B | Period => toggle_blank(show, Color32::BLACK), + W | Comma => toggle_blank(show, Color32::WHITE), + E => show.strokes.clear(), + G if plain(m) => show.open_grid(presenter), + Z if plain(m) => show.zoom_key(), + T if plain(m) => show.timer.toggle(now), + R if plain(m) => show.timer.reset(now), + OpenBracket if plain(m) => show.nudge_notes(-NOTES_STEP), + CloseBracket if plain(m) => show.nudge_notes(NOTES_STEP), + Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9 => { + if let Some(d) = k.name().chars().next() { + show.push_slide_number(d, now); + } + } + _ => return false, + } + true +} + /// Draw and advance the active show, handling hidden frames, timing, input, transitions, and /// presenter/reading presentation. pub fn ui(app: &mut SlideApp, ui: &mut Ui) {