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