Skip to content

+ xBRZ 1.9 as its own scaler entry - #23

Merged
Hawkynt merged 2 commits into
mainfrom
feat/xbrz-1.9
Sep 21, 2026
Merged

Hawkynt merged 2 commits into
mainfrom
feat/xbrz-1.9

Conversation

@Hawkynt

@Hawkynt Hawkynt commented Sep 21, 2026

Copy link
Copy Markdown
Owner

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:

  1. Steep direction threshold 2.2 → 2.4, encoded here as the integer ratio x * 22 <= y * 10 becoming * 24. Ten sites — the shallow and the steep test in each of the five scale kernels. Fewer near-diagonal runs now count as lines.
  2. Weighted and 50/50 blends round to nearest instead of truncating, so blended pixels stop being biased downwards by up to one step per channel.

What turned out not to be a gap

Worth stating, because all three look like gaps:

Suspected Verdict
centerDirectionBias (1.7) no — 1.7 turned a hard-coded 4 into a config field defaulting to 4. This port already uses 4.
Asymmetric colour distance (1.7) no — an artefact of packing channel differences into a byte-indexed lookup table. This port calls a colour metric directly and never had it.
ARGB outside area transparent (1.8) no — OutOfBoundsMode already exposes this per axis, configurably.

So the existing xBRZ entry was 1.7-equivalent, and 1.5/1.6 changed only API and performance.

The design question #22 left open

ILerp is 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.InvokeKernel implements IRescaler, so its constraints are fixed by that interface, and it cannot substitute one itself either: TWork is 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 calling lerp at 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. BatchDistanceDefaults already 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 a struct generic 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

Xbrz19 is its own [ScalerInfo("xBRZ 1.9")] entry, 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 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.

  • The existing entry does not move — output at all five scales pinned by SHA-256 against hashes captured from the code before this branch existed. Stable across net45, net48, netcoreapp3.1, net9.0, x86 and x64.
  • Threshold — a source whose accumulated distance ratios straddle 2.2 and 2.4, compared between the two thresholds with blending held at truncating, so a difference can only be the threshold's.
  • Rounding — blends whose weighted sum leaves a non-zero remainder against the total weight, covering the weighted and the 50/50 path, plus a kernel-level comparison with the threshold held at 2.2.

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 22 at 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. 2dimagefilter builds against this branch and goes 665 → 667, green.

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.
@Hawkynt
Hawkynt merged commit be3e13c into main Sep 21, 2026
9 checks passed
@Hawkynt
Hawkynt deleted the feat/xbrz-1.9 branch September 21, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant