From 5db8d32a942868095cf6815da4b54a6f6dbf0b6b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 21 Aug 2026 15:58:25 +1000 Subject: [PATCH 1/2] Retarget builds to .NET 10 and 11 Move the main target framework from net8.0 to net10.0 and the preview lane from net10.0 to net11.0, adding a global.json floor for SDK 10+ and updating CI coverage and test projects to match. The change also adjusts hardware-intrinsic feature tests for .NET 11's x86-64-v2 baseline, updates Magick.NET, adds PDF files to LFS, and converts many internal helper and codec APIs from Span to ReadOnlySpan where inputs are not mutated. --- .gitattributes | 1 + .github/workflows/build-and-test.yml | 37 +++++++------- .github/workflows/code-coverage.yml | 4 +- Directory.Build.props | 8 --- global.json | 18 +++++++ shared-infrastructure | 2 +- .../Common/Extensions/StreamExtensions.cs | 4 +- src/ImageSharp/Common/Helpers/Numerics.cs | 42 +++------------- .../Common/Helpers/SimdUtils.Shuffle.cs | 6 +-- src/ImageSharp/Common/Helpers/SimdUtils.cs | 4 +- .../Decompressors/B44ExrCompression.cs | 6 +-- .../Exr/Compression/ExrBaseDecompressor.cs | 4 +- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 16 +++--- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 8 +-- .../Formats/Jpeg/Components/Block8x8.cs | 4 +- .../Formats/Jpeg/Components/Block8x8F.cs | 4 +- .../Formats/Jpeg/JpegDecoderCore.cs | 6 +-- .../Formats/Jpeg/JpegEncoderCore.cs | 6 +-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 6 +-- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 6 +-- .../Compressors/T6BitCompressor.cs | 4 +- .../Compression/Compressors/TiffLzwEncoder.cs | 6 +-- .../Formats/Tiff/TiffDecoderCore.cs | 18 +++---- .../Formats/Tiff/Writers/TiffStreamWriter.cs | 4 +- src/ImageSharp/Formats/Webp/AlphaDecoder.cs | 12 ++--- .../Formats/Webp/BitWriter/BitWriterBase.cs | 4 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 4 +- .../Webp/Lossless/BackwardReferenceEncoder.cs | 4 +- .../Webp/Lossless/ColorSpaceTransformUtils.cs | 6 +-- .../Formats/Webp/Lossless/CostModel.cs | 2 +- .../Formats/Webp/Lossless/HistogramEncoder.cs | 2 +- .../Formats/Webp/Lossless/HuffmanUtils.cs | 8 +-- .../Formats/Webp/Lossless/LosslessUtils.cs | 4 +- .../Formats/Webp/Lossless/NearLosslessEnc.cs | 2 +- .../Formats/Webp/Lossless/PredictorEncoder.cs | 10 ++-- .../Formats/Webp/Lossless/Vp8LBitEntropy.cs | 8 +-- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 16 +++--- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 10 ++-- .../Webp/Lossless/WebpLosslessDecoder.cs | 4 +- .../Formats/Webp/Lossy/LossyUtils.cs | 50 +++++++++---------- .../Formats/Webp/Lossy/Vp8EncIterator.cs | 2 +- .../Formats/Webp/Lossy/Vp8EncProba.cs | 2 +- .../Formats/Webp/Lossy/Vp8Encoding.cs | 28 +++++------ .../Formats/Webp/Lossy/Vp8Residual.cs | 2 +- .../Formats/Webp/Lossy/Vp8SegmentInfo.cs | 2 +- .../Formats/Webp/Lossy/YuvConversion.cs | 10 ++-- src/ImageSharp/ImageSharp.csproj | 4 +- .../PixelFormats/PixelOperations{TPixel}.cs | 2 +- tests/Directory.Build.targets | 2 +- .../ImageSharp.Benchmarks.csproj | 4 +- .../ImageSharp.PublicApi.Tests.csproj | 4 +- .../ImageSharp.Tests.ProfilingSandbox.csproj | 4 +- .../ImageSharp.Tests/ImageSharp.Tests.csproj | 4 +- .../FeatureTesting/FeatureTestRunner.cs | 7 +++ .../Tests/FeatureTestRunnerTests.cs | 28 ++++++----- 55 files changed, 235 insertions(+), 240 deletions(-) create mode 100644 global.json diff --git a/.gitattributes b/.gitattributes index 031420e53..a0643702b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -110,6 +110,7 @@ ############################################################################### # Handle image files by git lfs ############################################################################### +*.pdf filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text *.bmp filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 634b904ae..bb0962784 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -64,48 +64,48 @@ jobs: matrix: options: - os: ubuntu-latest - framework: net10.0 - sdk: 10.0.x + framework: net11.0 + sdk: 11.0.x sdk-preview: true runtime: -x64 codecov: false - os: macos-26 - framework: net10.0 - sdk: 10.0.x + framework: net11.0 + sdk: 11.0.x sdk-preview: true runtime: -x64 codecov: false - os: windows-latest - framework: net10.0 - sdk: 10.0.x + framework: net11.0 + sdk: 11.0.x sdk-preview: true runtime: -x64 codecov: false - os: ubuntu-22.04-arm - framework: net10.0 - sdk: 10.0.x + framework: net11.0 + sdk: 11.0.x sdk-preview: true runtime: -x64 codecov: false - os: ubuntu-latest - framework: net8.0 - sdk: 8.0.x + framework: net10.0 + sdk: 10.0.x runtime: -x64 codecov: false - os: macos-26 - framework: net8.0 - sdk: 8.0.x + framework: net10.0 + sdk: 10.0.x runtime: -x64 codecov: false - os: windows-latest - framework: net8.0 - sdk: 8.0.x + framework: net10.0 + sdk: 10.0.x runtime: -x64 codecov: false - os: ubuntu-22.04-arm - framework: net8.0 - sdk: 8.0.x + framework: net10.0 + sdk: 10.0.x runtime: -x64 codecov: false @@ -169,14 +169,15 @@ jobs: uses: actions/setup-dotnet@v6 with: dotnet-version: | - 8.0.x + 10.0.x - name: DotNet Setup Preview if: ${{ matrix.options.sdk-preview == true }} uses: actions/setup-dotnet@v6 with: + dotnet-quality: preview dotnet-version: | - 10.0.x + 11.0.x - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 00f489b40..eca2f190f 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -11,7 +11,7 @@ jobs: matrix: options: - os: ubuntu-latest - framework: net8.0 + framework: net10.0 runtime: -x64 codecov: true @@ -70,7 +70,7 @@ jobs: uses: actions/setup-dotnet@v6 with: dotnet-version: | - 8.0.x + 10.0.x - name: DotNet Build shell: pwsh diff --git a/Directory.Build.props b/Directory.Build.props index 9bda76f88..ab898020c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,14 +21,6 @@ - - 12.0 - - - - 14.0 - - - + diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 0351ecae9..e060e1f33 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -39,12 +39,12 @@ - net8.0;net10.0 + net10.0;net11.0 - net8.0 + net10.0 diff --git a/tests/ImageSharp.PublicApi.Tests/ImageSharp.PublicApi.Tests.csproj b/tests/ImageSharp.PublicApi.Tests/ImageSharp.PublicApi.Tests.csproj index 33bbc1b94..bab8095c5 100644 --- a/tests/ImageSharp.PublicApi.Tests/ImageSharp.PublicApi.Tests.csproj +++ b/tests/ImageSharp.PublicApi.Tests/ImageSharp.PublicApi.Tests.csproj @@ -10,12 +10,12 @@ - net8.0;net10.0 + net10.0;net11.0 - net8.0 + net10.0 diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index f3aa910b9..658e32999 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -18,12 +18,12 @@ - net8.0;net10.0 + net10.0;net11.0 - net8.0 + net10.0 diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 25d30b514..9c9ee8da6 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -12,12 +12,12 @@ - net8.0;net10.0 + net10.0;net11.0 - net8.0 + net10.0 diff --git a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs index bbe5f221a..d0b77882d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs +++ b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs @@ -414,7 +414,14 @@ public static class FeatureTestRunner // Not a COMPlus value. We filter in calling method. features.Add(key, nameof(HwIntrinsics.AllowAll)); break; +#if NET11_0_OR_GREATER + case nameof(HwIntrinsics.DisableSSE42): + // SSE3 through SSE4.2 and POPCNT are x86-64-v2 baseline in .NET 11+ and the + // switch is inert, so skip it entirely rather than spawn a child that tests nothing. + // https://learn.microsoft.com/en-us/dotnet/core/compatibility/jit/11/minimum-hardware-requirements + break; +#endif default: features.Add(key, intrinsic.Replace("Disable", "Enable")); break; diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs index 74ad85b79..8b9210636 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs @@ -11,20 +11,25 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests; public class FeatureTestRunnerTests { - public static TheoryData Intrinsics => + public static TheoryData Intrinsics => new() { - { HwIntrinsics.DisableAES | HwIntrinsics.AllowAll, ["EnableAES", "AllowAll"] }, - { HwIntrinsics.DisableHWIntrinsic, ["EnableHWIntrinsic"] }, - { HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, ["EnableSSE42", "EnableAVX"] } + { HwIntrinsics.DisableAES | HwIntrinsics.AllowAll, HwIntrinsics.DisableAES | HwIntrinsics.AllowAll, ["EnableAES", "AllowAll"] }, + { HwIntrinsics.DisableHWIntrinsic, HwIntrinsics.DisableHWIntrinsic, ["EnableHWIntrinsic"] }, +#if NET11_0_OR_GREATER + // ToFeatureKeyValueCollection filters DisableSSE42: SSE4.2 is x86-64-v2 baseline in .NET 11+ and cannot be disabled. + { HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, HwIntrinsics.DisableAVX, ["EnableAVX"] } +#else + { HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, ["EnableSSE42", "EnableAVX"] } +#endif }; [Theory] [MemberData(nameof(Intrinsics))] - public void ToFeatureCollectionReturnsExpectedResult(HwIntrinsics expectedIntrinsics, string[] expectedValues) + public void ToFeatureCollectionReturnsExpectedResult(HwIntrinsics intrinsics, HwIntrinsics expectedIntrinsics, string[] expectedValues) { - Dictionary features = expectedIntrinsics.ToFeatureKeyValueCollection(); - HwIntrinsics[] keys = features.Keys.ToArray(); + Dictionary features = intrinsics.ToFeatureKeyValueCollection(); + HwIntrinsics[] keys = [.. features.Keys]; HwIntrinsics actualIntrinsics = keys[0]; for (int i = 1; i < keys.Length; i++) @@ -122,9 +127,8 @@ public class FeatureTestRunnerTests Assert.False(Ssse3.IsSupported, "Ssse3 should be disabled."); Assert.False(Sse41.IsSupported, "Sse41 should be disabled."); Assert.False(Popcnt.IsSupported, "Popcnt should be disabled."); -#else - Assert.False(Sse42.IsSupported, "Sse42 should be disabled when DisableSSE42 is set."); #endif + Assert.False(Sse42.IsSupported, "Sse42 should be disabled when DisableSSE42 is set."); break; case HwIntrinsics.DisableAVX: Assert.False(Avx.IsSupported, "AVX should be disabled when DisableAVX is set."); @@ -169,12 +173,12 @@ public class FeatureTestRunnerTests { Assert.NotNull(serializable); Assert.NotNull(FeatureTestRunner.DeserializeForXunit(serializable)); - Assert.False(Sse42.IsSupported, "SSE42 should be disabled when DisableSSE42 is set (sanity check using serializable param overload)."); + Assert.False(Avx.IsSupported, "AVX should be disabled when DisableAVX is set (sanity check using serializable param overload)."); } FeatureTestRunner.RunWithHwIntrinsicsFeature( AssertHwIntrinsicsFeatureDisabled, - HwIntrinsics.DisableSSE42, + HwIntrinsics.DisableAVX, new FakeSerializable()); } @@ -256,7 +260,7 @@ public class FeatureTestRunnerTests } } - foreach (HwIntrinsics intrinsic in (HwIntrinsics[])Enum.GetValues(typeof(HwIntrinsics))) + foreach (HwIntrinsics intrinsic in Enum.GetValues()) { FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertHwIntrinsicsFeatureDisabled, intrinsic, new FakeSerializable()); } From 06283b71642d7ab655df287a006826e5b594f4b6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 21 Aug 2026 21:38:43 +1000 Subject: [PATCH 2/2] Fix IsAsync expectations for .NET 11 on Unix FileStream.IsAsync is now false for regular files on Unix. See https://github.com/dotnet/runtime/pull/125220 --- tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs index a1eeb2597..56864680d 100644 --- a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs +++ b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs @@ -50,7 +50,12 @@ public class LocalFileSystemTests await using (FileStream stream = (FileStream)fs.OpenReadAsynchronous(path)) using (StreamReader reader = new(stream)) { + // .NET 11 reports IsAsync false for regular files on Unix: https://github.com/dotnet/runtime/pull/125220 +#if NET11_0_OR_GREATER + Assert.Equal(OperatingSystem.IsWindows(), stream.IsAsync); +#else Assert.True(stream.IsAsync); +#endif Assert.True(stream.CanRead); Assert.False(stream.CanWrite); @@ -105,7 +110,12 @@ public class LocalFileSystemTests await using (FileStream stream = (FileStream)fs.CreateAsynchronous(path)) await using (StreamWriter writer = new(stream)) { + // .NET 11 reports IsAsync false for regular files on Unix: https://github.com/dotnet/runtime/pull/125220 +#if NET11_0_OR_GREATER + Assert.Equal(OperatingSystem.IsWindows(), stream.IsAsync); +#else Assert.True(stream.IsAsync); +#endif Assert.True(stream.CanRead); Assert.True(stream.CanWrite);