Compare commits

...
Author SHA1 Message Date
Dylan M. Taylor 2f30931f89 refactor(show): split the key handler into grid, navigation and tool keys (S3776) 2026-10-25 20:38:54 -04:00
Dylan M. Taylor aa3a1baa01 refactor(av1): reduce complexity in cdef and loop restoration (S3776) (#1278) 2026-10-25 20:26:38 -04:00
Dylan M. Taylor edfee40392 refactor(av1): split inverse transform into stages and passes (S3776) (#1277) 2026-10-25 20:26:33 -04:00
Dylan M. Taylor 580b577cf2 fix(h264): use Option::as_ref instead of closure (S1612) (#1268) 2026-10-25 20:26:28 -04:00
Sofia Alves FigueredoandClaude Sonnet 5.5 230b6485d9 fix(windows): keep the shortcut icon after an update (#1114) (#1391)
* fix(windows): keep the shortcut icon after an update (#1114)

The MSI's Start menu shortcut took its icon from the WiX <Icon>, which Windows
Installer caches under %APPDATA%\Microsoft\Installer\{ProductCode}\. Every
update is a major upgrade with a new ProductCode, so the old folder is deleted.
A taskbar pin is a copy of the shortcut that the update never touches, so it
kept pointing at the deleted file and lost its icon.

Drop the Icon from the shortcut so it shows the icon embedded in deckcraft.exe,
whose path survives updates. The <Icon> stays for ARPPRODUCTICON. A test keeps
shortcuts from naming an MSI icon again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* contributors: credit Sofia Alves Figueredo by real name

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(windows): make the Start menu shortcut non-advertised (#1114)

Addresses the review comment: without an MSI Icon, a non-advertised shortcut
targets the installed exe directly and takes its embedded icon.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(windows): package.ps1 rejects a deckcraft.exe with no embedded icon (#1114)

The shortcuts now take the exe's own icon, and -SkipBuild packages an existing
exe that may come from a build without DECKCRAFT_REQUIRE_WINRES=1. Count its
icons with ExtractIconEx and stop before WiX when there are none.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:23:43 -04:00
10 changed files with 435 additions and 232 deletions
+2
View File
@@ -137,6 +137,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| Date | What landed |
|---|---|
| 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI `<Icon>`, so a taskbar pin keeps its icon after an update (#1114); a pin that already lost it has to be unpinned and pinned again once |
| 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off |
| 2026-10-24 | Tables: while the caret is in a cell, dragging into another cell selects that range of cells (shaded, merged cells taken in whole), and Shift+arrow grows or shrinks it; formatting applies to the selected cells only, and Delete clears their text and keeps the table |
| 2026-10-24 | Preferences ▸ Measurement units: inches or centimetres. The Format pane's Size and Position fields, Shape Format ▸ Size on the ribbon, the Slide Size dialog and the ruler show and take the chosen unit; the deck still stores points (#1326) |
@@ -293,6 +294,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | trivial | Progress log: Windows shortcut icon survives updates (#1114). Scores and milestone names unchanged |
| 2026-10-24 | trivial | Progress log: selecting a range of table cells by dragging. Scores and milestone names unchanged |
| 2026-10-24 | trivial | Progress log: measurement units in centimetres (#1326). Scores unchanged |
| 2026-10-22 | trivial | Progress log: Animation Pane ▸ Play From. Scores unchanged |
+23
View File
@@ -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 `<Icon>` 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 `<Icon>`.
#[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("<Shortcut").skip(1).filter_map(|s| s.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 <Icon>:{shortcut}");
}
assert!(wxs.contains("<Property Id=\"ARPPRODUCTICON\""), "Apps and features keeps its icon");
}
/// The shortcuts take the exe's own icon, and `package.ps1 -SkipBuild` packages an existing
/// exe without `DECKCRAFT_REQUIRE_WINRES`, so the script has to refuse one with no icon.
#[test]
fn the_windows_package_script_rejects_an_exe_without_an_icon() {
let script = include_str!("../../../packaging/windows/package.ps1");
assert!(script.contains("ExtractIconEx"), "package.ps1 does not count the exe's icons");
assert!(script.contains("has no embedded icon"), "package.ps1 does not reject an iconless exe");
}
}
+3
View File
@@ -65,3 +65,6 @@ sync_github_name = true
[people.Katacc]
sync_github_name = true
[people.SofiaAFigueredo]
real_name = "Sofia Alves Figueredo"
+115 -74
View File
@@ -45,7 +45,7 @@ fn cdef_block(fs: &FrameState, out: &mut FrameBuf, r: usize, c: usize, idx: usiz
let var_str = if (var >> 6) != 0 { (31 - ((var >> 6) as u32).leading_zeros()).min(12) as i32 } else { 0 };
pri = if var != 0 { (pri * (4 + var_str) + 8) >> 4 } else { 0 };
let damping = cp.damping as i32 + coeff_shift as i32;
filter(fs, out, 0, r, c, pri, sec, damping, dir);
filter(fs, out, 0, (r, c), (pri, sec), damping, dir);
if fs.num_planes == 1 {
return true;
}
@@ -53,8 +53,8 @@ fn cdef_block(fs: &FrameState, out: &mut FrameBuf, r: usize, c: usize, idx: usiz
let sec = (cp.uv_sec[idx] << coeff_shift) as i32;
let dir = if pri == 0 { 0 } else { CDEF_UV_DIR[fs.ssx][fs.ssy][y_dir] as usize };
let damping = cp.damping as i32 + coeff_shift as i32 - 1;
filter(fs, out, 1, r, c, pri, sec, damping, dir);
filter(fs, out, 2, r, c, pri, sec, damping, dir);
filter(fs, out, 1, (r, c), (pri, sec), damping, dir);
filter(fs, out, 2, (r, c), (pri, sec), damping, dir);
true
}
@@ -124,33 +124,74 @@ fn constrain(diff: i32, threshold: i32, adj: u32) -> i32 {
if diff < 0 { -mag } else { mag }
}
#[allow(clippy::too_many_arguments)]
fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, r: usize, c: usize, pri: i32, sec: i32, damping: i32, dir: usize) {
if pri == 0 && sec == 0 {
// Every tap contributes 0 and the clamp to [min, max] keeps x: CdefFrame = CurrFrame.
return;
/// Tap offsets, weights and strengths of one block.
struct Taps {
pri: i32,
sec: i32,
pri_adj: u32,
sec_adj: u32,
pri_off: [isize; 2],
sec_off: [[isize; 2]; 2],
pri_taps: [i32; 2],
sec_taps: [i32; 2],
}
impl Taps {
fn new(coeff_shift: u32, (pri, sec): (i32, i32), damping: i32, dir: usize) -> Taps {
let pri_tap = ((pri >> coeff_shift) & 1) as usize;
let off = |d: usize, k: usize| CDEF_DIRECTIONS[d][k][0] as isize * PAD_STRIDE as isize + CDEF_DIRECTIONS[d][k][1] as isize;
let pri_off = [off(dir, 0), off(dir, 1)];
let d_lo = (dir + 6) & 7;
let d_hi = (dir + 2) & 7;
let sec_off = [[off(d_lo, 0), off(d_hi, 0)], [off(d_lo, 1), off(d_hi, 1)]];
let pri_taps = [CDEF_PRI_TAPS[pri_tap][0] as i32, CDEF_PRI_TAPS[pri_tap][1] as i32];
let sec_taps = [CDEF_SEC_TAPS[pri_tap][0] as i32, CDEF_SEC_TAPS[pri_tap][1] as i32];
let adj = |t: i32| if t == 0 { 0 } else { (damping - (31 - (t as u32).leading_zeros()) as i32).max(0) as u32 };
Taps { pri, sec, pri_adj: adj(pri), sec_adj: adj(sec), pri_off, sec_off, pri_taps, sec_taps }
}
let coeff_shift = fs.bit_depth - 8;
/// The 12 (offset, weight, strength, adjustment) taps in filtering order.
fn tap_list(&self) -> [(isize, i32, i32, u32); 12] {
let mut taps = [(0isize, 0i32, 0i32, 0u32); 12];
let mut n = 0;
for k in 0..2 {
for sign in [-1isize, 1] {
taps[n] = (sign * self.pri_off[k], self.pri_taps[k], self.pri, self.pri_adj);
taps[n + 1] = (sign * self.sec_off[k][0], self.sec_taps[k], self.sec, self.sec_adj);
taps[n + 2] = (sign * self.sec_off[k][1], self.sec_taps[k], self.sec, self.sec_adj);
n += 3;
}
}
taps
}
}
/// Position and size of a block within its plane.
#[derive(Clone, Copy)]
struct BlockRect {
x0: usize,
y0: usize,
w: usize,
h: usize,
}
/// The block with two samples of padding on every side, plus whether every sample is available
/// (CdefAvailable: its 4x4 block is inside the frame).
fn padded_block(fs: &FrameState, plane: usize, src: &Plane, rect: BlockRect) -> ([i32; PAD_STRIDE * PAD_STRIDE], bool) {
let (sub_x, sub_y) = if plane > 0 { (fs.ssx, fs.ssy) } else { (0, 0) };
let x0 = (c * 4) >> sub_x;
let y0 = (r * 4) >> sub_y;
let w = 8 >> sub_x;
let h = 8 >> sub_y;
let src = &fs.cur.planes[plane];
let (mi_rows, mi_cols) = (fs.fh.mi_rows as isize, fs.fh.mi_cols as isize);
// CdefAvailable per sample: its 4x4 block is inside the frame.
let mut buf = [CDEF_NA; PAD_STRIDE * PAD_STRIDE];
let mut all_avail = true;
for i in 0..h + 4 {
let y = (y0 + i) as isize - 2;
for i in 0..rect.h + 4 {
let y = (rect.y0 + i) as isize - 2;
let cand_r = (y << sub_y) >> 2;
if y < 0 || cand_r >= mi_rows {
all_avail = false;
continue;
}
let row = src.row(y as usize);
for j in 0..w + 4 {
let x = (x0 + j) as isize - 2;
for j in 0..rect.w + 4 {
let x = (rect.x0 + j) as isize - 2;
let cand_c = (x << sub_x) >> 2;
if x >= 0 && cand_c < mi_cols {
buf[i * PAD_STRIDE + j] = row[x as usize] as i32;
@@ -159,70 +200,70 @@ fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, r: usize, c: usize,
}
}
}
let pri_tap = ((pri >> coeff_shift) & 1) as usize;
let off = |d: usize, k: usize| CDEF_DIRECTIONS[d][k][0] as isize * PAD_STRIDE as isize + CDEF_DIRECTIONS[d][k][1] as isize;
let pri_off = [off(dir, 0), off(dir, 1)];
let d_lo = (dir + 6) & 7;
let d_hi = (dir + 2) & 7;
let sec_off = [[off(d_lo, 0), off(d_hi, 0)], [off(d_lo, 1), off(d_hi, 1)]];
let pri_taps = [CDEF_PRI_TAPS[pri_tap][0] as i32, CDEF_PRI_TAPS[pri_tap][1] as i32];
let sec_taps = [CDEF_SEC_TAPS[pri_tap][0] as i32, CDEF_SEC_TAPS[pri_tap][1] as i32];
let adj = |t: i32| if t == 0 { 0 } else { (damping - (31 - (t as u32).leading_zeros()) as i32).max(0) as u32 };
let (pri_adj, sec_adj) = (adj(pri), adj(sec));
(buf, all_avail)
}
/// Filters one sample whose taps may be unavailable (CDEF_NA).
fn filter_sample(buf: &[i32; PAD_STRIDE * PAD_STRIDE], taps: &Taps, ci: isize, x: i32) -> u16 {
let mut sum = 0i32;
let mut max = x;
let mut min = x;
for k in 0..2 {
for sign in [-1isize, 1] {
let p = buf[(ci + sign * taps.pri_off[k]) as usize];
if p != CDEF_NA {
if taps.pri != 0 {
sum += taps.pri_taps[k] * constrain(p - x, taps.pri, taps.pri_adj);
}
max = max.max(p);
min = min.min(p);
}
for s in 0..2 {
let q = buf[(ci + sign * taps.sec_off[k][s]) as usize];
if q != CDEF_NA {
if taps.sec != 0 {
sum += taps.sec_taps[k] * constrain(q - x, taps.sec, taps.sec_adj);
}
max = max.max(q);
min = min.min(q);
}
}
}
}
(x + ((8 + sum - (sum < 0) as i32) >> 4)).clamp(min, max) as u16
}
fn filter(fs: &FrameState, out: &mut FrameBuf, plane: usize, (r, c): (usize, usize), strengths: (i32, i32), damping: i32, dir: usize) {
let (pri, sec) = strengths;
if pri == 0 && sec == 0 {
// Every tap contributes 0 and the clamp to [min, max] keeps x: CdefFrame = CurrFrame.
return;
}
let coeff_shift = fs.bit_depth - 8;
let (sub_x, sub_y) = if plane > 0 { (fs.ssx, fs.ssy) } else { (0, 0) };
let rect = BlockRect { x0: (c * 4) >> sub_x, y0: (r * 4) >> sub_y, w: 8 >> sub_x, h: 8 >> sub_y };
let src = &fs.cur.planes[plane];
let (buf, all_avail) = padded_block(fs, plane, src, rect);
let taps = Taps::new(coeff_shift, strengths, damping, dir);
let dst = &mut out.planes[plane];
if all_avail {
// Every tap is available: whole rows at a time (constrain() with threshold 0 is 0).
let mut taps = [(0isize, 0i32, 0i32, 0u32); 12];
let mut n = 0;
for k in 0..2 {
for sign in [-1isize, 1] {
taps[n] = (sign * pri_off[k], pri_taps[k], pri, pri_adj);
taps[n + 1] = (sign * sec_off[k][0], sec_taps[k], sec, sec_adj);
taps[n + 2] = (sign * sec_off[k][1], sec_taps[k], sec, sec_adj);
n += 3;
}
}
let rows = (y0, x0, h);
match w {
8 => filter_rows::<8>(&buf, &taps, dst, rows),
_ => filter_rows::<4>(&buf, &taps, dst, rows),
let list = taps.tap_list();
let rows = (rect.y0, rect.x0, rect.h);
match rect.w {
8 => filter_rows::<8>(&buf, &list, dst, rows),
_ => filter_rows::<4>(&buf, &list, dst, rows),
}
return;
}
for i in 0..h {
for i in 0..rect.h {
// The centre sample is read even where CdefAvailable is 0 (8x8 blocks straddling an odd
// MiRows / MiCols edge).
let srow = &src.row(y0 + i)[x0..x0 + w];
let drow = &mut dst.row_mut(y0 + i)[x0..x0 + w];
for j in 0..w {
let srow = &src.row(rect.y0 + i)[rect.x0..rect.x0 + rect.w];
let drow = &mut dst.row_mut(rect.y0 + i)[rect.x0..rect.x0 + rect.w];
for j in 0..rect.w {
let ci = ((i + 2) * PAD_STRIDE + j + 2) as isize;
let x = srow[j] as i32;
let mut sum = 0i32;
let mut max = x;
let mut min = x;
for k in 0..2 {
for sign in [-1isize, 1] {
let p = buf[(ci + sign * pri_off[k]) as usize];
if p != CDEF_NA {
if pri != 0 {
sum += pri_taps[k] * constrain(p - x, pri, pri_adj);
}
max = max.max(p);
min = min.min(p);
}
for s in 0..2 {
let q = buf[(ci + sign * sec_off[k][s]) as usize];
if q != CDEF_NA {
if sec != 0 {
sum += sec_taps[k] * constrain(q - x, sec, sec_adj);
}
max = max.max(q);
min = min.min(q);
}
}
}
}
drow[j] = (x + ((8 + sum - (sum < 0) as i32) >> 4)).clamp(min, max) as u16;
drow[j] = filter_sample(&buf, &taps, ci, srow[j] as i32);
}
}
}
+113 -78
View File
@@ -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];
}
+47 -3
View File
@@ -165,6 +165,15 @@ pub(crate) fn inverse_dct<T: Lane>(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: Lane>(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: Lane>(t: &mut [T], n: u32, r: u32) {
hd(t, 16 + 2 * i, 17 + 2 * i, i & 1 == 1, r);
}
}
}
fn inverse_dct_stage2<T: Lane>(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: Lane>(t: &mut [T], n: u32, r: u32) {
}
}
}
}
fn inverse_dct_stage3<T: Lane>(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: Lane>(t: &mut [T], n: u32, r: u32) {
}
}
}
}
fn inverse_dct_stage4<T: Lane>(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: Lane>(t: &mut [T], n: u32, r: u32) {
}
}
}
}
fn inverse_dct_stage5<T: Lane>(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: Lane>(t: &mut [T], n: u32, r: u32) {
hd(t, i, 15 - i, false, r);
}
}
}
fn inverse_dct_stage6<T: Lane>(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: Lane>(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<const L: usize>(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<const L: usize>(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<const L: usize>(dequant: &[i32], residual: &mut [i32], p: &Tx
}
i0 += L;
}
}
fn lanes_col_pass<const L: usize>(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 {
+1 -1
View File
@@ -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()?;
+114 -74
View File
@@ -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<ShowAction>,
) -> 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) {
+8 -2
View File
@@ -58,9 +58,15 @@
<Component Id="DeckcraftApp" Guid="D87EA444-41A4-4C19-922E-66D1A29461FF">
<File Id="DeckcraftNotice" Source="$(var.BinDir)\NOTICE" KeyPath="no" />
<File Id="DeckcraftExe" Source="$(var.BinDir)\deckcraft.exe" KeyPath="yes">
<!-- No Icon here, so the shortcut shows the icon embedded in deckcraft.exe (build.rs). An
MSI Icon is cached under %APPDATA%\Microsoft\Installer\{ProductCode}\, and every
update is a major upgrade with a new ProductCode that deletes the old folder. A
taskbar pin is a copy of this shortcut that the update never touches, so it kept
pointing at the deleted file and lost its icon (#1114). The exe's path survives
updates; the shortcut is not advertised, so it targets that path and its icon is the
exe's own. The Icon above stays for ARPPRODUCTICON (Apps and features). -->
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="DeckCraft"
Description="DeckCraft presentations and slide shows" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
Icon="DeckcraftIcon.ico" IconIndex="0" />
Description="DeckCraft presentations and slide shows" WorkingDirectory="INSTALLFOLDER" Advertise="no" />
</File>
<!-- .deckcraft: DeckCraft's own format. -->
+9
View File
@@ -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