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.