From cff6b618c40468320c5e9e1eb7d23f8c558c68fd Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Mon, 21 Sep 2026 07:12:25 +0200 Subject: [PATCH 1/2] + a rounding counterpart for both ILerp blends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- .../ColorProcessing/ColorMath/ColorLerp.cs | 64 ++++++++ .../ColorProcessing/ColorMath/ColorLerpInt.cs | 148 ++++++++++++++++++ .../ColorProcessing/ColorMath/ILerp.cs | 42 +++++ 3 files changed, 254 insertions(+) diff --git a/System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerp.cs b/System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerp.cs index 8dc2e47a..3c726903 100644 --- a/System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerp.cs +++ b/System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerp.cs @@ -47,6 +47,24 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + /// + /// The channels are floating point, so the blend never truncates to a representable step in the + /// first place and the rounded blend is literally the same operation. Forwarding keeps the two + /// modes from drifting apart. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b); + + /// + /// + /// The channels are floating point, so the blend never truncates to a representable step in the + /// first place and the rounded blend is literally the same operation. Forwarding keeps the two + /// modes from drifting apart. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => this.Lerp(a, b, w1, w2); + } /// @@ -76,6 +94,24 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + /// + /// The channels are floating point, so the blend never truncates to a representable step in the + /// first place and the rounded blend is literally the same operation. Forwarding keeps the two + /// modes from drifting apart. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b); + + /// + /// + /// The channels are floating point, so the blend never truncates to a representable step in the + /// first place and the rounded blend is literally the same operation. Forwarding keeps the two + /// modes from drifting apart. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => this.Lerp(a, b, w1, w2); + } /// @@ -107,6 +143,24 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + /// + /// The channels are floating point, so the blend never truncates to a representable step in the + /// first place and the rounded blend is literally the same operation. Forwarding keeps the two + /// modes from drifting apart. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b); + + /// + /// + /// The channels are floating point, so the blend never truncates to a representable step in the + /// first place and the rounded blend is literally the same operation. Forwarding keeps the two + /// modes from drifting apart. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => this.Lerp(a, b, w1, w2); + } /// @@ -126,4 +180,14 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TWork Lerp(in TWork a, in TWork b, int w1, int w2) => a; + + /// + /// Nothing is divided, so there is nothing to round. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) => a; + + /// + /// Nothing is divided, so there is nothing to round. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => a; } diff --git a/System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerpInt.cs b/System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerpInt.cs index c4ad7d88..f17d24ef 100644 --- a/System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerpInt.cs +++ b/System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerpInt.cs @@ -52,6 +52,27 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) + => ColorFactory.Create3B( + (byte)((a.C1 + b.C1 + 1) >> 1), + (byte)((a.C2 + b.C2 + 1) >> 1), + (byte)((a.C3 + b.C3 + 1) >> 1) + ); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) { + var total = w1 + w2; + var half = total >> 1; + return ColorFactory.Create3B( + (byte)((a.C1 * w1 + b.C1 * w2 + half) / total), + (byte)((a.C2 * w1 + b.C2 * w2 + half) / total), + (byte)((a.C3 * w1 + b.C3 * w2 + half) / total) + ); + } + } /// @@ -84,6 +105,29 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) + => ColorFactory.Create4B( + (byte)((a.C1 + b.C1 + 1) >> 1), + (byte)((a.C2 + b.C2 + 1) >> 1), + (byte)((a.C3 + b.C3 + 1) >> 1), + (byte)((a.A + b.A + 1) >> 1) + ); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) { + var total = w1 + w2; + var half = total >> 1; + return ColorFactory.Create4B( + (byte)((a.C1 * w1 + b.C1 * w2 + half) / total), + (byte)((a.C2 * w1 + b.C2 * w2 + half) / total), + (byte)((a.C3 * w1 + b.C3 * w2 + half) / total), + (byte)((a.A * w1 + b.A * w2 + half) / total) + ); + } + } /// @@ -118,6 +162,31 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) + => ColorFactory.Create5B( + (byte)((a.C1 + b.C1 + 1) >> 1), + (byte)((a.C2 + b.C2 + 1) >> 1), + (byte)((a.C3 + b.C3 + 1) >> 1), + (byte)((a.C4 + b.C4 + 1) >> 1), + (byte)((a.A + b.A + 1) >> 1) + ); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) { + var total = w1 + w2; + var half = total >> 1; + return ColorFactory.Create5B( + (byte)((a.C1 * w1 + b.C1 * w2 + half) / total), + (byte)((a.C2 * w1 + b.C2 * w2 + half) / total), + (byte)((a.C3 * w1 + b.C3 * w2 + half) / total), + (byte)((a.C4 * w1 + b.C4 * w2 + half) / total), + (byte)((a.A * w1 + b.A * w2 + half) / total) + ); + } + } /// @@ -158,6 +227,29 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + /// + /// already rounds a half-step upwards, so the 50/50 blend is the + /// same operation in both modes and this simply forwards. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) { + var (a1, a2, a3, aa) = a.ToNormalized(); + var (b1, b2, b3, ba) = b.ToNormalized(); + var total = (ulong)(w1 + w2); + var half = total >> 1; + return ColorFactory.FromNormalized_4( + UNorm32.FromRaw((uint)(((ulong)a1.RawValue * (uint)w1 + (ulong)b1.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)a2.RawValue * (uint)w1 + (ulong)b2.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)a3.RawValue * (uint)w1 + (ulong)b3.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)aa.RawValue * (uint)w1 + (ulong)ba.RawValue * (uint)w2 + half) / total)) + ); + } + } /// @@ -196,6 +288,28 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + /// + /// already rounds a half-step upwards, so the 50/50 blend is the + /// same operation in both modes and this simply forwards. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) { + var (a1, a2, a3) = a.ToNormalized(); + var (b1, b2, b3) = b.ToNormalized(); + var total = (ulong)(w1 + w2); + var half = total >> 1; + return ColorFactory.FromNormalized_3( + UNorm32.FromRaw((uint)(((ulong)a1.RawValue * (uint)w1 + (ulong)b1.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)a2.RawValue * (uint)w1 + (ulong)b2.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)a3.RawValue * (uint)w1 + (ulong)b3.RawValue * (uint)w2 + half) / total)) + ); + } + } /// @@ -238,6 +352,30 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { ); } + /// + /// + /// already rounds a half-step upwards, so the 50/50 blend is the + /// same operation in both modes and this simply forwards. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) { + var (a1, a2, a3, a4, aa) = a.ToNormalized(); + var (b1, b2, b3, b4, ba) = b.ToNormalized(); + var total = (ulong)(w1 + w2); + var half = total >> 1; + return ColorFactory.FromNormalized_5( + UNorm32.FromRaw((uint)(((ulong)a1.RawValue * (uint)w1 + (ulong)b1.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)a2.RawValue * (uint)w1 + (ulong)b2.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)a3.RawValue * (uint)w1 + (ulong)b3.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)a4.RawValue * (uint)w1 + (ulong)b4.RawValue * (uint)w2 + half) / total)), + UNorm32.FromRaw((uint)(((ulong)aa.RawValue * (uint)w1 + (ulong)ba.RawValue * (uint)w2 + half) / total)) + ); + } + } /// @@ -257,4 +395,14 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TWork Lerp(in TWork a, in TWork b, int w1, int w2) => a; + + /// + /// Nothing is divided, so there is nothing to round. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b) => a; + + /// + /// Nothing is divided, so there is nothing to round. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => a; } diff --git a/System.Drawing.Extensions/ColorProcessing/ColorMath/ILerp.cs b/System.Drawing.Extensions/ColorProcessing/ColorMath/ILerp.cs index bcaca740..7b129cf0 100644 --- a/System.Drawing.Extensions/ColorProcessing/ColorMath/ILerp.cs +++ b/System.Drawing.Extensions/ColorProcessing/ColorMath/ILerp.cs @@ -27,6 +27,21 @@ namespace Hawkynt.ColorProcessing.ColorMath; /// Used in scaling algorithms and color blending operations. /// Implementations should operate in linear color space for correct results. /// Uses integer weights for all interpolation to ensure pure integer operations in fast mode. +/// +/// Each blend comes in two rounding modes. Lerp truncates the division, which is what +/// fixed-point blenders have always done here; LerpRounded rounds to nearest instead. The +/// pair exists because some algorithms are specified against one mode or the other — xBRZ, for +/// example, truncates up to release 1.8 and rounds from 1.9 on — and the caller picks the lerp +/// implementation long before the algorithm gets to say which mode it wants. +/// +/// +/// The rounded members are declared, not defaulted. C# 8 default interface methods would keep the +/// interface source-compatible for outside implementors, but they need runtime support this package +/// cannot assume: net35..net48 do not have it and the compiler rejects them outright (CS8701). The +/// same constraint is why exists. An implementation for +/// which rounding is meaningless — anything in floating point, or a no-op blender — should forward +/// the rounded member to the truncating one. +/// /// public interface ILerp where T : unmanaged { @@ -47,4 +62,31 @@ public interface ILerp where T : unmanaged { /// The weight for the second color. /// The weighted blend: (a * w1 + b * w2) / (w1 + w2). T Lerp(in T a, in T b, int w1, int w2); + + /// + /// Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. + /// + /// The first color. + /// The second color. + /// The color at the midpoint between a and b, rounded rather than truncated. + /// + /// Differs from only where the midpoint falls between two + /// representable values; for a fixed-point channel that is a difference of at most one step. + /// + T LerpRounded(in T a, in T b); + + /// + /// Linearly interpolates between two colors with integer weights, rounding to nearest. + /// + /// The first color. + /// The second color. + /// The weight for the first color. + /// The weight for the second color. + /// The weighted blend (a * w1 + b * w2) / (w1 + w2), rounded rather than truncated. + /// + /// Differs from whenever the weighted sum leaves a + /// non-zero remainder against the total weight. Truncation biases every such blend downwards; + /// rounding does not. + /// + T LerpRounded(in T a, in T b, int w1, int w2); } From 00b10e089139abc9b7450ea7f5861476eb4ad828 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Mon, 21 Sep 2026 07:12:37 +0200 Subject: [PATCH 2/2] + xBRZ 1.9 as its own scaler entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Resizing/Rescalers/Xbrz-VersionNotes.md | 78 ++- .../Resizing/Rescalers/Xbrz.cs | 574 ++++++++++++++---- System.Drawing.Extensions/REFERENCE.md | 47 +- System.Drawing.Extensions/Readme.md | 5 +- .../ColorProcessing/XbrzVersionTests.cs | 396 ++++++++++++ 5 files changed, 955 insertions(+), 145 deletions(-) create mode 100644 Tests/System.Drawing.Extensions.Tests/ColorProcessing/XbrzVersionTests.cs diff --git a/System.Drawing.Extensions/ColorProcessing/Resizing/Rescalers/Xbrz-VersionNotes.md b/System.Drawing.Extensions/ColorProcessing/Resizing/Rescalers/Xbrz-VersionNotes.md index f1077ca3..d8385ca0 100644 --- a/System.Drawing.Extensions/ColorProcessing/Resizing/Rescalers/Xbrz-VersionNotes.md +++ b/System.Drawing.Extensions/ColorProcessing/Resizing/Rescalers/Xbrz-VersionNotes.md @@ -71,22 +71,62 @@ and anti-aliased edges come out marginally lighter and less biased downwards. `ColorLerpInt` currently truncates — `(a.C1 * w1 + b.C1 * w2) / total` — so this library matches pre-1.9 behaviour and a 1.9 variant needs a rounding blend. -## What an implementation has to do - -1. Add a variant selector to `Xbrz` and thread it into the per-scale kernels. -2. For the 1.9 variant use the `24` numerator in both steep/shallow comparisons. -3. For the 1.9 variant round the weighted blend to nearest instead of truncating. `ILerp` is chosen - by the pipeline rather than by the scaler, so this needs a decision: either a rounding - counterpart to `Color3BLerpInt`/`Color4BLerpInt` that the 1.9 path selects, or a rounding - operation added to the kernels themselves. -4. Register the variant as its own `[ScalerInfo]` entry, alongside `XBR` / `XBR NoBlend` / - `XBR 3x Original`, which already establish that graphically distinct variants get their own - names. Leave the existing `xBRZ` entry's name unchanged so existing scripts and settings keep - working. - -## Checking it - -Neither change can be verified by eye alone. Both are exact and integer, so they can be pinned -directly: the threshold change by inputs whose two accumulated distances straddle the 2.2 and 2.4 -ratios, and the rounding change by a blend whose weighted sum has a non-zero remainder against `N`. -The existing variant must stay bit-identical, which a comparison against its current output pins. +## What was implemented + +1. A variant is carried as a struct type parameter `TVariant` on each per-scale kernel, so both + knobs fold to constants when the kernel is specialised and the pre-1.9 path takes no branch. + `Xbrz` selects it through an internal constructor; its public constructor is unchanged. +2. The 1.9 variant uses the `24` numerator in both steep/shallow comparisons, at all ten sites. +3. The 1.9 variant rounds both the weighted and the 50/50 blend. See the decision below. +4. `Xbrz19` is registered as `[ScalerInfo("xBRZ 1.9")]`, its own entry alongside `XBR` / + `XBR NoBlend` / `XBR 3x Original`. The existing `xBRZ` entry keeps its name and its pixels. + +## The design decision, and why + +`ILerp` is picked by the pipeline, not by the scaler: `Color4BLerpInt` is hard-coded at +every shared call site in `BitmapScalerExtensions` and `BitmapFilterExtensions`. Two options were +considered in this document and both were rejected. + +A rounding counterpart *type* that the 1.9 path selects cannot work. `Xbrz.InvokeKernel` implements +`IRescaler`, so its constraints are fixed by that interface and it receives whatever `TLerp` the +call site chose; it also cannot substitute one itself, because `TWork` is generic there and it +cannot know whether the 3-component or the 4-component rounding struct is the right one. Rounding +inside the kernels was rejected too: it means not calling `lerp` at all and open-coding the channel +arithmetic, discarding the colour-space abstraction the interface exists to provide. + +**What was done instead: `ILerp` grew a rounding counterpart *method*, `LerpRounded`, in both +overloads.** The kernels keep blending through the interface and simply call the member matching +their variant. + +The members are declared, not defaulted. Default interface methods would have kept the interface +source-compatible for outside implementors, but they need runtime support this package does not +have: it targets down to net35, and net35 through net48 make the compiler reject them outright +(CS8701). `BatchDistanceDefaults` already exists in this package for the same reason. All eleven +shipped implementations implement the new members explicitly, which also keeps them off the boxing +path — these are hot loops reached through a `struct` generic constraint. Implementations for which +rounding is meaningless forward to the truncating member and say so in a comment: the float-based +lerps, where the blend never truncated to a representable step, and the no-op lerps. + +Outside implementors of `ILerp` must add the two members. That is a source break with no +alternative on these target frameworks, and it is why the members are documented with the behaviour +a forwarding implementation reproduces. + +## How it is checked + +`XbrzVersionTests` pins all three properties exactly, since all three are exact and integer. + +- **The existing entry does not move.** Its output at all five scales is pinned by SHA-256 against + hashes captured from the code *before* 1.9 support existed. Verified stable across net45, net48, + netcoreapp3.1 and net9.0, and across x86 and x64. +- **The threshold.** A source whose accumulated distance ratios straddle 2.2 and 2.4, compared + between the 2.2 and 2.4 kernels with blending held at the truncating mode, so a difference can + only be the threshold's. +- **The rounding.** Blends whose weighted sum leaves a non-zero remainder against the total weight, + covering both the weighted and the 50/50 path, plus a kernel-level comparison with the threshold + held at 2.2. + +The two single-change variants exist only for that attribution: asserting against the combined 1.9 +variant alone would let either change go missing unnoticed, because the other still produces a +difference. Every assertion was confirmed to bite by perturbing what it tests — restoring a `22`, +dropping a rounding term, and pointing the public entry at 1.9 — and checking the matching test +fails. diff --git a/System.Drawing.Extensions/ColorProcessing/Resizing/Rescalers/Xbrz.cs b/System.Drawing.Extensions/ColorProcessing/Resizing/Rescalers/Xbrz.cs index e259019b..ddeeb084 100644 --- a/System.Drawing.Extensions/ColorProcessing/Resizing/Rescalers/Xbrz.cs +++ b/System.Drawing.Extensions/ColorProcessing/Resizing/Rescalers/Xbrz.cs @@ -1,4 +1,4 @@ -#region (c)2010-2042 Hawkynt +#region (c)2010-2042 Hawkynt // This file is part of Hawkynt's .NET Framework extensions. // @@ -31,6 +31,29 @@ namespace Hawkynt.ColorProcessing.Resizing.Rescalers; #region Xbrz (Unified) +/// +/// Which xBRZ release's output a scaler instance reproduces. +/// +/// +/// Internal because the public surface exposes the two shipping variants as their own scaler types +/// ( and ). The two single-change members exist only so tests +/// can attribute an output difference to one of 1.9's two changes rather than to both at once. +/// +internal enum XbrzVariant { + + /// xBRZ as released up to and including 1.8. + Classic = 0, + + /// xBRZ 1.9: a 2.4 steep threshold and blends rounded to nearest. + Version19 = 1, + + /// Test-only: 1.9's steep threshold, pre-1.9 truncating blends. + SteepThresholdOnly = 2, + + /// Test-only: 1.9's rounded blends, pre-1.9 steep threshold. + RoundedBlendsOnly = 3 +} + /// /// xBRZ pixel-art scaling algorithm by Zenju (2x, 3x, 4x, 5x, 6x). /// @@ -38,6 +61,12 @@ namespace Hawkynt.ColorProcessing.Resizing.Rescalers; /// Extension of XBR algorithm with improved edge detection and blending. /// Uses YCbCr color distance with 4-rotation symmetric pattern analysis. /// Key parameters: luminanceWeight=1, equalColorTolerance=30, dominantDirectionThreshold=3.6, steepDirectionThreshold=2.2 +/// +/// Reproduces the algorithm as released up to and including 1.8; see for +/// release 1.9, which raised the steep threshold to 2.4 and rounds its blends. The two are +/// separate scalers rather than a switch on this one so that existing scripts and saved settings +/// naming xBRZ keep getting exactly the pixels they got before. +/// /// Reference: https://sourceforge.net/projects/xbrz /// [ScalerInfo("xBRZ", Author = "Zenju", Year = 2012, @@ -46,15 +75,28 @@ namespace Hawkynt.ColorProcessing.Resizing.Rescalers; public readonly struct Xbrz : IRescaler { private readonly int _scale; + private readonly XbrzVariant _variant; /// /// Creates an xBRZ scaler with specified scale factor. /// /// The scale factor (2, 3, 4, 5, or 6). - public Xbrz(int scale = 2) { + /// + /// Reproduces xBRZ as released up to and including 1.8. For release 1.9 use + /// ; this type's output is unchanged and stays that way. + /// + public Xbrz(int scale = 2) : this(scale, XbrzVariant.Classic) { } + + /// + /// Creates an xBRZ scaler reproducing a specific release's output. + /// + /// The scale factor (2, 3, 4, 5, or 6). + /// Which release's behaviour to reproduce. + internal Xbrz(int scale, XbrzVariant variant) { ArgumentOutOfRangeException.ThrowIfLessThan(scale, 2); ArgumentOutOfRangeException.ThrowIfGreaterThan(scale, 6); this._scale = scale; + this._variant = variant; } /// @@ -72,13 +114,11 @@ public TResult InvokeKernel where TLerp : struct, ILerp where TEncode : struct, IEncode - => this._scale switch { - 0 or 2 => callback.Invoke(new Xbrz2xKernel(equality, default, lerp)), - 3 => callback.Invoke(new Xbrz3xKernel(equality, default, lerp)), - 4 => callback.Invoke(new Xbrz4xKernel(equality, default, lerp)), - 5 => callback.Invoke(new Xbrz5xKernel(equality, default, lerp)), - 6 => callback.Invoke(new Xbrz6xKernel(equality, default, lerp)), - _ => throw new InvalidOperationException($"Invalid scale factor: {this._scale}") + => this._variant switch { + XbrzVariant.Version19 => XbrzDispatch.Invoke(this._scale, callback, equality, lerp), + XbrzVariant.SteepThresholdOnly => XbrzDispatch.Invoke(this._scale, callback, equality, lerp), + XbrzVariant.RoundedBlendsOnly => XbrzDispatch.Invoke(this._scale, callback, equality, lerp), + _ => XbrzDispatch.Invoke(this._scale, callback, equality, lerp) }; /// @@ -121,6 +161,131 @@ public TResult InvokeKernel Scale2x; } +/// +/// Picks the per-scale kernel and closes it over a variant. +/// +/// +/// A file-local type because the variant constraint names a file-local interface, which C# does not +/// allow in the signature of a non-file-local member. +/// +file static class XbrzDispatch { + + public static TResult Invoke( + int scale, + IKernelCallback callback, + TEquality equality, + TLerp lerp) + where TWork : unmanaged, IColorSpace + where TKey : unmanaged, IColorSpace + where TPixel : unmanaged, IStorageSpace + where TDistance : struct, IColorMetric, INormalizedMetric + where TEquality : struct, IColorEquality + where TLerp : struct, ILerp + where TEncode : struct, IEncode + where TVariant : struct, IXbrzVariant + => scale switch { + 0 or 2 => callback.Invoke(new Xbrz2xKernel(equality, default, lerp)), + 3 => callback.Invoke(new Xbrz3xKernel(equality, default, lerp)), + 4 => callback.Invoke(new Xbrz4xKernel(equality, default, lerp)), + 5 => callback.Invoke(new Xbrz5xKernel(equality, default, lerp)), + 6 => callback.Invoke(new Xbrz6xKernel(equality, default, lerp)), + _ => throw new InvalidOperationException($"Invalid scale factor: {scale}") + }; +} + +#endregion + +#region Xbrz 1.9 + +/// +/// xBRZ as released in 1.9 (2x, 3x, 4x, 5x, 6x). +/// +/// +/// +/// Differs from in exactly two ways, both of which 1.9 introduced. The +/// steep/shallow direction threshold rises 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 instead of truncating, so blended +/// pixels stop being biased downwards by up to one step per channel. +/// +/// +/// Releases 1.5 through 1.8 needed nothing: 1.5 and 1.6 changed only the API and performance, 1.7's +/// centre direction bias made a hard-coded 4 configurable with 4 as its default and its asymmetric +/// colour distance was an artefact of a lookup table this port never used, and 1.8's +/// transparent-outside-area rule is already reachable per axis through OutOfBoundsMode. See +/// Xbrz-VersionNotes.md next to this file for the derivation. +/// +/// Reference: https://sourceforge.net/projects/xbrz +/// +[ScalerInfo("xBRZ 1.9", Author = "Zenju", Year = 2026, + Description = "xBRZ 1.9 — 2.4 steep threshold and blends rounded to nearest", + Category = ScalerCategory.Rescaler, + Url = "https://sourceforge.net/projects/xbrz")] +public readonly struct Xbrz19 : IRescaler { + + private readonly int _scale; + + /// + /// Creates an xBRZ 1.9 scaler with the specified scale factor. + /// + /// The scale factor (2, 3, 4, 5, or 6). + public Xbrz19(int scale = 2) { + ArgumentOutOfRangeException.ThrowIfLessThan(scale, 2); + ArgumentOutOfRangeException.ThrowIfGreaterThan(scale, 6); + this._scale = scale; + } + + /// + public ScaleFactor Scale => this._scale == 0 ? new(2, 2) : new(this._scale, this._scale); + + /// + public TResult InvokeKernel( + IKernelCallback callback, + TEquality equality = default, + TLerp lerp = default) + where TWork : unmanaged, IColorSpace + where TKey : unmanaged, IColorSpace + where TPixel : unmanaged, IStorageSpace + where TDistance : struct, IColorMetric, INormalizedMetric + where TEquality : struct, IColorEquality + where TLerp : struct, ILerp + where TEncode : struct, IEncode + => new Xbrz(this._scale == 0 ? 2 : this._scale, XbrzVariant.Version19) + .InvokeKernel(callback, equality, lerp); + + /// Gets the list of scale factors supported by xBRZ 1.9. + public static ScaleFactor[] SupportedScales { get; } = [new(2, 2), new(3, 3), new(4, 4), new(5, 5), new(6, 6)]; + + /// Determines whether xBRZ 1.9 supports the specified scale factor. + public static bool SupportsScale(ScaleFactor scale) => scale is { X: 2, Y: 2 } or { X: 3, Y: 3 } or { X: 4, Y: 4 } or { X: 5, Y: 5 } or { X: 6, Y: 6 }; + + /// Enumerates all possible target dimensions for xBRZ 1.9. + public static IEnumerable<(int Width, int Height)> GetPossibleTargets(int sourceWidth, int sourceHeight) { + yield return (sourceWidth * 2, sourceHeight * 2); + yield return (sourceWidth * 3, sourceHeight * 3); + yield return (sourceWidth * 4, sourceHeight * 4); + yield return (sourceWidth * 5, sourceHeight * 5); + yield return (sourceWidth * 6, sourceHeight * 6); + } + + /// Gets an xBRZ 1.9 2x scaler. + public static Xbrz19 Scale2x => new(2); + + /// Gets an xBRZ 1.9 3x scaler. + public static Xbrz19 Scale3x => new(3); + + /// Gets an xBRZ 1.9 4x scaler. + public static Xbrz19 Scale4x => new(4); + + /// Gets an xBRZ 1.9 5x scaler. + public static Xbrz19 Scale5x => new(5); + + /// Gets an xBRZ 1.9 6x scaler. + public static Xbrz19 Scale6x => new(6); + + /// Gets the default xBRZ 1.9 configuration (2x). + public static Xbrz19 Default => Scale2x; +} + #endregion #region xBRZ Algorithm Helpers @@ -192,12 +357,75 @@ public static byte Rotate(byte b, int rotDeg) { } } +/// +/// The knobs that differ between the xBRZ releases this port reproduces. +/// +/// +/// +/// Carried as a struct type parameter rather than a field so each kernel is specialised per +/// variant and both members fold to constants, leaving the inner loop with no version branch. +/// +/// +/// Only the two knobs that actually change output are here. See +/// Xbrz-VersionNotes.md next to this file for why 1.5 through 1.8 need none. +/// +/// +file interface IXbrzVariant { + + /// + /// Gets the numerator of the steep/shallow direction threshold expressed against a denominator + /// of 10: 22 for the 2.2 threshold used up to 1.8, 24 for the 2.4 threshold of 1.9. + /// + ulong SteepNumerator { get; } + + /// + /// Gets whether weighted and 50/50 blends round to nearest (1.9) rather than truncate (up to 1.8). + /// + bool RoundsBlends { get; } +} + +/// +/// xBRZ as released up to and including 1.8 — a 2.2 steep threshold and truncating blends. +/// +file readonly struct XbrzClassicVariant : IXbrzVariant { + public ulong SteepNumerator => 22; + public bool RoundsBlends => false; +} + +/// +/// xBRZ 1.9 — a 2.4 steep threshold and blends rounded to nearest. +/// +file readonly struct Xbrz19Variant : IXbrzVariant { + public ulong SteepNumerator => 24; + public bool RoundsBlends => true; +} + +/// +/// 1.9's steep threshold with the pre-1.9 truncating blends. Exists so a test can attribute an +/// output difference to the threshold alone; not reachable through the public surface. +/// +file readonly struct XbrzSteepOnlyVariant : IXbrzVariant { + public ulong SteepNumerator => 24; + public bool RoundsBlends => false; +} + +/// +/// 1.9's rounded blends with the pre-1.9 steep threshold. Exists so a test can attribute an output +/// difference to the rounding alone; not reachable through the public surface. +/// +file readonly struct XbrzRoundOnlyVariant : IXbrzVariant { + public ulong SteepNumerator => 22; + public bool RoundsBlends => true; +} + /// /// xBRZ configuration and helper methods. /// file static class XbrzHelpers { public const float LuminanceWeight = 1.0f; public const float DominantDirectionThreshold = 3.6f; + + /// The steep/shallow direction threshold up to release 1.8; 1.9 raised it to 2.4. public const float SteepDirectionThreshold = 2.2f; [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -399,7 +627,7 @@ static Rot3x3() { #region xBRZ 2x Kernel -file readonly struct Xbrz2xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) +file readonly struct Xbrz2xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) : IScaler where TWork : unmanaged, IColorSpace where TKey : unmanaged, IColorSpace @@ -407,7 +635,8 @@ file readonly struct Xbrz2xKernel where TMetric : struct, IColorMetric where TLerp : struct, ILerp - where TEncode : struct, IEncode { + where TEncode : struct, IEncode + where TVariant : struct, IXbrzVariant { public int ScaleX => 2; public int ScaleY => 2; @@ -444,6 +673,24 @@ public unsafe void Scale(in NeighborWindow window, TPixel* dest, in ScalePixel2x(ker, work, blendXy, rot, dest, destStride, encoder, lerp, metric, equality); } + /// + /// Applies a weighted blend in the rounding mode this variant calls for. + /// + /// + /// is a struct type parameter, so the branch is resolved when the + /// kernel is specialised and costs the pre-1.9 path nothing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b, int w1, int w2) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b, w1, w2) : lerp.Lerp(a, b, w1, w2); + + /// + /// Applies a 50/50 blend in the rounding mode this variant calls for. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b) : lerp.Lerp(a, b); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ScalePixel2x( TKey[] ker, TWork[] work, @@ -488,15 +735,16 @@ private static unsafe void ScalePixel2x( if (!doLineBlend) { // BlendCorner for 2x: blend pixel (1,1) with weight 21/100 - dest[ri1 * destStride + rj1] = encoder.Encode(lerp.Lerp(we, px, 79, 21)); + dest[ri1 * destStride + rj1] = encoder.Encode(Blend(lerp, we, px, 79, 21)); return; } var fg = metric.Distance(f, g); var hc = metric.Distance(h, c); + var steepNumerator = default(TVariant).SteepNumerator; - var haveShallowLine = (ulong)fg.RawValue * 22 <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold (2.2) as integer ratio - var haveSteepLine = (ulong)hc.RawValue * 22 <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold (2.2) as integer ratio + var haveShallowLine = (ulong)fg.RawValue * steepNumerator <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on + var haveSteepLine = (ulong)hc.RawValue * steepNumerator <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on var (ri0, rj1_0) = RotationLookup.Get(2, rotDeg, 1, 0); var (ri1_0, rj0) = RotationLookup.Get(2, rotDeg, 0, 1); @@ -504,22 +752,22 @@ private static unsafe void ScalePixel2x( if (haveShallowLine) { if (haveSteepLine) { // BlendLineSteepAndShallow - dest[ri0 * destStride + rj1_0] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri1_0 * destStride + rj0] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri1 * destStride + rj1] = encoder.Encode(lerp.Lerp(we, px, 1, 5)); + dest[ri0 * destStride + rj1_0] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri1_0 * destStride + rj0] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri1 * destStride + rj1] = encoder.Encode(Blend(lerp, we, px, 1, 5)); } else { // BlendLineShallow - dest[ri0 * destStride + rj1_0] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri1 * destStride + rj1] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri0 * destStride + rj1_0] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri1 * destStride + rj1] = encoder.Encode(Blend(lerp, we, px, 1, 3)); } } else { if (haveSteepLine) { // BlendLineSteep - dest[ri1_0 * destStride + rj0] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri1 * destStride + rj1] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri1_0 * destStride + rj0] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri1 * destStride + rj1] = encoder.Encode(Blend(lerp, we, px, 1, 3)); } else { // BlendLineDiagonal - dest[ri1 * destStride + rj1] = encoder.Encode(lerp.Lerp(we, px)); + dest[ri1 * destStride + rj1] = encoder.Encode(Blend(lerp, we, px)); } } } @@ -529,7 +777,7 @@ private static unsafe void ScalePixel2x( #region xBRZ 3x Kernel -file readonly struct Xbrz3xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) +file readonly struct Xbrz3xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) : IScaler where TWork : unmanaged, IColorSpace where TKey : unmanaged, IColorSpace @@ -537,7 +785,8 @@ file readonly struct Xbrz3xKernel where TMetric : struct, IColorMetric where TLerp : struct, ILerp - where TEncode : struct, IEncode { + where TEncode : struct, IEncode + where TVariant : struct, IXbrzVariant { public int ScaleX => 3; public int ScaleY => 3; @@ -572,6 +821,24 @@ public unsafe void Scale(in NeighborWindow window, TPixel* dest, in ScalePixel3x(ker, work, blendXy, rot, dest, destStride, encoder, lerp, metric, equality); } + /// + /// Applies a weighted blend in the rounding mode this variant calls for. + /// + /// + /// is a struct type parameter, so the branch is resolved when the + /// kernel is specialised and costs the pre-1.9 path nothing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b, int w1, int w2) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b, w1, w2) : lerp.Lerp(a, b, w1, w2); + + /// + /// Applies a 50/50 blend in the rounding mode this variant calls for. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b) : lerp.Lerp(a, b); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ScalePixel3x( TKey[] ker, TWork[] work, @@ -614,15 +881,16 @@ private static unsafe void ScalePixel3x( if (!doLineBlend) { // BlendCorner for 3x: blend pixel (2,2) with weight 45/100 - dest[ri2j2i * destStride + ri2j2j] = encoder.Encode(lerp.Lerp(we, px, 11, 9)); + dest[ri2j2i * destStride + ri2j2j] = encoder.Encode(Blend(lerp, we, px, 11, 9)); return; } var fg = metric.Distance(f, g); var hc = metric.Distance(h, c); + var steepNumerator = default(TVariant).SteepNumerator; - var haveShallowLine = (ulong)fg.RawValue * 22 <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold (2.2) as integer ratio - var haveSteepLine = (ulong)hc.RawValue * 22 <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold (2.2) as integer ratio + var haveShallowLine = (ulong)fg.RawValue * steepNumerator <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on + var haveSteepLine = (ulong)hc.RawValue * steepNumerator <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on if (haveShallowLine) { if (haveSteepLine) { @@ -632,10 +900,10 @@ private static unsafe void ScalePixel3x( var (ri21, rj21) = RotationLookup.Get(3, rotDeg, 2, 1); var (ri12, rj12) = RotationLookup.Get(3, rotDeg, 1, 2); - dest[ri20 * destStride + rj20] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri02 * destStride + rj02] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri21 * destStride + rj21] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri12 * destStride + rj12] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri20 * destStride + rj20] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri02 * destStride + rj02] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri21 * destStride + rj21] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri12 * destStride + rj12] = encoder.Encode(Blend(lerp, we, px, 1, 3)); dest[ri2j2i * destStride + ri2j2j] = encoder.Encode(px); } else { // BlendLineShallow for 3x @@ -643,9 +911,9 @@ private static unsafe void ScalePixel3x( var (ri12, rj12) = RotationLookup.Get(3, rotDeg, 1, 2); var (ri21, rj21) = RotationLookup.Get(3, rotDeg, 2, 1); - dest[ri20 * destStride + rj20] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri12 * destStride + rj12] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri21 * destStride + rj21] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri20 * destStride + rj20] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri12 * destStride + rj12] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri21 * destStride + rj21] = encoder.Encode(Blend(lerp, we, px, 1, 3)); dest[ri2j2i * destStride + ri2j2j] = encoder.Encode(px); } } else { @@ -655,18 +923,18 @@ private static unsafe void ScalePixel3x( var (ri21, rj21) = RotationLookup.Get(3, rotDeg, 2, 1); var (ri12, rj12) = RotationLookup.Get(3, rotDeg, 1, 2); - dest[ri02 * destStride + rj02] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri21 * destStride + rj21] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri12 * destStride + rj12] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri02 * destStride + rj02] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri21 * destStride + rj21] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri12 * destStride + rj12] = encoder.Encode(Blend(lerp, we, px, 1, 3)); dest[ri2j2i * destStride + ri2j2j] = encoder.Encode(px); } else { // BlendLineDiagonal for 3x var (ri12, rj12) = RotationLookup.Get(3, rotDeg, 1, 2); var (ri21, rj21) = RotationLookup.Get(3, rotDeg, 2, 1); - dest[ri12 * destStride + rj12] = encoder.Encode(lerp.Lerp(we, px, 7, 1)); - dest[ri21 * destStride + rj21] = encoder.Encode(lerp.Lerp(we, px, 7, 1)); - dest[ri2j2i * destStride + ri2j2j] = encoder.Encode(lerp.Lerp(we, px, 1, 7)); + dest[ri12 * destStride + rj12] = encoder.Encode(Blend(lerp, we, px, 7, 1)); + dest[ri21 * destStride + rj21] = encoder.Encode(Blend(lerp, we, px, 7, 1)); + dest[ri2j2i * destStride + ri2j2j] = encoder.Encode(Blend(lerp, we, px, 1, 7)); } } } @@ -676,7 +944,7 @@ private static unsafe void ScalePixel3x( #region xBRZ 4x Kernel -file readonly struct Xbrz4xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) +file readonly struct Xbrz4xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) : IScaler where TWork : unmanaged, IColorSpace where TKey : unmanaged, IColorSpace @@ -684,7 +952,8 @@ file readonly struct Xbrz4xKernel where TMetric : struct, IColorMetric where TLerp : struct, ILerp - where TEncode : struct, IEncode { + where TEncode : struct, IEncode + where TVariant : struct, IXbrzVariant { public int ScaleX => 4; public int ScaleY => 4; @@ -718,6 +987,24 @@ public unsafe void Scale(in NeighborWindow window, TPixel* dest, in ScalePixel4x(ker, work, blendXy, rot, dest, destStride, encoder, lerp, metric, equality); } + /// + /// Applies a weighted blend in the rounding mode this variant calls for. + /// + /// + /// is a struct type parameter, so the branch is resolved when the + /// kernel is specialised and costs the pre-1.9 path nothing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b, int w1, int w2) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b, w1, w2) : lerp.Lerp(a, b, w1, w2); + + /// + /// Applies a 50/50 blend in the rounding mode this variant calls for. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b) : lerp.Lerp(a, b); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ScalePixel4x( TKey[] ker, TWork[] work, @@ -762,17 +1049,18 @@ private static unsafe void ScalePixel4x( if (!doLineBlend) { // BlendCorner for 4x - dest[ri33i * destStride + ri33j] = encoder.Encode(lerp.Lerp(we, px, 8, 17)); - dest[ri32i * destStride + ri32j] = encoder.Encode(lerp.Lerp(we, px, 91, 9)); - dest[ri23i * destStride + ri23j] = encoder.Encode(lerp.Lerp(we, px, 91, 9)); + dest[ri33i * destStride + ri33j] = encoder.Encode(Blend(lerp, we, px, 8, 17)); + dest[ri32i * destStride + ri32j] = encoder.Encode(Blend(lerp, we, px, 91, 9)); + dest[ri23i * destStride + ri23j] = encoder.Encode(Blend(lerp, we, px, 91, 9)); return; } var fg = metric.Distance(f, g); var hc = metric.Distance(h, c); + var steepNumerator = default(TVariant).SteepNumerator; - var haveShallowLine = (ulong)fg.RawValue * 22 <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold (2.2) as integer ratio - var haveSteepLine = (ulong)hc.RawValue * 22 <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold (2.2) as integer ratio + var haveShallowLine = (ulong)fg.RawValue * steepNumerator <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on + var haveSteepLine = (ulong)hc.RawValue * steepNumerator <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on if (haveShallowLine) { if (haveSteepLine) { @@ -783,11 +1071,11 @@ private static unsafe void ScalePixel4x( var (ri03i, ri03j) = RotationLookup.Get(4, rotDeg, 0, 3); var (ri22i, ri22j) = RotationLookup.Get(4, rotDeg, 2, 2); - dest[ri31i * destStride + ri31j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri13i * destStride + ri13j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri30i * destStride + ri30j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri03i * destStride + ri03j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri22i * destStride + ri22j] = encoder.Encode(lerp.Lerp(we, px, 2, 1)); + dest[ri31i * destStride + ri31j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri13i * destStride + ri13j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri30i * destStride + ri30j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri03i * destStride + ri03j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri22i * destStride + ri22j] = encoder.Encode(Blend(lerp, we, px, 2, 1)); dest[ri33i * destStride + ri33j] = encoder.Encode(px); dest[ri32i * destStride + ri32j] = encoder.Encode(px); dest[ri23i * destStride + ri23j] = encoder.Encode(px); @@ -798,10 +1086,10 @@ private static unsafe void ScalePixel4x( var (ri31i, ri31j) = RotationLookup.Get(4, rotDeg, 3, 1); var (ri23i2, ri23j2) = RotationLookup.Get(4, rotDeg, 2, 3); - dest[ri30i * destStride + ri30j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri22i * destStride + ri22j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri31i * destStride + ri31j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri23i2 * destStride + ri23j2] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri30i * destStride + ri30j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri22i * destStride + ri22j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri31i * destStride + ri31j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri23i2 * destStride + ri23j2] = encoder.Encode(Blend(lerp, we, px, 1, 3)); dest[ri32i * destStride + ri32j] = encoder.Encode(px); dest[ri33i * destStride + ri33j] = encoder.Encode(px); } @@ -813,10 +1101,10 @@ private static unsafe void ScalePixel4x( var (ri13i, ri13j) = RotationLookup.Get(4, rotDeg, 1, 3); var (ri32i2, ri32j2) = RotationLookup.Get(4, rotDeg, 3, 2); - dest[ri03i * destStride + ri03j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri22i * destStride + ri22j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri13i * destStride + ri13j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri32i2 * destStride + ri32j2] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri03i * destStride + ri03j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri22i * destStride + ri22j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri13i * destStride + ri13j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri32i2 * destStride + ri32j2] = encoder.Encode(Blend(lerp, we, px, 1, 3)); dest[ri23i * destStride + ri23j] = encoder.Encode(px); dest[ri33i * destStride + ri33j] = encoder.Encode(px); } else { @@ -824,8 +1112,8 @@ private static unsafe void ScalePixel4x( var (ri32d, rj32d) = RotationLookup.Get(4, rotDeg, 3, 2); var (ri23d, rj23d) = RotationLookup.Get(4, rotDeg, 2, 3); - dest[ri32d * destStride + rj32d] = encoder.Encode(lerp.Lerp(we, px)); - dest[ri23d * destStride + rj23d] = encoder.Encode(lerp.Lerp(we, px)); + dest[ri32d * destStride + rj32d] = encoder.Encode(Blend(lerp, we, px)); + dest[ri23d * destStride + rj23d] = encoder.Encode(Blend(lerp, we, px)); dest[ri33i * destStride + ri33j] = encoder.Encode(px); } } @@ -836,7 +1124,7 @@ private static unsafe void ScalePixel4x( #region xBRZ 5x Kernel -file readonly struct Xbrz5xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) +file readonly struct Xbrz5xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) : IScaler where TWork : unmanaged, IColorSpace where TKey : unmanaged, IColorSpace @@ -844,7 +1132,8 @@ file readonly struct Xbrz5xKernel where TMetric : struct, IColorMetric where TLerp : struct, ILerp - where TEncode : struct, IEncode { + where TEncode : struct, IEncode + where TVariant : struct, IXbrzVariant { public int ScaleX => 5; public int ScaleY => 5; @@ -878,6 +1167,24 @@ public unsafe void Scale(in NeighborWindow window, TPixel* dest, in ScalePixel5x(ker, work, blendXy, rot, dest, destStride, encoder, lerp, metric, equality); } + /// + /// Applies a weighted blend in the rounding mode this variant calls for. + /// + /// + /// is a struct type parameter, so the branch is resolved when the + /// kernel is specialised and costs the pre-1.9 path nothing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b, int w1, int w2) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b, w1, w2) : lerp.Lerp(a, b, w1, w2); + + /// + /// Applies a 50/50 blend in the rounding mode this variant calls for. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b) : lerp.Lerp(a, b); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ScalePixel5x( TKey[] ker, TWork[] work, @@ -922,17 +1229,18 @@ private static unsafe void ScalePixel5x( if (!doLineBlend) { // BlendCorner for 5x - dest[ri44i * destStride + ri44j] = encoder.Encode(lerp.Lerp(we, px, 7, 43)); - dest[ri43i * destStride + ri43j] = encoder.Encode(lerp.Lerp(we, px, 77, 23)); - dest[ri34i * destStride + ri34j] = encoder.Encode(lerp.Lerp(we, px, 77, 23)); + dest[ri44i * destStride + ri44j] = encoder.Encode(Blend(lerp, we, px, 7, 43)); + dest[ri43i * destStride + ri43j] = encoder.Encode(Blend(lerp, we, px, 77, 23)); + dest[ri34i * destStride + ri34j] = encoder.Encode(Blend(lerp, we, px, 77, 23)); return; } var fg = metric.Distance(f, g); var hc = metric.Distance(h, c); + var steepNumerator = default(TVariant).SteepNumerator; - var haveShallowLine = (ulong)fg.RawValue * 22 <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold (2.2) as integer ratio - var haveSteepLine = (ulong)hc.RawValue * 22 <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold (2.2) as integer ratio + var haveShallowLine = (ulong)fg.RawValue * steepNumerator <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on + var haveSteepLine = (ulong)hc.RawValue * steepNumerator <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on if (haveShallowLine) { if (haveSteepLine) { @@ -946,17 +1254,17 @@ private static unsafe void ScalePixel5x( var (ri24s, rj24s) = RotationLookup.Get(5, rotDeg, 2, 4); var (ri33i, ri33j) = RotationLookup.Get(5, rotDeg, 3, 3); - dest[ri04i * destStride + ri04j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri24i * destStride + ri24j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri14i * destStride + ri14j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri40i * destStride + ri40j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri42i * destStride + ri42j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri41i * destStride + ri41j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri04i * destStride + ri04j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri24i * destStride + ri24j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri14i * destStride + ri14j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri40i * destStride + ri40j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri42i * destStride + ri42j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri41i * destStride + ri41j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); dest[ri24s * destStride + rj24s] = encoder.Encode(px); dest[ri34i * destStride + ri34j] = encoder.Encode(px); dest[ri43i * destStride + ri43j] = encoder.Encode(px); dest[ri44i * destStride + ri44j] = encoder.Encode(px); - dest[ri33i * destStride + ri33j] = encoder.Encode(lerp.Lerp(we, px, 1, 2)); + dest[ri33i * destStride + ri33j] = encoder.Encode(Blend(lerp, we, px, 1, 2)); } else { // BlendLineShallow for 5x var (ri40i, ri40j) = RotationLookup.Get(5, rotDeg, 4, 0); @@ -966,11 +1274,11 @@ private static unsafe void ScalePixel5x( var (ri33i, ri33j) = RotationLookup.Get(5, rotDeg, 3, 3); var (ri42i, ri42j) = RotationLookup.Get(5, rotDeg, 4, 2); - dest[ri40i * destStride + ri40j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri32i * destStride + ri32j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri24i * destStride + ri24j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri41i * destStride + ri41j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri33i * destStride + ri33j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri40i * destStride + ri40j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri32i * destStride + ri32j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri24i * destStride + ri24j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri41i * destStride + ri41j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri33i * destStride + ri33j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); dest[ri42i * destStride + ri42j] = encoder.Encode(px); dest[ri43i * destStride + ri43j] = encoder.Encode(px); dest[ri44i * destStride + ri44j] = encoder.Encode(px); @@ -986,11 +1294,11 @@ private static unsafe void ScalePixel5x( var (ri33i, ri33j) = RotationLookup.Get(5, rotDeg, 3, 3); var (ri24i, ri24j) = RotationLookup.Get(5, rotDeg, 2, 4); - dest[ri04i * destStride + ri04j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri23i * destStride + ri23j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri42i * destStride + ri42j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri14i * destStride + ri14j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri33i * destStride + ri33j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri04i * destStride + ri04j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri23i * destStride + ri23j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri42i * destStride + ri42j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri14i * destStride + ri14j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri33i * destStride + ri33j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); dest[ri24i * destStride + ri24j] = encoder.Encode(px); dest[ri34i * destStride + ri34j] = encoder.Encode(px); dest[ri44i * destStride + ri44j] = encoder.Encode(px); @@ -1001,11 +1309,11 @@ private static unsafe void ScalePixel5x( var (ri33i, ri33j) = RotationLookup.Get(5, rotDeg, 3, 3); var (ri24i, ri24j) = RotationLookup.Get(5, rotDeg, 2, 4); - dest[ri42i * destStride + ri42j] = encoder.Encode(lerp.Lerp(we, px, 7, 1)); - dest[ri33i * destStride + ri33j] = encoder.Encode(lerp.Lerp(we, px, 7, 1)); - dest[ri24i * destStride + ri24j] = encoder.Encode(lerp.Lerp(we, px, 7, 1)); - dest[ri43i * destStride + ri43j] = encoder.Encode(lerp.Lerp(we, px, 1, 7)); - dest[ri34i * destStride + ri34j] = encoder.Encode(lerp.Lerp(we, px, 1, 7)); + dest[ri42i * destStride + ri42j] = encoder.Encode(Blend(lerp, we, px, 7, 1)); + dest[ri33i * destStride + ri33j] = encoder.Encode(Blend(lerp, we, px, 7, 1)); + dest[ri24i * destStride + ri24j] = encoder.Encode(Blend(lerp, we, px, 7, 1)); + dest[ri43i * destStride + ri43j] = encoder.Encode(Blend(lerp, we, px, 1, 7)); + dest[ri34i * destStride + ri34j] = encoder.Encode(Blend(lerp, we, px, 1, 7)); dest[ri44i * destStride + ri44j] = encoder.Encode(px); } } @@ -1016,7 +1324,7 @@ private static unsafe void ScalePixel5x( #region xBRZ 6x Kernel -file readonly struct Xbrz6xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) +file readonly struct Xbrz6xKernel(TEquality equality = default, TMetric metric = default, TLerp lerp = default) : IScaler where TWork : unmanaged, IColorSpace where TKey : unmanaged, IColorSpace @@ -1024,7 +1332,8 @@ file readonly struct Xbrz6xKernel where TMetric : struct, IColorMetric where TLerp : struct, ILerp - where TEncode : struct, IEncode { + where TEncode : struct, IEncode + where TVariant : struct, IXbrzVariant { public int ScaleX => 6; public int ScaleY => 6; @@ -1058,6 +1367,24 @@ public unsafe void Scale(in NeighborWindow window, TPixel* dest, in ScalePixel6x(ker, work, blendXy, rot, dest, destStride, encoder, lerp, metric, equality); } + /// + /// Applies a weighted blend in the rounding mode this variant calls for. + /// + /// + /// is a struct type parameter, so the branch is resolved when the + /// kernel is specialised and costs the pre-1.9 path nothing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b, int w1, int w2) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b, w1, w2) : lerp.Lerp(a, b, w1, w2); + + /// + /// Applies a 50/50 blend in the rounding mode this variant calls for. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TWork Blend(TLerp lerp, in TWork a, in TWork b) + => default(TVariant).RoundsBlends ? lerp.LerpRounded(a, b) : lerp.Lerp(a, b); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ScalePixel6x( TKey[] ker, TWork[] work, @@ -1103,21 +1430,22 @@ private static unsafe void ScalePixel6x( if (!doLineBlend) { // BlendCorner for 6x - exact weights from reference: 97/100, 42/100, 6/100 - dest[ri55i * destStride + ri55j] = encoder.Encode(lerp.Lerp(we, px, 3, 97)); - dest[ri54i * destStride + ri54j] = encoder.Encode(lerp.Lerp(we, px, 29, 21)); - dest[ri45i * destStride + ri45j] = encoder.Encode(lerp.Lerp(we, px, 29, 21)); + dest[ri55i * destStride + ri55j] = encoder.Encode(Blend(lerp, we, px, 3, 97)); + dest[ri54i * destStride + ri54j] = encoder.Encode(Blend(lerp, we, px, 29, 21)); + dest[ri45i * destStride + ri45j] = encoder.Encode(Blend(lerp, we, px, 29, 21)); var (ri53i, ri53j) = RotationLookup.Get(6, rotDeg, 5, 3); var (ri35i, ri35j) = RotationLookup.Get(6, rotDeg, 3, 5); - dest[ri53i * destStride + ri53j] = encoder.Encode(lerp.Lerp(we, px, 47, 3)); - dest[ri35i * destStride + ri35j] = encoder.Encode(lerp.Lerp(we, px, 47, 3)); + dest[ri53i * destStride + ri53j] = encoder.Encode(Blend(lerp, we, px, 47, 3)); + dest[ri35i * destStride + ri35j] = encoder.Encode(Blend(lerp, we, px, 47, 3)); return; } var fg = metric.Distance(f, g); var hc = metric.Distance(h, c); + var steepNumerator = default(TVariant).SteepNumerator; - var haveShallowLine = (ulong)fg.RawValue * 22 <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold (2.2) as integer ratio - var haveSteepLine = (ulong)hc.RawValue * 22 <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold (2.2) as integer ratio + var haveShallowLine = (ulong)fg.RawValue * steepNumerator <= (ulong)hc.RawValue * 10 && !equality.Equals(e, g) && !equality.Equals(d, g); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on + var haveSteepLine = (ulong)hc.RawValue * steepNumerator <= (ulong)fg.RawValue * 10 && !equality.Equals(e, c) && !equality.Equals(b, c); // SteepDirectionThreshold as an integer ratio: 2.2 before xBRZ 1.9, 2.4 from 1.9 on if (haveShallowLine) { if (haveSteepLine) { @@ -1137,14 +1465,14 @@ private static unsafe void ScalePixel6x( var (ri53i, ri53j) = RotationLookup.Get(6, rotDeg, 5, 3); // Gradient blends - dest[ri05i * destStride + ri05j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri24i * destStride + ri24j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri15i * destStride + ri15j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri34i * destStride + ri34j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri50i * destStride + ri50j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri42i * destStride + ri42j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri51i * destStride + ri51j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri43i * destStride + ri43j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri05i * destStride + ri05j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri24i * destStride + ri24j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri15i * destStride + ri15j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri34i * destStride + ri34j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri50i * destStride + ri50j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri42i * destStride + ri42j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri51i * destStride + ri51j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri43i * destStride + ri43j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); // Solid fills dest[ri25i * destStride + ri25j] = encoder.Encode(px); dest[ri35i * destStride + ri35j] = encoder.Encode(px); @@ -1167,12 +1495,12 @@ private static unsafe void ScalePixel6x( var (ri44i, ri44j) = RotationLookup.Get(6, rotDeg, 4, 4); // Gradient blends - dest[ri50i * destStride + ri50j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri42i * destStride + ri42j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri34i * destStride + ri34j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri51i * destStride + ri51j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri43i * destStride + ri43j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri35i * destStride + ri35j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri50i * destStride + ri50j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri42i * destStride + ri42j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri34i * destStride + ri34j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri51i * destStride + ri51j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri43i * destStride + ri43j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri35i * destStride + ri35j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); // Solid fills dest[ri52i * destStride + ri52j] = encoder.Encode(px); dest[ri53i * destStride + ri53j] = encoder.Encode(px); @@ -1195,12 +1523,12 @@ private static unsafe void ScalePixel6x( var (ri44i, ri44j) = RotationLookup.Get(6, rotDeg, 4, 4); // Gradient blends - dest[ri05i * destStride + ri05j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri24i * destStride + ri24j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri43i * destStride + ri43j] = encoder.Encode(lerp.Lerp(we, px, 3, 1)); - dest[ri15i * destStride + ri15j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri34i * destStride + ri34j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); - dest[ri53i * destStride + ri53j] = encoder.Encode(lerp.Lerp(we, px, 1, 3)); + dest[ri05i * destStride + ri05j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri24i * destStride + ri24j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri43i * destStride + ri43j] = encoder.Encode(Blend(lerp, we, px, 3, 1)); + dest[ri15i * destStride + ri15j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri34i * destStride + ri34j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); + dest[ri53i * destStride + ri53j] = encoder.Encode(Blend(lerp, we, px, 1, 3)); // Solid fills dest[ri25i * destStride + ri25j] = encoder.Encode(px); dest[ri35i * destStride + ri35j] = encoder.Encode(px); @@ -1215,9 +1543,9 @@ private static unsafe void ScalePixel6x( var (ri35i, ri35j) = RotationLookup.Get(6, rotDeg, 3, 5); // Gradient blends (1/2 weight) - dest[ri53i * destStride + ri53j] = encoder.Encode(lerp.Lerp(we, px)); - dest[ri44i * destStride + ri44j] = encoder.Encode(lerp.Lerp(we, px)); - dest[ri35i * destStride + ri35j] = encoder.Encode(lerp.Lerp(we, px)); + dest[ri53i * destStride + ri53j] = encoder.Encode(Blend(lerp, we, px)); + dest[ri44i * destStride + ri44j] = encoder.Encode(Blend(lerp, we, px)); + dest[ri35i * destStride + ri35j] = encoder.Encode(Blend(lerp, we, px)); // Solid fills dest[ri45i * destStride + ri45j] = encoder.Encode(px); dest[ri55i * destStride + ri55j] = encoder.Encode(px); diff --git a/System.Drawing.Extensions/REFERENCE.md b/System.Drawing.Extensions/REFERENCE.md index 842962f3..85de4e59 100644 --- a/System.Drawing.Extensions/REFERENCE.md +++ b/System.Drawing.Extensions/REFERENCE.md @@ -934,6 +934,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -945,6 +947,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -956,6 +960,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -967,6 +973,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -978,6 +986,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -989,6 +999,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -1000,6 +1012,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -1011,6 +1025,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -1022,6 +1038,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -1081,6 +1099,8 @@ Provides linear interpolation between two colors. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `T LerpRounded(in T a, in T b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `T LerpRounded(in T a, in T b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `T Lerp(in T a, in T b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `T Lerp(in T a, in T b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -1103,6 +1123,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -1114,6 +1136,8 @@ Implements `ILerp`. | Member | Signature | Summary | | --- | --- | --- | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest. | +| `LerpRounded` | `TWork LerpRounded(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights, rounding to nearest. | | `Lerp` | `TWork Lerp(in TWork a, in TWork b)` | Linearly interpolates between two colors at the midpoint (50/50 blend). | | `Lerp` | `TWork Lerp(in TWork a, in TWork b, int w1, int w2)` | Linearly interpolates between two colors with integer weights. | @@ -9451,7 +9475,7 @@ Implements `IResampler`, `IScalerInfo`. ### Namespace `Hawkynt.ColorProcessing.Resizing.Rescalers` -[`Aann`](#aann) · [`AnimeLineEnhancer`](#animelineenhancer) · [`Bilateral`](#bilateral) · [`BilateralVariant`](#bilateralvariant) · [`BilinearPlus`](#bilinearplus) · [`BilinearPlusOriginal`](#bilinearplusoriginal) · [`CatmullRom`](#catmullrom) · [`Clean`](#clean) · [`CrtCaligari`](#crtcaligari) · [`CrtEasymode`](#crteasymode) · [`CrtGeom`](#crtgeom) · [`CrtHyllian`](#crthyllian) · [`CrtLottes`](#crtlottes) · [`CrtRoyale`](#crtroyale) · [`Cut`](#cut) · [`Ddt`](#ddt) · [`DdtVariant`](#ddtvariant) · [`Des`](#des) · [`Des2`](#des2) · [`DotMatrix`](#dotmatrix) · [`Eagle`](#eagle) · [`Eagle3x`](#eagle3x) · [`Eagle3xB`](#eagle3xb) · [`Edge`](#edge) · [`Epx`](#epx) · [`Epx3`](#epx3) · [`EpxB`](#epxb) · [`EpxC`](#epxc) · [`Fxaa`](#fxaa) · [`GameBoyShader`](#gameboyshader) · [`Gtu`](#gtu) · [`HawkyntTv`](#hawkynttv) · [`Hq`](#hq) · [`HqLq`](#hqlq) · [`HqMode`](#hqmode) · [`HqQuality`](#hqquality) · [`LcdGhosting`](#lcdghosting) · [`LcdGrid`](#lcdgrid) · [`Lq`](#lq) · [`MameAdvInterp`](#mameadvinterp) · [`MameRgb`](#mamergb) · [`MameTv`](#mametv) · [`Mlaa`](#mlaa) · [`MlaaVariant`](#mlaavariant) · [`Mmpx2x`](#mmpx2x) · [`NearestNeighborPlus`](#nearestneighborplus) · [`Nedi`](#nedi) · [`Nis`](#nis) · [`Nnedi3`](#nnedi3) · [`Nnedi3Quality`](#nnedi3quality) · [`Ntsc`](#ntsc) · [`Omniscale`](#omniscale) · [`Pixellate`](#pixellate) · [`Quilez`](#quilez) · [`ReverseAa`](#reverseaa) · [`RotSprite`](#rotsprite) · [`Saa5050`](#saa5050) · [`Sabr`](#sabr) · [`SabrSharp`](#sabrsharp) · [`SabrSmooth`](#sabrsmooth) · [`SabrVariant`](#sabrvariant) · [`Sai2x`](#sai2x) · [`Sal`](#sal) · [`Scale`](#scale) · [`Scale.Mode`](#scalemode) · [`ScaleFx3x`](#scalefx3x) · [`ScaleHq`](#scalehq) · [`ScaleNxPlus`](#scalenxplus) · [`ScaleNxSfx`](#scalenxsfx) · [`ScanlineHorizontal`](#scanlinehorizontal) · [`ScanlineHorizontalFullLight`](#scanlinehorizontalfulllight) · [`ScanlineHorizontalHalfLight`](#scanlinehorizontalhalflight) · [`ScanlineVertical`](#scanlinevertical) · [`ScanlineVerticalFullLight`](#scanlineverticalfulllight) · [`ScanlineVerticalHalfLight`](#scanlineverticalhalflight) · [`Scl2x`](#scl2x) · [`Scl2xSuper`](#scl2xsuper) · [`Scl2xUltra`](#scl2xultra) · [`SharpBilinear`](#sharpbilinear) · [`Simple`](#simple) · [`Smaa`](#smaa) · [`SmaaQuality`](#smaaquality) · [`Soft`](#soft) · [`SoftSmart`](#softsmart) · [`Super2xSaI`](#super2xsai) · [`SuperEagle`](#supereagle) · [`SuperXbr`](#superxbr) · [`TriplePoint`](#triplepoint) · [`TwoXpm`](#twoxpm) · [`Xbr`](#xbr) · [`Xbr3xOriginal`](#xbr3xoriginal) · [`XbrNoBlend`](#xbrnoblend) · [`Xbrz`](#xbrz) · [`ZfastCrt`](#zfastcrt) +[`Aann`](#aann) · [`AnimeLineEnhancer`](#animelineenhancer) · [`Bilateral`](#bilateral) · [`BilateralVariant`](#bilateralvariant) · [`BilinearPlus`](#bilinearplus) · [`BilinearPlusOriginal`](#bilinearplusoriginal) · [`CatmullRom`](#catmullrom) · [`Clean`](#clean) · [`CrtCaligari`](#crtcaligari) · [`CrtEasymode`](#crteasymode) · [`CrtGeom`](#crtgeom) · [`CrtHyllian`](#crthyllian) · [`CrtLottes`](#crtlottes) · [`CrtRoyale`](#crtroyale) · [`Cut`](#cut) · [`Ddt`](#ddt) · [`DdtVariant`](#ddtvariant) · [`Des`](#des) · [`Des2`](#des2) · [`DotMatrix`](#dotmatrix) · [`Eagle`](#eagle) · [`Eagle3x`](#eagle3x) · [`Eagle3xB`](#eagle3xb) · [`Edge`](#edge) · [`Epx`](#epx) · [`Epx3`](#epx3) · [`EpxB`](#epxb) · [`EpxC`](#epxc) · [`Fxaa`](#fxaa) · [`GameBoyShader`](#gameboyshader) · [`Gtu`](#gtu) · [`HawkyntTv`](#hawkynttv) · [`Hq`](#hq) · [`HqLq`](#hqlq) · [`HqMode`](#hqmode) · [`HqQuality`](#hqquality) · [`LcdGhosting`](#lcdghosting) · [`LcdGrid`](#lcdgrid) · [`Lq`](#lq) · [`MameAdvInterp`](#mameadvinterp) · [`MameRgb`](#mamergb) · [`MameTv`](#mametv) · [`Mlaa`](#mlaa) · [`MlaaVariant`](#mlaavariant) · [`Mmpx2x`](#mmpx2x) · [`NearestNeighborPlus`](#nearestneighborplus) · [`Nedi`](#nedi) · [`Nis`](#nis) · [`Nnedi3`](#nnedi3) · [`Nnedi3Quality`](#nnedi3quality) · [`Ntsc`](#ntsc) · [`Omniscale`](#omniscale) · [`Pixellate`](#pixellate) · [`Quilez`](#quilez) · [`ReverseAa`](#reverseaa) · [`RotSprite`](#rotsprite) · [`Saa5050`](#saa5050) · [`Sabr`](#sabr) · [`SabrSharp`](#sabrsharp) · [`SabrSmooth`](#sabrsmooth) · [`SabrVariant`](#sabrvariant) · [`Sai2x`](#sai2x) · [`Sal`](#sal) · [`Scale`](#scale) · [`Scale.Mode`](#scalemode) · [`ScaleFx3x`](#scalefx3x) · [`ScaleHq`](#scalehq) · [`ScaleNxPlus`](#scalenxplus) · [`ScaleNxSfx`](#scalenxsfx) · [`ScanlineHorizontal`](#scanlinehorizontal) · [`ScanlineHorizontalFullLight`](#scanlinehorizontalfulllight) · [`ScanlineHorizontalHalfLight`](#scanlinehorizontalhalflight) · [`ScanlineVertical`](#scanlinevertical) · [`ScanlineVerticalFullLight`](#scanlineverticalfulllight) · [`ScanlineVerticalHalfLight`](#scanlineverticalhalflight) · [`Scl2x`](#scl2x) · [`Scl2xSuper`](#scl2xsuper) · [`Scl2xUltra`](#scl2xultra) · [`SharpBilinear`](#sharpbilinear) · [`Simple`](#simple) · [`Smaa`](#smaa) · [`SmaaQuality`](#smaaquality) · [`Soft`](#soft) · [`SoftSmart`](#softsmart) · [`Super2xSaI`](#super2xsai) · [`SuperEagle`](#supereagle) · [`SuperXbr`](#superxbr) · [`TriplePoint`](#triplepoint) · [`TwoXpm`](#twoxpm) · [`Xbr`](#xbr) · [`Xbr3xOriginal`](#xbr3xoriginal) · [`XbrNoBlend`](#xbrnoblend) · [`Xbrz`](#xbrz) · [`Xbrz19`](#xbrz19) · [`ZfastCrt`](#zfastcrt) #### `Aann` @@ -11043,6 +11067,27 @@ Implements `IRescaler`, `IScalerInfo`. | `InvokeKernel` | `TResult InvokeKernel(IKernelCallback callback, TEquality equality = null, TLerp lerp = null)` | Invokes a callback with the concrete kernel type, enabling struct-constrained dispatch. | | `SupportsScale` | `static bool SupportsScale(ScaleFactor scale)` | Determines whether xBRZ supports the specified scale factor. | +#### `Xbrz19` + +xBRZ as released in 1.9 (2x, 3x, 4x, 5x, 6x). + +Implements `IRescaler`, `IScalerInfo`. + +| Member | Signature | Summary | +| --- | --- | --- | +| `Xbrz19` | `Xbrz19(int scale = 2)` | Creates an xBRZ 1.9 scaler with the specified scale factor. | +| `Default` | `static Xbrz19 Default { get; }` | Gets the default xBRZ 1.9 configuration (2x). | +| `Scale2x` | `static Xbrz19 Scale2x { get; }` | Gets an xBRZ 1.9 2x scaler. | +| `Scale3x` | `static Xbrz19 Scale3x { get; }` | Gets an xBRZ 1.9 3x scaler. | +| `Scale4x` | `static Xbrz19 Scale4x { get; }` | Gets an xBRZ 1.9 4x scaler. | +| `Scale5x` | `static Xbrz19 Scale5x { get; }` | Gets an xBRZ 1.9 5x scaler. | +| `Scale6x` | `static Xbrz19 Scale6x { get; }` | Gets an xBRZ 1.9 6x scaler. | +| `Scale` | `ScaleFactor Scale { get; }` | Gets the scaling factor for this scaler instance. | +| `SupportedScales` | `static ScaleFactor[] SupportedScales { get; }` | Gets the list of scale factors supported by xBRZ 1.9. | +| `GetPossibleTargets` | `static IEnumerable> GetPossibleTargets(int sourceWidth, int sourceHeight)` | Enumerates all possible target dimensions for xBRZ 1.9. | +| `InvokeKernel` | `TResult InvokeKernel(IKernelCallback callback, TEquality equality = null, TLerp lerp = null)` | Invokes a callback with the concrete kernel type, enabling struct-constrained dispatch. | +| `SupportsScale` | `static bool SupportsScale(ScaleFactor scale)` | Determines whether xBRZ 1.9 supports the specified scale factor. | + #### `ZfastCrt` Stylistic CRT-style upscale inspired by the zfast-crt shader. NOT a port: this filter uses a fixed darkening pattern over each scaled block in place of the shader's per-fragment scanline math. diff --git a/System.Drawing.Extensions/Readme.md b/System.Drawing.Extensions/Readme.md index 027eb02e..2349c23d 100644 --- a/System.Drawing.Extensions/Readme.md +++ b/System.Drawing.Extensions/Readme.md @@ -1194,7 +1194,8 @@ using var seams = source.Resample(SeamCarving.Default, 800, 600); | [`Hqnx`](https://en.wikipedia.org/wiki/Hqx) | Maxim Stepin | 2003 | 2x, 3x, 4x | High-quality magnification using YUV comparisons | | [`Lqnx`](https://en.wikipedia.org/wiki/Hqx) | - | - | 2x, 3x, 4x | Low-quality simplified variant of HQnx. [Ref](https://github.com/luckytyphlosion/vba-link/blob/master/src/lq2x.h) | | [`Xbr`](https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms#xBR_family) | Hyllian | 2011 | 2x, 3x, 4x | xBR (scale By Rules) edge-detection scaler | -| [`Xbrz`](https://sourceforge.net/projects/xbrz/) | Zenju | 2012 | 2x-6x | Enhanced xBR with improved edge handling | +| [`Xbrz`](https://sourceforge.net/projects/xbrz/) | Zenju | 2012 | 2x-6x | Enhanced xBR with improved edge handling (as released up to 1.8) | +| [`Xbrz19`](https://sourceforge.net/projects/xbrz/) | Zenju | 2026 | 2x-6x | xBRZ 1.9 — steep direction threshold raised to 2.4, blends rounded rather than truncated | | [`Sal`](https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms#2xSaI) | Kreed | 2001 | 2x | Simple Assembly Language scaler with anti-aliasing | | [`Sabr`](https://github.com/libretro/common-shaders/tree/master/sabr) | Joshua Street | 2012 | 2x-4x | Scalable Bicubic Renderer with multi-angle (45/30/60) edge detection and smoothstep blending. Variants: `SabrSharp` (more edges), `SabrSmooth` (softer) | | [`Mmpx`](https://casual-effects.com/research/McGuire2021PixelArt/) | Morgan McGuire | 2021 | 2x | Modern AI-inspired pixel art scaling | @@ -2022,7 +2023,7 @@ Add the package, then use the members catalogued above — they are extension me -Every public and protected member of all 1021 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/C--FrameworkExtensions/blob/main/System.Drawing.Extensions/REFERENCE.md). +Every public and protected member of all 1022 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/C--FrameworkExtensions/blob/main/System.Drawing.Extensions/REFERENCE.md). diff --git a/Tests/System.Drawing.Extensions.Tests/ColorProcessing/XbrzVersionTests.cs b/Tests/System.Drawing.Extensions.Tests/ColorProcessing/XbrzVersionTests.cs new file mode 100644 index 00000000..aae4e564 --- /dev/null +++ b/Tests/System.Drawing.Extensions.Tests/ColorProcessing/XbrzVersionTests.cs @@ -0,0 +1,396 @@ +#region (c)2010-2042 Hawkynt + +// This file is part of Hawkynt's .NET Framework extensions. +// +// Hawkynt's .NET Framework extensions are free software: +// you can redistribute and/or modify it under the terms +// given in the LICENSE file. +// +// Hawkynt's .NET Framework extensions is distributed in the hope that +// it will be useful, but WITHOUT ANY WARRANTY without even the implied +// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the LICENSE file for more details. +// +// You should have received a copy of the License along with Hawkynt's +// .NET Framework extensions. If not, see +// . + +#endregion + +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Hawkynt.ColorProcessing.ColorMath; +using Hawkynt.ColorProcessing.Resizing; +using Hawkynt.ColorProcessing.Resizing.Rescalers; +using Hawkynt.ColorProcessing.Storage; +using Hawkynt.ColorProcessing.Working; +using Hawkynt.Drawing; +using NUnit.Framework; + +namespace System.Drawing.Tests.ColorProcessing; + +/// +/// Pins the two behavioural differences xBRZ 1.9 introduced, and pins the pre-1.9 entry against +/// acquiring them by accident. +/// +/// +/// +/// 1.9 changed exactly two things: the steep/shallow direction threshold rose from 2.2 to 2.4, and +/// the weighted and 50/50 blends round to nearest rather than truncating. Both are exact and +/// integer, so both are pinned exactly rather than by eye or by tolerance. +/// +/// +/// The two single-change variants ( and +/// ) exist so a failing test names which of the two +/// changes broke. Asserting only against the combined 1.9 variant would let either change go +/// missing without any test noticing, because the other would still produce a difference. +/// +/// +[TestFixture] +[Category("Unit")] +[Category("ColorProcessing")] +[Category("Scalers")] +public class XbrzVersionTests { + + #region helpers + + /// + /// Builds a deterministic source image. The speckle field gives the corner pre-pass a wide spread + /// of accumulated colour-distance ratios, so some corners land between the 2.2 and the 2.4 + /// threshold; the diagonals and the circle give the line-blend paths something to blend. + /// + private static Bitmap _CreateSource(int width, int height) { + var result = new Bitmap(width, height, PixelFormat.Format32bppArgb); + var state = 0x12345678u; + + uint Next() { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + return state; + } + + for (var y = 0; y < height; ++y) + for (var x = 0; x < width; ++x) { + int r, g, b; + if (Math.Abs(x - y) <= 1) { + r = 250; + g = 20; + b = 20; + } else if (Math.Abs(x * 2 - y) <= 1) { + r = 17; + g = 200; + b = 240; + } else if (Math.Abs(x + y - width) <= 1) { + r = 40; + g = 240; + b = 60; + } else { + var dx = x - width / 2.0; + var dy = y - height / 2.0; + if (Math.Abs(Math.Sqrt(dx * dx + dy * dy) - width / 3.0) < 1.0) { + r = 255; + g = 255; + b = 30; + } else { + var n = Next(); + r = (int)(n & 0xFF); + g = (int)((n >> 8) & 0xFF); + b = (int)((n >> 16) & 0xFF); + } + } + + result.SetPixel(x, y, Color.FromArgb(255, r, g, b)); + } + + return result; + } + + private static byte[] _RawPixels(Bitmap bitmap) { + var data = bitmap.LockBits(new(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + try { + var stride = Math.Abs(data.Stride); + var result = new byte[stride * data.Height]; + var origin = data.Scan0.ToInt64(); + for (var y = 0; y < data.Height; ++y) + Marshal.Copy(new(origin + y * (long)data.Stride), result, y * stride, stride); + + return result; + } finally { + bitmap.UnlockBits(data); + } + } + + private static string _HashOf(Bitmap bitmap) { + using var sha = SHA256.Create(); + var builder = new StringBuilder(); + foreach (var value in sha.ComputeHash(_RawPixels(bitmap))) + builder.Append(value.ToString("x2")); + + return builder.ToString(); + } + + private static Bitmap _Scale(Bitmap source, int scale, XbrzVariant variant) + => source.Upscale(new Xbrz(scale, variant), ScalerQuality.Fast); + + private static Bgra8888 _Color(byte c1, byte c2, byte c3, byte a) => new(c1, c2, c3, a); + + #endregion + + #region the pre-1.9 entry must not move + + /// + /// The hashes below were taken from the xBRZ entry's output before 1.9 support was + /// added. They are the whole point of the exercise: a version selector that quietly changes what + /// the existing entry produces is a regression for every script and saved setting naming it. + /// + [Test] + [Category("HappyPath")] + [TestCase(2, "bd63fc67557bdc45e6e9fbcf2336beec6b9bb931d496ea3239bc70a7e34d441c")] + [TestCase(3, "0e5c5cfeca7b4f75a293d0052af83a514faff8aa2776dada95ffe5740162c1eb")] + [TestCase(4, "e5585ded852f40c0b513072c60a6d597d3b0130d2eabb2a28e7543c5b09aab74")] + [TestCase(5, "7269fb01f984812c2df58ce1f9ae7215ce32540bee09e1fcf4ab173c03efc25d")] + [TestCase(6, "d7372f9420f3e8bdeeedfe8cfd993fad652014ed72259f6adbff3d3d78b15a44")] + public void Xbrz_AtEveryScale_StillProducesTheOutputItProducedBefore19Existed(int scale, string expected) { + using var source = _CreateSource(24, 24); + using var actual = source.Upscale(new Xbrz(scale), ScalerQuality.Fast); + + Assert.That(actual.Width, Is.EqualTo(24 * scale)); + Assert.That(_HashOf(actual), Is.EqualTo(expected), $"the xBRZ {scale}x entry changed its output"); + } + + [Test] + [Category("HappyPath")] + public void Xbrz_DefaultConstructed_SelectsThePre19Behaviour() { + using var source = _CreateSource(24, 24); + using var viaDefault = source.Upscale(new Xbrz(2), ScalerQuality.Fast); + using var viaClassic = _Scale(source, 2, XbrzVariant.Classic); + + Assert.That(_RawPixels(viaDefault), Is.EqualTo(_RawPixels(viaClassic))); + } + + #endregion + + #region the steep direction threshold: 2.2 before 1.9, 2.4 from 1.9 + + /// + /// Raising the threshold makes the steep/shallow test harder to satisfy, so a source whose + /// accumulated distance ratios land between the two thresholds must classify differently. With + /// blending held at the pre-1.9 truncating mode, any difference here is the threshold's alone. + /// + [Test] + [Category("HappyPath")] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void SteepThreshold_RaisedTo24_ChangesHowSomeNearDiagonalsAreClassified(int scale) { + using var source = _CreateSource(48, 48); + using var at22 = _Scale(source, scale, XbrzVariant.Classic); + using var at24 = _Scale(source, scale, XbrzVariant.SteepThresholdOnly); + + Assert.That(_RawPixels(at24), Is.Not.EqualTo(_RawPixels(at22)), + $"the {scale}x kernel classified every corner identically at 2.2 and at 2.4, so the threshold is not being read"); + } + + #endregion + + #region blends: truncated before 1.9, rounded from 1.9 + + /// + /// A weighted sum with a non-zero remainder against the total weight is exactly where truncation + /// and rounding must part company. 0*3 + 3*1 = 3 over a total of 4: truncation floors to 0, + /// rounding takes it to 1. + /// + [Test] + [Category("HappyPath")] + public void WeightedLerp_RemainderAgainstTotal_TruncatesButRoundedDoesNot() { + var lerp = default(Color4BLerpInt); + var a = _Color(0, 0, 0, 0); + var b = _Color(3, 3, 3, 3); + + var truncated = lerp.Lerp(a, b, 3, 1); + var rounded = lerp.LerpRounded(a, b, 3, 1); + + Assert.That(truncated.C1, Is.EqualTo(0)); + Assert.That(rounded.C1, Is.EqualTo(1)); + Assert.That(rounded.A, Is.EqualTo(1), "the alpha channel rounds like every other channel"); + } + + /// The 21/100 corner blend truncates the same way; 3 * 21 = 63 over 100. + [Test] + [Category("HappyPath")] + public void WeightedLerp_AtTheCornerBlendWeights_TruncatesButRoundedDoesNot() { + var lerp = default(Color4BLerpInt); + var a = _Color(0, 0, 0, 0); + var b = _Color(3, 3, 3, 3); + + Assert.That(lerp.Lerp(a, b, 79, 21).C1, Is.EqualTo(0)); + Assert.That(lerp.LerpRounded(a, b, 79, 21).C1, Is.EqualTo(1)); + } + + /// The 50/50 blend shifts rather than divides, and truncates just the same. + [Test] + [Category("HappyPath")] + public void MidpointLerp_OddSum_TruncatesButRoundedDoesNot() { + var lerp = default(Color4BLerpInt); + var a = _Color(0, 4, 10, 0); + var b = _Color(1, 5, 11, 1); + + var truncated = lerp.Lerp(a, b); + var rounded = lerp.LerpRounded(a, b); + + Assert.That(truncated.C1, Is.EqualTo(0)); + Assert.That(rounded.C1, Is.EqualTo(1)); + Assert.That(truncated.C2, Is.EqualTo(4)); + Assert.That(rounded.C2, Is.EqualTo(5)); + } + + [Test] + [Category("EdgeCase")] + public void Lerp_WhenTheDivisionIsExact_BothModesAgree() { + var lerp = default(Color4BLerpInt); + var a = _Color(0, 0, 0, 0); + var b = _Color(4, 8, 40, 4); + + Assert.That(lerp.LerpRounded(a, b, 3, 1).C1, Is.EqualTo(lerp.Lerp(a, b, 3, 1).C1)); + Assert.That(lerp.LerpRounded(a, b, 1, 1).C2, Is.EqualTo(lerp.Lerp(a, b, 1, 1).C2)); + } + + [Test] + [Category("EdgeCase")] + public void RoundedLerp_AtTheTopOfTheChannelRange_DoesNotOverflow() { + var lerp = default(Color4BLerpInt); + var a = _Color(255, 255, 255, 255); + var b = _Color(255, 255, 255, 255); + + Assert.That(lerp.LerpRounded(a, b).C1, Is.EqualTo(255)); + Assert.That(lerp.LerpRounded(a, b, 3, 1).C1, Is.EqualTo(255)); + Assert.That(lerp.LerpRounded(a, b, 79, 21).A, Is.EqualTo(255)); + } + + [Test] + [Category("EdgeCase")] + public void RoundedLerp_ForAThreeComponentSpace_RoundsEveryChannel() { + var lerp = default(Color3BLerpInt); + var a = new Bgr888(0, 0, 0); + var b = new Bgr888(3, 3, 3); + + Assert.That(lerp.Lerp(a, b, 3, 1).R, Is.EqualTo(0)); + Assert.That(lerp.LerpRounded(a, b, 3, 1).R, Is.EqualTo(1)); + Assert.That(lerp.LerpRounded(a, b, 3, 1).B, Is.EqualTo(1)); + } + + /// + /// Floating-point blends never truncated to a representable step, so their rounded members are + /// deliberately the same operation. Pinning that keeps a later "fix" from making them differ. + /// + [Test] + [Category("EdgeCase")] + public void RoundedLerp_ForAFloatSpace_IsTheSameOperationAsTheTruncatingOne() { + var lerp = default(Color4FLerp); + var a = new LinearRgbaF(0f, 0f, 0f, 0f); + var b = new LinearRgbaF(0.3f, 0.3f, 0.3f, 0.3f); + + Assert.That(lerp.LerpRounded(a, b, 3, 1).C1, Is.EqualTo(lerp.Lerp(a, b, 3, 1).C1)); + Assert.That(lerp.LerpRounded(a, b).C1, Is.EqualTo(lerp.Lerp(a, b).C1)); + } + + /// + /// With the threshold held at 2.2, any difference the kernels produce is the rounding's alone. + /// + [Test] + [Category("HappyPath")] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void RoundedBlends_InTheKernels_ChangeBlendedPixels(int scale) { + using var source = _CreateSource(48, 48); + using var truncating = _Scale(source, scale, XbrzVariant.Classic); + using var rounding = _Scale(source, scale, XbrzVariant.RoundedBlendsOnly); + + Assert.That(_RawPixels(rounding), Is.Not.EqualTo(_RawPixels(truncating)), + $"the {scale}x kernel produced identical pixels truncating and rounding, so it is not using the rounded blend"); + } + + #endregion + + #region the 1.9 entry + + [Test] + [Category("HappyPath")] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void Xbrz19_AtEveryScale_DiffersFromThePre19Entry(int scale) { + using var source = _CreateSource(48, 48); + using var classic = source.Upscale(new Xbrz(scale), ScalerQuality.Fast); + using var v19 = source.Upscale(new Xbrz19(scale), ScalerQuality.Fast); + + Assert.That(_RawPixels(v19), Is.Not.EqualTo(_RawPixels(classic))); + } + + [Test] + [Category("HappyPath")] + public void Xbrz19_CarriesBothOf19sChanges() { + using var source = _CreateSource(48, 48); + using var v19 = source.Upscale(new Xbrz19(2), ScalerQuality.Fast); + using var steepOnly = _Scale(source, 2, XbrzVariant.SteepThresholdOnly); + using var roundOnly = _Scale(source, 2, XbrzVariant.RoundedBlendsOnly); + + Assert.That(_RawPixels(v19), Is.Not.EqualTo(_RawPixels(steepOnly)), "1.9 must round its blends, not only raise the threshold"); + Assert.That(_RawPixels(v19), Is.Not.EqualTo(_RawPixels(roundOnly)), "1.9 must raise the threshold, not only round its blends"); + } + + [Test] + [Category("HappyPath")] + [TestCase(2)] + [TestCase(6)] + public void Xbrz19_OutputDimensions_MatchTheScaleFactor(int scale) { + using var source = TestUtilities.CreateTestPattern(10, 10); + using var result = source.Upscale(new Xbrz19(scale), ScalerQuality.Fast); + + Assert.That(result.Width, Is.EqualTo(10 * scale)); + Assert.That(result.Height, Is.EqualTo(10 * scale)); + } + + [Test] + [Category("EdgeCase")] + [TestCase(1)] + [TestCase(7)] + public void Xbrz19_WithAnUnsupportedScale_Throws(int scale) + => Assert.That(() => new Xbrz19(scale), Throws.InstanceOf()); + + [Test] + [Category("HappyPath")] + public void Xbrz19_SupportedScales_AreTwoThroughSix() { + Assert.That(Xbrz19.SupportedScales.Length, Is.EqualTo(5)); + Assert.That(Xbrz19.SupportsScale(new(6, 6)), Is.True); + Assert.That(Xbrz19.SupportsScale(new(7, 7)), Is.False); + } + + /// + /// The registry is what the GUI and the command line enumerate, so the pre-1.9 entry keeping its + /// exact name is what keeps existing scripts and saved settings working. + /// + [Test] + [Category("HappyPath")] + public void ScalerRegistry_ListsBothEntries_WithThePre19NameUnchanged() { + var names = ScalerRegistry.Rescalers.Select(s => s.Name).ToArray(); + + Assert.That(names, Does.Contain("xBRZ")); + Assert.That(names, Does.Contain("xBRZ 1.9")); + } + + #endregion + +}