From 1e054f7109dd96c40c9ad70041b4ea9dfbaefc01 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Thu, 6 Aug 2026 17:50:03 +0200 Subject: [PATCH] docs(zoom): the ties both round up, they do not oppose Review caught the explanation of the 0.015 round-trip break, not the fix. Three copies of it claimed 16.015 rounds up while 16.005 rounds down --- "opposite directions". Both round UP. Verified rather than reasoned about: at the point the quantizer sees them, 16.015 * 100 is exactly 1601.5 and 16.005 * 100 is exactly 1600.5. Both are exact ties, and half-up sends both away from zero. So the mechanism is not opposed rounding, it is that half-up is not symmetric under negation. Rounding up on the way in adds half a centi-pixel; rounding up on the way out adds another, so the two errors ACCUMULATE instead of cancelling, and 16.00 -> 16.02 -> 16.01 ends one centi-pixel high. "Opposite directions" would have predicted them cancelling, which is the reverse of what happens. Corrected in all three places that carried it: the module comment, the test's doc comment, and framing section 3.2. Comments only --- no behavior change, and the witness values in the tests were already right. Zoom suite still 15/15. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- builtin/runtime/zoom.lua | 16 ++++++++++------ docs/gui-zoom-framing.md | 10 +++++++--- tests/gui_zoom_acceptance.rs | 6 ++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/builtin/runtime/zoom.lua b/builtin/runtime/zoom.lua index a211520..f54b265 100644 --- a/builtin/runtime/zoom.lua +++ b/builtin/runtime/zoom.lua @@ -77,12 +77,16 @@ end -- workaround for one. -- -- It also RESTORES the round-trip contract, which a raw step breaks: --- with 0.015 the sequence is 16.00 -> 16.02 -> 16.01, because each --- operation rounds independently and 16.015 and 16.005 round in --- opposite directions. Quantizing first makes every step exact --- addition in the quantized domain, so n in and n out returns to the --- starting value for ANY accepted step, not only for the ones that --- happened to be representable. +-- with 0.015 the sequence is 16.00 -> 16.02 -> 16.01. Each operation +-- rounds independently, and both intermediates land on an EXACT tie --- +-- 16.015 and 16.005 are 1601.5 and 1600.5 centi-px --- which this +-- half-up quantizer sends UP. Half-up is not symmetric under negation: +-- rounding up on the way in adds half a centi-pixel, and rounding up on +-- the way out adds another, so the two errors accumulate instead of +-- cancelling. Quantizing first makes every step exact addition in the +-- quantized domain, so n in and n out returns to the starting value for +-- ANY accepted step, not only for the ones that happened to be +-- representable. local function effective_step() return quantize(pmacs.config.get("ui.gpu-zoom-step")) end diff --git a/docs/gui-zoom-framing.md b/docs/gui-zoom-framing.md index b7bb5eb..97fff50 100644 --- a/docs/gui-zoom-framing.md +++ b/docs/gui-zoom-framing.md @@ -133,9 +133,13 @@ validates **finiteness and bounds and nothing else** *after* a value is stored — they cannot veto. So `0.015` is a perfectly settable step, and nothing in the registry can refuse it. -Used raw it breaks the guarantee below, because each operation rounds -independently and `16.015` and `16.005` round in **opposite -directions**: +Used raw it breaks the guarantee below. Each operation rounds +independently, and both intermediates land on an **exact tie** — +`16.015` and `16.005` are `1601.5` and `1600.5` centi-pixels — which the +half-up quantizer sends **up**. Half-up is not symmetric under negation: +rounding up on the way in adds half a centi-pixel, rounding up on the +way out adds another, so the two errors accumulate instead of +cancelling: ``` step 0.015: 16.00 -> 16.02 -> 16.01 round trip broken diff --git a/tests/gui_zoom_acceptance.rs b/tests/gui_zoom_acceptance.rs index 0d7a50d..6fe4925 100644 --- a/tests/gui_zoom_acceptance.rs +++ b/tests/gui_zoom_acceptance.rs @@ -128,8 +128,10 @@ fn n_steps_in_then_n_out_returns_exactly() { /// The registry accepts any finite number in range — `ConfigKind::Number` /// validates finiteness and bounds and nothing else, and `on_change` /// cannot veto — so 0.015 is a settable step. Used raw it breaks the -/// contract: each operation rounds independently, 16.015 rounds up and -/// 16.005 rounds down, giving 16.00 -> 16.02 -> 16.01. +/// contract: each operation rounds independently, and both intermediates +/// land on an exact tie — 16.015 and 16.005 — which half-up rounding +/// sends *up*. Up on the way in and up on the way out accumulate rather +/// than cancel, giving 16.00 -> 16.02 -> 16.01. /// /// Quantizing the step at the point of use restores exactness for every /// accepted step, not just the representable ones.