diff --git a/Cargo.lock b/Cargo.lock index 7ca4ec26..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", ] @@ -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", @@ -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.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.61.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.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3920,7 +3920,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.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.61.2", + "windows-sys 0.52.0", ] [[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.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", 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"] } diff --git a/ROADMAP.md b/ROADMAP.md index 56fc162d..1543f22c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -138,6 +138,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| | 2026-10-25 | A photo in a picture placeholder is cut into the shape its layout gives the placeholder, as PowerPoint draws it: a layout's round picture placeholder keeps a slide's photo in a circle with the layout's white border, where it came out as a rectangle (the reader only passed `p:sp` placeholders to `inherit_placeholder_geometry`, not `p:pic`) | +| 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) | @@ -295,6 +296,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| | 2026-10-25 | trivial | Progress log: a picture in a picture placeholder takes the layout's shape. Scores and milestone names unchanged | +| 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("> 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/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::*; 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::*; 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]; } diff --git a/crates/av1/src/transform.rs b/crates/av1/src/transform.rs index afa649a4..65db0b04 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 { @@ -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 { 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) 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, 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()?; 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..17fb8ba7 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); } } @@ -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 { 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..236c14de 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,27 +73,40 @@ 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 (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 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; - // 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() }; @@ -126,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) { @@ -144,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 { @@ -175,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 { @@ -237,20 +262,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 +290,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 +306,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 48afc3b5..5bf9f07b 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}; @@ -397,6 +398,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 +444,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 +513,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); @@ -668,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(()); } @@ -697,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; @@ -714,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; } @@ -728,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); @@ -826,6 +838,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 +878,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 @@ -898,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 @@ -923,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 = @@ -935,36 +951,50 @@ 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) } - 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<()> { + /// 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, 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]; 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 { @@ -1264,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 { @@ -1283,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]; @@ -1335,96 +1365,107 @@ 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 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(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(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]> { @@ -1504,11 +1545,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); } } } 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; 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) { diff --git a/packaging/windows/deckcraft.wxs b/packaging/windows/deckcraft.wxs index cf07e388..3d429ea7 100644 --- a/packaging/windows/deckcraft.wxs +++ b/packaging/windows/deckcraft.wxs @@ -58,9 +58,15 @@ + + 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