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); } 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 + +}