aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-06 09:14:30 -0500
committerCraig Jennings <c@cjennings.net>2026-07-06 09:14:30 -0500
commitdca4e105e20d9218277ad61bb9c7f3e199ef699f (patch)
treea195a4a3bd43e5efb5f0653d5d9a303a5809a43c
parent648c150f6e4e89c9c04158795da1ab13370d7f73 (diff)
downloaddotemacs-dca4e105e20d9218277ad61bb9c7f3e199ef699f.tar.gz
dotemacs-dca4e105e20d9218277ad61bb9c7f3e199ef699f.zip
fix(dirvish): stop webm preview thumbnails flashing blank
dirvish keys its media thumbnail cache on the exact preview-window pixel width (thumbnails/<width>/<md5>.jpg). Fast image thumbnails finish before the window settles, so their width is stable. Slow webm generation loses that race: the jpg is written at one width, then redisplay recomputes the width, and jitter across a floor boundary looks it up in a different bucket, so it misses, regenerates, and flashes blank. I quantize the computed size to a 100px bucket (cj/dirvish-thumb-width-bucket) so jitter maps to one stable cache key. It's a filter-return advice on dirvish-media--img-size, added once dirvish-widgets loads. It's a local workaround for the upstream cache design. If the settle swing turns out larger than one bucket, widen the bucket or fall back to a width-independent hash. The quantize math is unit-tested. The live preview render needs a manual check.
-rw-r--r--modules/dirvish-config.el45
-rw-r--r--tests/test-dirvish-config--quantize-thumb-size.el60
2 files changed, 105 insertions, 0 deletions
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
index 713a5e69..0e22dcee 100644
--- a/modules/dirvish-config.el
+++ b/modules/dirvish-config.el
@@ -685,6 +685,51 @@ no popup frame is live."
("v" . dirvish-vc-menu)
("y" . dirvish-yank-menu)))
+;;; --------------------- Dirvish Thumbnail Cache Width Bucket ------------------
+;; dirvish keys its media thumbnail cache path on the exact preview-window pixel
+;; width (thumbnails/<width>/<md5>.jpg). Fast image thumbnails finish before the
+;; window settles, so their width is stable across a landing. Slow video (webm
+;; VP9 decode + seek) loses that race: the jpg is written at the generation-time
+;; width, then `dirvish-media--cache-sentinel' recomputes the width for redisplay,
+;; and if the preview window jittered even one pixel across a `floor' boundary the
+;; lookup hits a different bucket, misses, regenerates, and flashes blank.
+;; Quantizing the computed size to a coarse bucket maps that jitter to one stable
+;; key. A local workaround for the upstream cache design.
+
+(defgroup cj/dirvish nil
+ "Local dirvish tweaks on top of the upstream package."
+ :group 'dirvish)
+
+(defcustom cj/dirvish-thumb-width-bucket 100
+ "Pixel bucket dirvish's thumbnail cache width is quantized to.
+Rounding `dirvish-media--img-size' to this bucket keeps small preview-window
+jitter on one cache key, so a slow video thumbnail isn't looked up under a
+different width than it was written at. A larger bucket tolerates more jitter at
+a slight thumbnail-resolution cost. Set to 0 or nil to disable quantization."
+ :type '(choice (const :tag "Off" nil) integer)
+ :group 'cj/dirvish)
+
+(defun cj/--dirvish-quantize-thumb-size (size bucket)
+ "Round SIZE to the nearest multiple of BUCKET, clamped to at least BUCKET.
+BUCKET nil or non-positive returns SIZE unchanged (quantization off). Pure
+helper behind the `dirvish-media--img-size' advice."
+ (if (and (integerp bucket) (> bucket 0))
+ (max bucket (* bucket (round size bucket)))
+ size))
+
+(defun cj/--dirvish-thumb-size-advice (size)
+ "Quantize dirvish's computed thumbnail SIZE to a stable cache bucket.
+`:filter-return' advice on `dirvish-media--img-size'; the bucket is
+`cj/dirvish-thumb-width-bucket'."
+ (cj/--dirvish-quantize-thumb-size size cj/dirvish-thumb-width-bucket))
+
+;; dirvish-media--img-size lives in dirvish-widgets.el (loaded lazily on the
+;; first media preview), so advise it only once that file is present. advice-add
+;; with a named function is idempotent, so a module reload never stacks copies.
+(with-eval-after-load 'dirvish-widgets
+ (advice-add 'dirvish-media--img-size :filter-return
+ #'cj/--dirvish-thumb-size-advice))
+
;;; ----------------------------- Dired Text Greying ----------------------------
;; `default' is remapped buffer-locally to `shadow' inside dired/dirvish (see
diff --git a/tests/test-dirvish-config--quantize-thumb-size.el b/tests/test-dirvish-config--quantize-thumb-size.el
new file mode 100644
index 00000000..a26ef090
--- /dev/null
+++ b/tests/test-dirvish-config--quantize-thumb-size.el
@@ -0,0 +1,60 @@
+;;; test-dirvish-config--quantize-thumb-size.el --- thumbnail width-bucket tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--dirvish-quantize-thumb-size' rounds dirvish's computed thumbnail size to
+;; a coarse pixel bucket so small preview-window jitter maps to one stable cache
+;; key instead of a fresh miss + regenerate (the webm thumbnail-flash bug). Pure
+;; math; the :filter-return advice that wires it onto `dirvish-media--img-size'
+;; is verified live.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'dirvish-config)
+
+(declare-function cj/--dirvish-quantize-thumb-size "dirvish-config" (size bucket))
+
+;;; ------------------------------- rounding -----------------------------------
+
+(ert-deftest test-dirvish-quantize-rounds-down ()
+ "Normal: a size below the bucket midpoint rounds down to the bucket."
+ (should (= (cj/--dirvish-quantize-thumb-size 931 100) 900)))
+
+(ert-deftest test-dirvish-quantize-rounds-up ()
+ "Normal: a size above the bucket midpoint rounds up to the next bucket."
+ (should (= (cj/--dirvish-quantize-thumb-size 552 100) 600)))
+
+(ert-deftest test-dirvish-quantize-on-bucket-unchanged ()
+ "Normal: a size already on a bucket boundary is returned unchanged."
+ (should (= (cj/--dirvish-quantize-thumb-size 600 100) 600)))
+
+(ert-deftest test-dirvish-quantize-jitter-maps-to-one-key ()
+ "Boundary: nearby sizes within a bucket collapse to the same cache key."
+ (should (= (cj/--dirvish-quantize-thumb-size 931 100)
+ (cj/--dirvish-quantize-thumb-size 949 100))))
+
+(ert-deftest test-dirvish-quantize-float-input ()
+ "Boundary: a float size (pre-floor) still yields an integer bucket."
+ (let ((r (cj/--dirvish-quantize-thumb-size 931.4 100)))
+ (should (integerp r))
+ (should (= r 900))))
+
+;;; ------------------------------- clamping -----------------------------------
+
+(ert-deftest test-dirvish-quantize-tiny-clamps-to-bucket ()
+ "Boundary: a size that would round to 0 clamps up to one bucket, never 0."
+ (should (= (cj/--dirvish-quantize-thumb-size 1 100) 100)))
+
+;;; ------------------------------ disabled path -------------------------------
+
+(ert-deftest test-dirvish-quantize-zero-bucket-passthrough ()
+ "Error/disabled: a zero bucket returns the size unchanged."
+ (should (= (cj/--dirvish-quantize-thumb-size 931 0) 931)))
+
+(ert-deftest test-dirvish-quantize-nil-bucket-passthrough ()
+ "Error/disabled: a nil bucket returns the size unchanged."
+ (should (= (cj/--dirvish-quantize-thumb-size 931 nil) 931)))
+
+(provide 'test-dirvish-config--quantize-thumb-size)
+;;; test-dirvish-config--quantize-thumb-size.el ends here