Repository navigation
+ xBRZ 1.9 as its own scaler entry - #23
Merged
Merged
Conversation
Every blend in this package truncates its division. That is the right answer for the algorithms specified against it and the wrong one for those specified against round-to-nearest, and the scaler cannot choose: the lerp implementation is fixed by the pipeline at the call site, long before the algorithm runs. ILerp<T> therefore grows LerpRounded in both overloads — the weighted blend and the 50/50 one, which shifts rather than divides and truncates just the same. All eleven shipped implementations implement them. The byte and UNorm32 ones add half the total weight before dividing; the float-based ones never truncated to a representable step, so theirs forward to the truncating member with a comment saying why, as do the no-op blenders. The members are declared rather than defaulted. Default interface methods would keep the interface source-compatible for outside implementors, but this package targets down to net35 and net35 through net48 have no runtime support for them, so the compiler rejects them outright (CS8701). BatchDistanceDefaults already exists here for the same reason. Implementing them explicitly in each struct also keeps them off the boxing path, which matters: these are hot loops reached through a struct generic constraint.
The xBRZ port tracked the algorithm as far as release 1.7 and stopped. 1.9 changed the output in two ways, both exact and integer: the steep/shallow direction threshold rose from 2.2 to 2.4, so fewer near-diagonal runs are treated as lines, and the weighted and 50/50 blends round to nearest rather than truncating, so blended pixels stop being biased downwards by up to one step per channel. Releases 1.5 through 1.8 needed nothing, which is worth recording because it looks like they should. 1.5 and 1.6 changed only the API and performance. 1.7's "centre direction bias" turned a hard-coded 4 into a configuration field whose default is also 4, and its asymmetric colour distance was an artefact of packing channel differences into a byte-indexed lookup table — this port calls a colour metric directly and never had it. 1.8's transparent-outside-area rule is already reachable, per axis and configurably, through OutOfBoundsMode. 1.9 is registered as its own entry, "xBRZ 1.9", following XBR / XBR NoBlend / XBR 3x Original. The existing xBRZ entry keeps its name, its public constructor and its pixels: a version selector that quietly changes what the existing entry produces would be a regression for every script and saved setting naming it. Its output at all five scales is pinned by hash against what it produced before this commit. The variant rides as a struct type parameter on the per-scale kernels, so both knobs fold to constants when the kernel is specialised and the pre-1.9 path takes no branch for the privilege. Two further variants carry one change each; they are internal and exist so a failing test names which of the two changes broke, rather than leaving either free to go missing while the other still makes a difference. Xbrz-VersionNotes.md records the decision the derived specification left open — why a rounding lerp type and kernel-local arithmetic were both rejected in favour of the new ILerp member — and what pins each property.
This was referenced Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Hawkynt/2dimagefilter#30 from the derived specification merged in #22. Rung 2 throughout: the specification was the input, none of Zenju's code was read into this.
What 1.9 actually changed
Two things, both exact and integer:
x * 22 <= y * 10becoming* 24. Ten sites — the shallow and the steep test in each of the five scale kernels. Fewer near-diagonal runs now count as lines.What turned out not to be a gap
Worth stating, because all three look like gaps:
centerDirectionBias(1.7)4into a config field defaulting to4. This port already uses4.OutOfBoundsModealready exposes this per axis, configurably.So the existing
xBRZentry was 1.7-equivalent, and 1.5/1.6 changed only API and performance.The design question #22 left open
ILerpis picked by the pipeline —Color4BLerpInt<Bgra8888>is hard-coded at every shared call site — so the 1.9 path cannot select a rounding lerp type.Xbrz.InvokeKernelimplementsIRescaler, so its constraints are fixed by that interface, and it cannot substitute one itself either:TWorkis generic there, so it cannot know whether the 3- or the 4-component rounding struct is right. Rounding inside the kernels was the other option and means not callinglerpat all — open-coding the channel arithmetic and discarding the colour-space abstraction.So
ILerp<T>grew a rounding counterpart method instead,LerpRounded, in both overloads. The kernels keep blending through the interface and call the member their variant calls for.The members are declared, not defaulted. Default interface methods would have kept the interface source-compatible for outside implementors, but this package targets down to net35, and net35–net48 have no runtime support — the compiler rejects them outright with CS8701.
BatchDistanceDefaultsalready exists here for exactly this reason. All eleven shipped implementations implement them explicitly, which also keeps them off the boxing path; these are hot loops reached through astructgeneric constraint. Float-based and no-op blenders forward to the truncating member, with a comment saying why.Outside implementors of
ILerp<T>must add the two members. There is no alternative on these frameworks; the members are documented with the behaviour a forwarding implementation reproduces.Registration
Xbrz19is its own[ScalerInfo("xBRZ 1.9")]entry, followingXBR/XBR NoBlend/XBR 3x Original. The existingxBRZentry keeps its name, its public constructor and its pixels — a version selector that quietly changed the default would be a regression for every script and saved setting naming it. The variant rides as a struct type parameter on the per-scale kernels, so both knobs fold to constants on specialisation and the pre-1.9 path takes no branch.Verification
All three properties are exact, so all three are pinned exactly.
Two internal single-change variants make that attribution possible; asserting only against combined 1.9 would let either change go missing unnoticed while the other still produced a difference.
Every new assertion was confirmed to bite by perturbing what it tests — restoring a
22at one of the ten sites, dropping a rounding term, and pointing the public entry at 1.9 — and checking the matching test fails, and only the matching test.1901 → 1936 tests, green.
2dimagefilterbuilds against this branch and goes 665 → 667, green.