Browse Source

Merge branch 'main' into pr/2899

# Conflicts:
#	src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs
#	tests/ImageSharp.Tests/TestImages.cs
pull/2899/head
James Jackson-South 3 weeks ago
parent
commit
fac4a24827
  1. 19
      .gitattributes
  2. 3
      .github/copilot-instructions.md
  3. 24
      .github/workflows/build-and-test.yml
  4. 14
      .github/workflows/code-coverage.yml
  5. 2
      .gitignore
  6. 41
      AGENTS.md
  7. 3
      CLAUDE.md
  8. 3
      GEMINI.md
  9. 16
      ImageSharp.sln
  10. 8
      README.md
  11. 96
      SECURITY.md
  12. 2
      shared-infrastructure
  13. 9
      src/ImageSharp/Advanced/AdvancedImageExtensions.cs
  14. 174
      src/ImageSharp/Advanced/AotCompilerTools.cs
  15. 21
      src/ImageSharp/Advanced/IImageFrameVisitor.cs
  16. 4
      src/ImageSharp/Advanced/IImageVisitor.cs
  17. 4
      src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
  18. 19
      src/ImageSharp/Advanced/ParallelExecutionSettings.cs
  19. 59
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  20. 6
      src/ImageSharp/Color/Color.WernerPalette.cs
  21. 186
      src/ImageSharp/Color/Color.cs
  22. 74
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  23. 17
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
  24. 141
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
  25. 2
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
  26. 1
      src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs
  27. 6
      src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs
  28. 67
      src/ImageSharp/Common/Helpers/ColorNumerics.cs
  29. 404
      src/ImageSharp/Common/Helpers/Numerics.cs
  30. 32
      src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs
  31. 136
      src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs
  32. 45
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs
  33. 354
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs
  34. 137
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs
  35. 31
      src/ImageSharp/Common/Helpers/SimdUtils.Convert.cs
  36. 172
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  37. 372
      src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs
  38. 93
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs
  39. 322
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs
  40. 87
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs
  41. 900
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs
  42. 249
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs
  43. 76
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs
  44. 332
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
  45. 101
      src/ImageSharp/Common/Helpers/Vector128Utilities.cs
  46. 86
      src/ImageSharp/Common/Helpers/Vector256Utilities.cs
  47. 99
      src/ImageSharp/Common/Helpers/Vector512Utilities.cs
  48. 72
      src/ImageSharp/Common/Tuples/Octet{T}.cs
  49. 119
      src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs
  50. 125
      src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs
  51. 277
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  52. 14
      src/ImageSharp/Configuration.cs
  53. 2
      src/ImageSharp/Formats/Bmp/BmpConstants.cs
  54. 71
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  55. 2
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  56. 2
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  57. 2
      src/ImageSharp/Formats/Bmp/BmpFormat.cs
  58. 2
      src/ImageSharp/Formats/DecoderOptions.cs
  59. 4
      src/ImageSharp/Formats/EncodingUtilities.cs
  60. 38
      src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs
  61. 86
      src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs
  62. 205
      src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
  63. 41
      src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
  64. 153
      src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs
  65. 98
      src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
  66. 42
      src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
  67. 75
      src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs
  68. 112
      src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
  69. 43
      src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs
  70. 45
      src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs
  71. 63
      src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs
  72. 30
      src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs
  73. 21
      src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs
  74. 25
      src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs
  75. 25
      src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs
  76. 43
      src/ImageSharp/Formats/Exr/ExrAttribute.cs
  77. 35
      src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs
  78. 48
      src/ImageSharp/Formats/Exr/ExrBox2i.cs
  79. 60
      src/ImageSharp/Formats/Exr/ExrChannelInfo.cs
  80. 18
      src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs
  81. 82
      src/ImageSharp/Formats/Exr/ExrConstants.cs
  82. 48
      src/ImageSharp/Formats/Exr/ExrDecoder.cs
  83. 1023
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  84. 13
      src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs
  85. 29
      src/ImageSharp/Formats/Exr/ExrEncoder.cs
  86. 710
      src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
  87. 34
      src/ImageSharp/Formats/Exr/ExrFormat.cs
  88. 108
      src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs
  89. 34
      src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs
  90. 157
      src/ImageSharp/Formats/Exr/ExrMetadata.cs
  91. 33
      src/ImageSharp/Formats/Exr/ExrThrowHelper.cs
  92. 52
      src/ImageSharp/Formats/Exr/ExrUtils.cs
  93. 4
      src/ImageSharp/Formats/Exr/README.md
  94. 171
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  95. 115
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  96. 8
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  97. 17
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  98. 68
      src/ImageSharp/Formats/ImageDecoderCore.cs
  99. 326
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs
  100. 40
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs

19
.gitattributes

@ -84,19 +84,16 @@
# treat as binary
###############################################################################
*.basis binary
*.a binary
*.dll binary
*.eot binary
*.dylib binary
*.exe binary
*.otf binary
*.pdf binary
*.ppt binary
*.pptx binary
*.pvr binary
*.so binary
*.snk binary
*.ttc binary
*.ttf binary
*.woff binary
*.woff2 binary
*.xls binary
*.xlsx binary
###############################################################################
@ -126,6 +123,7 @@
*.dds filter=lfs diff=lfs merge=lfs -text
*.ktx filter=lfs diff=lfs merge=lfs -text
*.ktx2 filter=lfs diff=lfs merge=lfs -text
*.astc filter=lfs diff=lfs merge=lfs -text
*.pam filter=lfs diff=lfs merge=lfs -text
*.pbm filter=lfs diff=lfs merge=lfs -text
*.pgm filter=lfs diff=lfs merge=lfs -text
@ -143,3 +141,12 @@
# Handle ICC files by git lfs
###############################################################################
*.icc filter=lfs diff=lfs merge=lfs -text
###############################################################################
# Handle font files by git lfs
###############################################################################
*.eot filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.ttc filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.woff filter=lfs diff=lfs merge=lfs -text
*.woff2 filter=lfs diff=lfs merge=lfs -text

3
.github/copilot-instructions.md

@ -0,0 +1,3 @@
# GitHub Copilot Instructions
Read and follow [AGENTS.md](../AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Prefer existing local patterns and repository configuration whenever generated code or suggestions are accepted.

24
.github/workflows/build-and-test.yml

@ -27,7 +27,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
@ -49,7 +49,7 @@ jobs:
run: echo "lfs_key=$LFS_KEY" >> "$GITHUB_OUTPUT"
- name: Git Setup LFS Cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: .git/lfs
key: ${{ steps.expose-key.outputs.lfs_key }}
@ -137,14 +137,14 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
# Use the warmed key from WarmLFS. Do not recompute or recreate .lfs-assets-id here.
- name: Git Setup LFS Cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: .git/lfs
key: ${{ needs.WarmLFS.outputs.lfs_key }}
@ -154,10 +154,10 @@ jobs:
run: git lfs pull
- name: NuGet Install
uses: NuGet/setup-nuget@v2
uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
uses: actions/cache@v4
uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget
@ -166,14 +166,14 @@ jobs:
- name: DotNet Setup
if: ${{ matrix.options.sdk-preview != true }}
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
- name: DotNet Setup Preview
if: ${{ matrix.options.sdk-preview == true }}
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
10.0.x
@ -209,7 +209,7 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
@ -227,16 +227,16 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- name: NuGet Install
uses: NuGet/setup-nuget@v2
uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
uses: actions/cache@v4
uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget

14
.github/workflows/code-coverage.yml

@ -31,7 +31,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
@ -46,7 +46,7 @@ jobs:
run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id
- name: Git Setup LFS Cache
uses: actions/cache@v4
uses: actions/cache@v6
id: lfs-cache
with:
path: .git/lfs
@ -56,10 +56,10 @@ jobs:
run: git lfs pull
- name: NuGet Install
uses: NuGet/setup-nuget@v2
uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
uses: actions/cache@v4
uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget
@ -67,7 +67,7 @@ jobs:
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Setup
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
@ -86,14 +86,14 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
path: tests/Images/ActualOutput/
- name: Codecov Update
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v7
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
with:
flags: unittests

2
.gitignore

@ -227,3 +227,5 @@ artifacts/
#lfs
hooks/**
lfs/**
.dotnet

41
AGENTS.md

@ -0,0 +1,41 @@
# Six Labors AI Coding Guidelines
These instructions apply to the entire repository. More-specific `AGENTS.md` files may add to or override them for their directory tree.
## Working Practices
- Inspect the relevant implementation, tests, benchmarks, project files, and nearby code before proposing or making changes. Do not infer current behavior when the source is available.
- Make the smallest complete change that solves the requested problem. Avoid unrelated cleanup, speculative abstractions, and formatting churn.
- Match established architecture, naming, formatting, documentation, and test patterns. Treat `.editorconfig`, analyzers, and repository build settings as authoritative.
- Preserve public API and observable behavior unless the task explicitly requires a change. Public API documentation must describe observable behavior, not implementation details.
- Do not use reflection against built assemblies, ad hoc assembly loading, or temporary probe projects unless explicitly requested.
- Build .NET projects in Release configuration unless explicitly instructed otherwise.
## Performance
- Treat throughput, latency, memory use, and binary size as design constraints, especially in pixel-processing, drawing, parsing, encoding, and other hot paths.
- Avoid unnecessary allocations, copies, boxing, closures, interface dispatch, repeated enumeration, and extra passes over data.
- Reuse the repository's existing memory ownership, pooling, span, vectorization, and parallelization patterns. Do not introduce a new mechanism when an established one fits.
- Keep hot loops simple and bounds-check-friendly. Hoist invariant work, preserve locality, and use the narrowest suitable data types without sacrificing correctness.
- Do not trade correctness or maintainability for assumed speed. Support non-obvious optimizations with measurements or clear evidence, and add or update benchmarks when performance is the purpose of the change.
- Consider all supported target frameworks and runtime capabilities. Do not regress fallback paths while optimizing newer runtimes.
## C# Conventions
- Follow the existing code around the change; local patterns take precedence over generic preferences.
- Do not use `record` or `record struct` types.
- Prefer established invariants over redundant guards. Validate at real external boundaries and do not add defensive checks for internally controlled states.
- Do not extract single-use helpers merely to name a block. Extract only for genuine reuse, an established local pattern, or meaningful complexity reduction.
- Add vertical whitespace after multi-line statements and declarations and between distinct logical stages. Never add trailing whitespace.
- Document every method, constructor, and property, regardless of whether it is public, internal, protected, or private. Keep public API documentation limited to observable behavior; use private and internal documentation to capture the contract and intent needed to maintain the code.
- Add inline comments throughout complex code. Explain algorithms, formulas, invariants, ownership, compatibility behavior, and performance tradeoffs at the operations and decisions they govern. Explain why the code is shaped that way rather than narrating the syntax.
- Document SIMD code especially thoroughly. Explain the vector layout, lane meaning, widening or narrowing, masks, shuffles, constants, alignment or remainder handling, supported instruction paths, scalar equivalence, and the reason each non-obvious operation is correct.
- Write algorithm and SIMD comments for a maintainer who is unfamiliar with the implementation. The reader should not need to reconstruct intent from external documentation, issue history, or benchmark results.
## Verification
- Add or update focused tests when behavior changes, following the test framework and conventions already used by the project.
- Never hack, weaken, skip, conditionally bypass, or otherwise manipulate a test to make it pass. Fix the production defect or the genuine test defect while preserving the test's intended coverage and sensitivity.
- Do not update golden files, reference images, snapshots, baselines, or expected-output artifacts to resolve a test failure. Treat a mismatch as evidence to investigate and correct the implementation.
- Run the narrowest relevant formatting, test, and Release build commands, then expand verification in proportion to the risk and scope of the change.
- Report what changed, the verification performed, and any remaining risks or unverified assumptions.

3
CLAUDE.md

@ -0,0 +1,3 @@
# Claude Code Instructions
Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `CLAUDE.md` found below the files being changed.

3
GEMINI.md

@ -0,0 +1,3 @@
# Gemini CLI Instructions
Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `GEMINI.md` found below the files being changed.

16
ImageSharp.sln

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
# Visual Studio Version 18
VisualStudioVersion = 18.5.11723.231 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}"
ProjectSection(SolutionItems) = preProject
@ -37,14 +37,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{815C0625-CD3
ProjectSection(SolutionItems) = preProject
src\Directory.Build.props = src\Directory.Build.props
src\Directory.Build.targets = src\Directory.Build.targets
src\README.md = src\README.md
src\ImageSharp.ruleset = src\ImageSharp.ruleset
src\README.md = src\README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp", "src\ImageSharp\ImageSharp.csproj", "{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{56801022-D71A-4FBE-BC5B-CBA08E2284EC}"
ProjectSection(SolutionItems) = preProject
tests\coverlet.runsettings = tests\coverlet.runsettings
tests\Directory.Build.props = tests\Directory.Build.props
tests\Directory.Build.targets = tests\Directory.Build.targets
tests\ImageSharp.Tests.ruleset = tests\ImageSharp.Tests.ruleset
@ -215,6 +216,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Jpg\issues\issue-1076-invalid-subsampling.jpg = tests\Images\Input\Jpg\issues\issue-1076-invalid-subsampling.jpg
tests\Images\Input\Jpg\issues\issue-1221-identify-multi-frame.jpg = tests\Images\Input\Jpg\issues\issue-1221-identify-multi-frame.jpg
tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
tests\Images\Input\Jpg\issues\issue1006-incorrect-resize.jpg = tests\Images\Input\Jpg\issues\issue1006-incorrect-resize.jpg
tests\Images\Input\Jpg\issues\issue1049-exif-resize.jpg = tests\Images\Input\Jpg\issues\issue1049-exif-resize.jpg
tests\Images\Input\Jpg\issues\Issue159-MissingFF00-Progressive-Bedroom.jpg = tests\Images\Input\Jpg\issues\Issue159-MissingFF00-Progressive-Bedroom.jpg
@ -238,7 +240,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg = tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg
tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg = tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg
tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg = tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg
tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}"
@ -555,6 +556,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Webp", "Webp", "{983A31E2-5
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Tests", "tests\ImageSharp.Tests\ImageSharp.Tests.csproj", "{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.PublicApi.Tests", "tests\ImageSharp.PublicApi.Tests\ImageSharp.PublicApi.Tests.csproj", "{7D89D21A-0F54-4B36-BEAA-2B90994E840E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Benchmarks", "tests\ImageSharp.Benchmarks\ImageSharp.Benchmarks.csproj", "{2BF743D8-2A06-412D-96D7-F448F00C5EA5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C0D7754B-5277-438E-ABEB-2BA34401B5A7}"
@ -681,6 +684,10 @@ Global
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU
{7D89D21A-0F54-4B36-BEAA-2B90994E840E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D89D21A-0F54-4B36-BEAA-2B90994E840E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D89D21A-0F54-4B36-BEAA-2B90994E840E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D89D21A-0F54-4B36-BEAA-2B90994E840E}.Release|Any CPU.Build.0 = Release|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -713,6 +720,7 @@ Global
{E1C42A6F-913B-4A7B-B1A8-2BB62843B254} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{983A31E2-5E26-4058-BD6E-03B4922D4BBF} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{7D89D21A-0F54-4B36-BEAA-2B90994E840E} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{2BF743D8-2A06-412D-96D7-F448F00C5EA5} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{C0D7754B-5277-438E-ABEB-2BA34401B5A7} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D}
{68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}

8
README.md

@ -10,16 +10,14 @@ SixLabors.ImageSharp
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions)
[![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp)
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)
</div>
### **ImageSharp** is a new, fully featured, fully managed, cross-platform, 2D graphics API.
### **ImageSharp** is a high-performance, fully managed, cross-platform 2D graphics API.
ImageSharp is a new, fully featured, fully managed, cross-platform, 2D graphics library.
Designed to simplify image processing, ImageSharp brings you an incredibly powerful yet beautifully simple API.
ImageSharp is a mature, fully featured, high-performance image processing and graphics library for .NET, built for workloads across device, cloud, and embedded/IoT scenarios.
ImageSharp is designed from the ground up to be flexible and extensible. The library provides API endpoints for common image processing operations and the building blocks to allow for the development of additional operations.
Designed from the ground up to balance performance, portability, and ease of use, ImageSharp provides a powerful yet approachable API for common image processing tasks, along with the low-level building blocks needed to extend the library for specialized workflows.
Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp can be used in device, cloud, and embedded/IoT scenarios.

96
SECURITY.md

@ -0,0 +1,96 @@
# Security Policy
## Supported Versions
Six Labors provides security fixes only for the latest major version of each library.
Older major versions are end-of-life and do not receive security fixes.
Users must upgrade to the latest major version to receive security fixes.
| Version | Supported |
| -------------------- | --------- |
| Latest major version | Yes |
| Older major versions | No |
Security fixes, if any, are provided at Six Labors' discretion.
This policy does not create any obligation to provide support, maintenance services, SLAs, custom fixes, hosted services, managed services, operational monitoring, professional services, consulting, or certification of customer products.
## Reporting a Vulnerability
Please report suspected security vulnerabilities using GitHub private vulnerability reporting for the relevant Six Labors repository, where available.
If GitHub private vulnerability reporting is not available for a repository, please report suspected security vulnerabilities by contacting Six Labors through the contact details published on the Six Labors website.
Do not report security vulnerabilities through public GitHub issues.
When reporting a vulnerability, please include as much relevant information as possible:
* affected package and version
* target framework and runtime
* operating system
* input file or minimal reproduction, if safe to share
* expected and actual behavior
* potential security impact
* whether you believe the issue is being actively exploited
Six Labors may review reported vulnerabilities and determine whether they are security issues affecting a supported version.
A report may be declined or closed without action if, in Six Labors' opinion, it:
* is not reproducible
* does not affect a supported version
* affects only an unsupported or end-of-life version
* is not a security vulnerability
* depends on unsafe, unsupported, or unintended use
* depends on a vulnerable application, environment, dependency, configuration, or deployment outside the Six Labors library itself
* lacks sufficient information for assessment
* is duplicative
* has already been fixed
* is otherwise outside the scope of this policy
If a vulnerability is accepted, Six Labors may handle it through GitHub Security Advisories and, where appropriate, CVE assignment.
Six Labors does not guarantee any response time, fix time, release date, advisory publication date, CVE assignment, workaround, mitigation, or particular outcome for any report.
## Scope
This policy applies only to security vulnerabilities in Six Labors libraries themselves.
This policy does not apply to:
* customer applications
* customer products
* customer deployments
* customer infrastructure
* customer data
* third-party services
* unsupported versions
* end-of-life versions
* forks or modified versions
* usage outside the documented or intended behavior of the relevant library
Organizations using Six Labors libraries are responsible for assessing, securing, testing, monitoring, updating, and maintaining their own applications, products, deployments, infrastructure, and supply chains.
## Cyber Resilience Act
Six Labors libraries are general-purpose software libraries.
They are not cybersecurity products, identity or access management systems, password managers, operating systems, browsers, firewalls, network management tools, SIEM tools, hypervisors, container runtimes, or other Cyber Resilience Act important or critical product classes.
If a Six Labors library is treated as a product with digital elements under the Cyber Resilience Act, Six Labors assesses it as an ordinary software component.
Organizations incorporating Six Labors libraries into products made available on the EU market are responsible for assessing and meeting their own regulatory obligations for those products, including any obligations under the Cyber Resilience Act.
Six Labors does not provide support, maintenance services, SLAs, managed services, hosted services, operational monitoring, custom fixes, professional services, consulting, or certification of customer products.
Security vulnerabilities in supported Six Labors libraries are handled through the GitHub Security Advisory process for the relevant repository, where appropriate.
From 11 September 2026, if Six Labors becomes aware of credible active exploitation of a vulnerability in a supported Six Labors library, or a severe security incident affecting a supported Six Labors library, Six Labors may report the matter through the applicable Cyber Resilience Act reporting mechanism where legally required.
## No Warranty
Six Labors libraries are provided in accordance with their applicable license terms.
Nothing in this policy creates any warranty, representation, guarantee, support obligation, maintenance obligation, service commitment, regulatory certification, or assumption of responsibility for any customer product, customer deployment, customer compliance obligation, or third-party system.

2
shared-infrastructure

@ -1 +1 @@
Subproject commit a1d3ac20494631e3cc13132897573796b0e4ee6d
Subproject commit 74b7f32b8e41fdf8fe2f3eda54fd5a82ebbedfbc

9
src/ImageSharp/Advanced/AdvancedImageExtensions.cs

@ -76,6 +76,15 @@ public static class AdvancedImageExtensions
public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default)
=> source.AcceptAsync(visitor, cancellationToken);
/// <summary>
/// Accepts a <see cref="IImageVisitor"/> to implement a double-dispatch pattern in order to
/// apply pixel-specific operations on non-generic <see cref="Image"/> instances
/// </summary>
/// <param name="source">The source image frame.</param>
/// <param name="visitor">The image visitor.</param>
public static void AcceptVisitor(this ImageFrame source, IImageFrameVisitor visitor)
=> source.Accept(visitor);
/// <summary>
/// Gets the representation of the pixels as a <see cref="IMemoryGroup{T}"/> containing the backing pixel data of the image
/// stored in row major order, as a list of contiguous <see cref="Memory{T}"/> blocks in the source image's pixel format.

174
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -7,7 +7,10 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
@ -54,7 +57,7 @@ internal static class AotCompilerTools
/// <remarks>
/// This method doesn't actually do anything but serves an important purpose...
/// If you are running ImageSharp on iOS and try to call SaveAsGif, it will throw an exception:
/// "Attempting to JIT compile method... OctreeFrameQuantizer.ConstructPalette... while running in aot-only mode."
/// "Attempting to JIT compile method... HexadecatreeQuantizer.ConstructPalette... while running in aot-only mode."
/// The reason this happens is the SaveAsGif method makes heavy use of generics, which are too confusing for the AoT
/// compiler used on Xamarin.iOS. It spins up the JIT compiler to try and figure it out, but that is an illegal op on
/// iOS so it bombs out.
@ -81,10 +84,13 @@ internal static class AotCompilerTools
Seed<A8>();
Seed<Argb32>();
Seed<Argb32P>();
Seed<Abgr32>();
Seed<Abgr32P>();
Seed<Bgr24>();
Seed<Bgr565>();
Seed<Bgra32>();
Seed<Bgra32P>();
Seed<Bgra4444>();
Seed<Bgra5551>();
Seed<Byte4>();
@ -95,16 +101,23 @@ internal static class AotCompilerTools
Seed<HalfSingle>();
Seed<HalfVector2>();
Seed<HalfVector4>();
Seed<HalfVector4P>();
Seed<NormalizedByte2>();
Seed<NormalizedByte4>();
Seed<NormalizedByte4P>();
Seed<NormalizedShort2>();
Seed<NormalizedShort4>();
Seed<Rg32>();
Seed<Rgb24>();
Seed<Rgb48>();
Seed<Rgb96>();
Seed<Rgba1010102>();
Seed<Rgba128>();
Seed<Rgba32>();
Seed<Rgba32P>();
Seed<Rgba64>();
Seed<RgbaHalf>();
Seed<RgbaHalfP>();
Seed<RgbaVector>();
Seed<Short2>();
Seed<Short4>();
@ -117,6 +130,67 @@ internal static class AotCompilerTools
throw new InvalidOperationException("This method is used for AOT code generation only. Do not call it at runtime.");
}
/// <summary>
/// Seeds the modern .NET AOT compiler with bulk pixel operations for every built-in pixel format.
/// </summary>
/// <exception cref="InvalidOperationException">
/// This method is used for AOT code generation only. Do not call it at runtime.
/// </exception>
[Preserve]
public static void SeedPixelOperations()
{
try
{
// Keep this inventory finite and explicit. ImageSharp cannot precompile consumer-defined pixel types, while
// these closed calls make every built-in specialization visible without retaining the much larger legacy seed graph.
AotCompilePixelOperations<A8>();
AotCompilePixelOperations<Argb32>();
AotCompilePixelOperations<Argb32P>();
AotCompilePixelOperations<Abgr32>();
AotCompilePixelOperations<Abgr32P>();
AotCompilePixelOperations<Bgr24>();
AotCompilePixelOperations<Bgr565>();
AotCompilePixelOperations<Bgra32>();
AotCompilePixelOperations<Bgra32P>();
AotCompilePixelOperations<Bgra4444>();
AotCompilePixelOperations<Bgra5551>();
AotCompilePixelOperations<Byte4>();
AotCompilePixelOperations<L16>();
AotCompilePixelOperations<L8>();
AotCompilePixelOperations<La16>();
AotCompilePixelOperations<La32>();
AotCompilePixelOperations<HalfSingle>();
AotCompilePixelOperations<HalfVector2>();
AotCompilePixelOperations<HalfVector4>();
AotCompilePixelOperations<HalfVector4P>();
AotCompilePixelOperations<NormalizedByte2>();
AotCompilePixelOperations<NormalizedByte4>();
AotCompilePixelOperations<NormalizedByte4P>();
AotCompilePixelOperations<NormalizedShort2>();
AotCompilePixelOperations<NormalizedShort4>();
AotCompilePixelOperations<Rg32>();
AotCompilePixelOperations<Rgb24>();
AotCompilePixelOperations<Rgb48>();
AotCompilePixelOperations<Rgb96>();
AotCompilePixelOperations<Rgba1010102>();
AotCompilePixelOperations<Rgba128>();
AotCompilePixelOperations<Rgba32>();
AotCompilePixelOperations<Rgba32P>();
AotCompilePixelOperations<Rgba64>();
AotCompilePixelOperations<RgbaHalf>();
AotCompilePixelOperations<RgbaHalfP>();
AotCompilePixelOperations<RgbaVector>();
AotCompilePixelOperations<Short2>();
AotCompilePixelOperations<Short4>();
}
catch
{
// The calls only need to exist in IL; this method must never contribute a runtime execution path.
}
throw new InvalidOperationException("This method is used for AOT code generation only. Do not call it at runtime.");
}
/// <summary>
/// Seeds the compiler using the given pixel format.
/// </summary>
@ -127,6 +201,7 @@ internal static class AotCompilerTools
{
// This is we actually call all the individual methods you need to seed.
AotCompileImage<TPixel>();
AotCompilePixelOperations<TPixel>();
AotCompileImageProcessingContextFactory<TPixel>();
AotCompileImageEncoderInternals<TPixel>();
AotCompileImageDecoderInternals<TPixel>();
@ -147,6 +222,68 @@ internal static class AotCompilerTools
// TODO: Do the discovery work to figure out what works and what doesn't.
}
/// <summary>
/// Seeds the selected <see cref="PixelOperations{TPixel}"/> methods required by Mono WASM AOT.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompilePixelOperations<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
// These default arguments are never consumed. Direct calls are required so the IL contains the exact closed
// MethodSpecs that Mono WASM AOT can otherwise miss when following static-abstract pixel dispatch indirectly.
PixelOperations<TPixel> operations = PixelOperations<TPixel>.Instance;
_ = operations.GetPixelTypeInfo();
_ = operations.GetPixelBlender(default(GraphicsOptions));
_ = operations.GetPixelBlender(default, default);
operations.FromVector4Destructive(default, default, default);
operations.FromVector4Destructive(default, default, default, default);
operations.ToVector4(default, default, default);
operations.ToVector4(default, default, default, default);
operations.PackFromRgbPlanes(default, default, default, default);
operations.UnpackIntoRgbPlanes(default, default, default, default);
operations.FromArgb32Bytes(default, default, default, default);
operations.ToArgb32Bytes(default, default, default, default);
operations.FromAbgr32Bytes(default, default, default, default);
operations.ToAbgr32Bytes(default, default, default, default);
operations.FromBgr24Bytes(default, default, default, default);
operations.ToBgr24Bytes(default, default, default, default);
operations.FromBgra32Bytes(default, default, default, default);
operations.ToBgra32Bytes(default, default, default, default);
operations.FromL8Bytes(default, default, default, default);
operations.ToL8Bytes(default, default, default, default);
operations.FromL16Bytes(default, default, default, default);
operations.ToL16Bytes(default, default, default, default);
operations.FromLa16Bytes(default, default, default, default);
operations.ToLa16Bytes(default, default, default, default);
operations.FromLa32Bytes(default, default, default, default);
operations.ToLa32Bytes(default, default, default, default);
operations.FromRgb24Bytes(default, default, default, default);
operations.ToRgb24Bytes(default, default, default, default);
operations.FromRgba32Bytes(default, default, default, default);
operations.ToRgba32Bytes(default, default, default, default);
operations.FromRgb48Bytes(default, default, default, default);
operations.ToRgb48Bytes(default, default, default, default);
operations.FromRgba64Bytes(default, default, default, default);
operations.ToRgba64Bytes(default, default, default, default);
operations.FromBgra5551Bytes(default, default, default, default);
operations.ToBgra5551Bytes(default, default, default, default);
}
/// <summary>
/// This method pre-seeds the <see cref="Image{TPixel}"/> for a given pixel format in the AoT compiler.
/// </summary>
@ -158,10 +295,13 @@ internal static class AotCompilerTools
Image<TPixel> img = default;
img.CloneAs<A8>(default);
img.CloneAs<Argb32>(default);
img.CloneAs<Argb32P>(default);
img.CloneAs<Abgr32>(default);
img.CloneAs<Abgr32P>(default);
img.CloneAs<Bgr24>(default);
img.CloneAs<Bgr565>(default);
img.CloneAs<Bgra32>(default);
img.CloneAs<Bgra32P>(default);
img.CloneAs<Bgra4444>(default);
img.CloneAs<Bgra5551>(default);
img.CloneAs<Byte4>(default);
@ -172,16 +312,23 @@ internal static class AotCompilerTools
img.CloneAs<HalfSingle>(default);
img.CloneAs<HalfVector2>(default);
img.CloneAs<HalfVector4>(default);
img.CloneAs<HalfVector4P>(default);
img.CloneAs<NormalizedByte2>(default);
img.CloneAs<NormalizedByte4>(default);
img.CloneAs<NormalizedByte4P>(default);
img.CloneAs<NormalizedShort2>(default);
img.CloneAs<NormalizedShort4>(default);
img.CloneAs<Rg32>(default);
img.CloneAs<Rgb24>(default);
img.CloneAs<Rgb48>(default);
img.CloneAs<Rgb96>(default);
img.CloneAs<Rgba1010102>(default);
img.CloneAs<Rgba128>(default);
img.CloneAs<Rgba32>(default);
img.CloneAs<Rgba32P>(default);
img.CloneAs<Rgba64>(default);
img.CloneAs<RgbaHalf>(default);
img.CloneAs<RgbaHalfP>(default);
img.CloneAs<RgbaVector>(default);
img.CloneAs<Short2>(default);
img.CloneAs<Short4>(default);
@ -208,7 +355,10 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel<TPixel>
{
default(BmpEncoderCore).Encode<TPixel>(default, default, default);
default(CurEncoderCore).Encode<TPixel>(default, default, default);
default(ExrEncoderCore).Encode<TPixel>(default, default, default);
default(GifEncoderCore).Encode<TPixel>(default, default, default);
default(IcoEncoderCore).Encode<TPixel>(default, default, default);
default(JpegEncoderCore).Encode<TPixel>(default, default, default);
default(PbmEncoderCore).Encode<TPixel>(default, default, default);
default(PngEncoderCore).Encode<TPixel>(default, default, default);
@ -227,7 +377,10 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel<TPixel>
{
default(BmpDecoderCore).Decode<TPixel>(default, default, default);
default(CurDecoderCore).Decode<TPixel>(default, default, default);
default(ExrDecoderCore).Decode<TPixel>(default, default, default);
default(GifDecoderCore).Decode<TPixel>(default, default, default);
default(IcoDecoderCore).Decode<TPixel>(default, default, default);
default(JpegDecoderCore).Decode<TPixel>(default, default, default);
default(PbmDecoderCore).Decode<TPixel>(default, default, default);
default(PngDecoderCore).Decode<TPixel>(default, default, default);
@ -247,10 +400,14 @@ internal static class AotCompilerTools
{
AotCompileImageEncoder<TPixel, WebpEncoder>();
AotCompileImageEncoder<TPixel, BmpEncoder>();
AotCompileImageEncoder<TPixel, CurEncoder>();
AotCompileImageEncoder<TPixel, ExrEncoder>();
AotCompileImageEncoder<TPixel, GifEncoder>();
AotCompileImageEncoder<TPixel, IcoEncoder>();
AotCompileImageEncoder<TPixel, JpegEncoder>();
AotCompileImageEncoder<TPixel, PbmEncoder>();
AotCompileImageEncoder<TPixel, PngEncoder>();
AotCompileImageEncoder<TPixel, QoiEncoder>();
AotCompileImageEncoder<TPixel, TgaEncoder>();
AotCompileImageEncoder<TPixel, TiffEncoder>();
}
@ -265,10 +422,14 @@ internal static class AotCompilerTools
{
AotCompileImageDecoder<TPixel, WebpDecoder>();
AotCompileImageDecoder<TPixel, BmpDecoder>();
AotCompileImageDecoder<TPixel, CurDecoder>();
AotCompileImageDecoder<TPixel, ExrDecoder>();
AotCompileImageDecoder<TPixel, GifDecoder>();
AotCompileImageDecoder<TPixel, IcoDecoder>();
AotCompileImageDecoder<TPixel, JpegDecoder>();
AotCompileImageDecoder<TPixel, PbmDecoder>();
AotCompileImageDecoder<TPixel, PngDecoder>();
AotCompileImageDecoder<TPixel, QoiDecoder>();
AotCompileImageDecoder<TPixel, TgaDecoder>();
AotCompileImageDecoder<TPixel, TiffDecoder>();
}
@ -336,6 +497,7 @@ internal static class AotCompilerTools
AotCompileImageProcessor<TPixel, VignetteProcessor>();
AotCompileImageProcessor<TPixel, AdaptiveHistogramEqualizationProcessor>();
AotCompileImageProcessor<TPixel, AdaptiveHistogramEqualizationSlidingWindowProcessor>();
AotCompileImageProcessor<TPixel, AutoLevelProcessor>();
AotCompileImageProcessor<TPixel, GlobalHistogramEqualizationProcessor>();
AotCompileImageProcessor<TPixel, AchromatomalyProcessor>();
AotCompileImageProcessor<TPixel, AchromatopsiaProcessor>();
@ -368,11 +530,13 @@ internal static class AotCompilerTools
AotCompileImageProcessor<TPixel, PaletteDitherProcessor>();
AotCompileImageProcessor<TPixel, BokehBlurProcessor>();
AotCompileImageProcessor<TPixel, BoxBlurProcessor>();
AotCompileImageProcessor<TPixel, ConvolutionProcessor>();
AotCompileImageProcessor<TPixel, EdgeDetector2DProcessor>();
AotCompileImageProcessor<TPixel, EdgeDetectorCompassProcessor>();
AotCompileImageProcessor<TPixel, EdgeDetectorProcessor>();
AotCompileImageProcessor<TPixel, GaussianBlurProcessor>();
AotCompileImageProcessor<TPixel, GaussianSharpenProcessor>();
AotCompileImageProcessor<TPixel, MedianBlurProcessor>();
AotCompileImageProcessor<TPixel, AdaptiveThresholdProcessor>();
AotCompileImageProcessor<TPixel, BinaryThresholdProcessor>();
@ -479,7 +643,7 @@ internal static class AotCompilerTools
private static void AotCompileQuantizers<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
AotCompileQuantizer<TPixel, OctreeQuantizer>();
AotCompileQuantizer<TPixel, HexadecatreeQuantizer>();
AotCompileQuantizer<TPixel, PaletteQuantizer>();
AotCompileQuantizer<TPixel, WebSafePaletteQuantizer>();
AotCompileQuantizer<TPixel, WernerPaletteQuantizer>();
@ -523,10 +687,8 @@ internal static class AotCompilerTools
private static void AotCompilePixelMaps<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(EuclideanPixelMap<TPixel, HybridCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, AccurateCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, CoarseCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, NullCache>).GetClosestColor(default, out _);
}
/// <summary>
@ -551,8 +713,8 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel<TPixel>
where TDither : struct, IDither
{
OctreeQuantizer<TPixel> octree = default;
default(TDither).ApplyQuantizationDither<OctreeQuantizer<TPixel>, TPixel>(ref octree, default, default, default);
HexadecatreeQuantizer<TPixel> hexadecatree = default;
default(TDither).ApplyQuantizationDither<HexadecatreeQuantizer<TPixel>, TPixel>(ref hexadecatree, default, default, default);
PaletteQuantizer<TPixel> palette = default;
default(TDither).ApplyQuantizationDither<PaletteQuantizer<TPixel>, TPixel>(ref palette, default, default, default);

21
src/ImageSharp/Advanced/IImageFrameVisitor.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Advanced;
/// <summary>
/// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations
/// on non-generic <see cref="ImageFrame"/> instances.
/// </summary>
public interface IImageFrameVisitor
{
/// <summary>
/// Provides a pixel-specific implementation for a given operation.
/// </summary>
/// <param name="frame">The image frame.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
public void Visit<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>;
}

4
src/ImageSharp/Advanced/IImageVisitor.cs

@ -16,7 +16,7 @@ public interface IImageVisitor
/// </summary>
/// <param name="image">The image.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
void Visit<TPixel>(Image<TPixel> image)
public void Visit<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>;
}
@ -33,6 +33,6 @@ public interface IImageVisitorAsync
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task VisitAsync<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
public Task VisitAsync<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>;
}

4
src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs

@ -15,12 +15,12 @@ public interface IRowOperation<TBuffer>
/// </summary>
/// <param name="bounds">The bounds of the operation.</param>
/// <returns>The required buffer length.</returns>
int GetRequiredBufferLength(Rectangle bounds);
public int GetRequiredBufferLength(Rectangle bounds);
/// <summary>
/// Invokes the method passing the row and a buffer.
/// </summary>
/// <param name="y">The row y coordinate.</param>
/// <param name="span">The contiguous region of memory.</param>
void Invoke(int y, Span<TBuffer> span);
public void Invoke(int y, Span<TBuffer> span);
}

19
src/ImageSharp/Advanced/ParallelExecutionSettings.cs

@ -18,7 +18,10 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
/// <param name="maxDegreeOfParallelism">The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.</param>
/// <param name="maxDegreeOfParallelism">
/// The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// If set to <c>-1</c>, there is no limit on the number of concurrently running operations.
/// </param>
/// <param name="minimumPixelsProcessedPerTask">The value for <see cref="MinimumPixelsProcessedPerTask"/>.</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/>.</param>
public ParallelExecutionSettings(
@ -28,7 +31,7 @@ public readonly struct ParallelExecutionSettings
{
// Shall be compatible with ParallelOptions.MaxDegreeOfParallelism:
// https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.paralleloptions.maxdegreeofparallelism
if (maxDegreeOfParallelism == 0 || maxDegreeOfParallelism < -1)
if (maxDegreeOfParallelism is 0 or < -1)
{
throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
}
@ -44,7 +47,10 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
/// <param name="maxDegreeOfParallelism">The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.</param>
/// <param name="maxDegreeOfParallelism">
/// The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// If set to <c>-1</c>, there is no limit on the number of concurrently running operations.
/// </param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/>.</param>
public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator)
: this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator)
@ -58,6 +64,7 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Gets the value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// A value of <c>-1</c> means there is no limit on the number of concurrently running operations.
/// </summary>
public int MaxDegreeOfParallelism { get; }
@ -86,12 +93,10 @@ public readonly struct ParallelExecutionSettings
}
/// <summary>
/// Get the default <see cref="SixLabors.ImageSharp.Advanced.ParallelExecutionSettings"/> for a <see cref="SixLabors.ImageSharp.Configuration"/>
/// Get the default <see cref="ParallelExecutionSettings"/> for a <see cref="Configuration"/>
/// </summary>
/// <param name="configuration">The <see cref="Configuration"/>.</param>
/// <returns>The <see cref="ParallelExecutionSettings"/>.</returns>
public static ParallelExecutionSettings FromConfiguration(Configuration configuration)
{
return new ParallelExecutionSettings(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator);
}
=> new(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator);
}

59
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -50,8 +50,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@ -65,10 +64,10 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -115,8 +114,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
@ -135,10 +133,10 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -180,8 +178,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@ -192,10 +189,10 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -242,8 +239,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
@ -259,10 +255,10 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -272,6 +268,37 @@ public static partial class ParallelRowIterator
[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
/// <summary>
/// Creates the <see cref="ParallelOptions"/> for the current iteration.
/// </summary>
/// <param name="parallelSettings">The execution settings.</param>
/// <param name="numOfSteps">The number of row partitions to execute.</param>
/// <returns>The <see cref="ParallelOptions"/> instance.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static ParallelOptions CreateParallelOptions(in ParallelExecutionSettings parallelSettings, int numOfSteps)
=> new() { MaxDegreeOfParallelism = parallelSettings.MaxDegreeOfParallelism == -1 ? -1 : numOfSteps };
/// <summary>
/// Calculates the number of row partitions to execute for the given region.
/// </summary>
/// <param name="width">The width of the region.</param>
/// <param name="height">The height of the region.</param>
/// <param name="parallelSettings">The execution settings.</param>
/// <returns>The number of row partitions to execute.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetNumberOfSteps(int width, int height, in ParallelExecutionSettings parallelSettings)
{
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
if (parallelSettings.MaxDegreeOfParallelism == -1)
{
// Row batching cannot produce more useful partitions than the number of rows available.
return Math.Min(height, maxSteps);
}
return Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
}
private static void ValidateRectangle(Rectangle rectangle)
{
Guard.MustBeGreaterThan(

6
src/ImageSharp/Color/Color.WernerPalette.cs

@ -127,6 +127,10 @@ public partial struct Color
ParseHex("#8b7859"),
ParseHex("#9b856b"),
ParseHex("#766051"),
ParseHex("#453b32")
ParseHex("#453b32"),
// Werner does not define a transparent color, but we need to add one to
// make the palette work with the rest of the library.
Transparent
];
}

186
src/ImageSharp/Color/Color.cs

@ -21,29 +21,44 @@ public readonly partial struct Color : IEquatable<Color>
{
private readonly Vector4 data;
private readonly IPixel? boxedHighPrecisionPixel;
private readonly bool isAssociated;
private readonly bool dataIsAssociated;
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="vector">The <see cref="Vector4"/> containing the color information.</param>
/// <param name="alphaRepresentation">The alpha representation exposed by the color.</param>
/// <param name="dataAlphaRepresentation">The alpha representation of <paramref name="vector"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Color(Vector4 vector)
private Color(Vector4 vector, PixelAlphaRepresentation alphaRepresentation, PixelAlphaRepresentation dataAlphaRepresentation)
{
this.data = Numerics.Clamp(vector, Vector4.Zero, Vector4.One);
this.boxedHighPrecisionPixel = null;
this.isAssociated = alphaRepresentation == PixelAlphaRepresentation.Associated;
this.dataIsAssociated = dataAlphaRepresentation == PixelAlphaRepresentation.Associated;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The pixel containing color information.</param>
/// <param name="alphaRepresentation">The alpha representation of <paramref name="pixel"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Color(IPixel pixel)
private Color(IPixel pixel, PixelAlphaRepresentation alphaRepresentation)
{
this.boxedHighPrecisionPixel = pixel;
this.data = default;
this.isAssociated = alphaRepresentation == PixelAlphaRepresentation.Associated;
this.dataIsAssociated = this.isAssociated;
}
/// <summary>
/// Gets the alpha representation used by this color's scaled vector.
/// </summary>
public PixelAlphaRepresentation AlphaRepresentation
=> this.isAssociated ? PixelAlphaRepresentation.Associated : PixelAlphaRepresentation.Unassociated;
/// <summary>
/// Checks whether two <see cref="Color"/> structures are equal.
/// </summary>
@ -80,21 +95,46 @@ public readonly partial struct Color : IEquatable<Color>
{
// Avoid boxing in case we can convert to Vector4 safely and efficiently
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
if (info.ComponentInfo.HasValue)
{
return new Color(source.ToScaledVector4());
int maximumComponentPrecision = info.ComponentInfo.Value.GetMaximumComponentPrecision();
if (maximumComponentPrecision <= (int)PixelComponentBitDepth.Bit32)
{
if (info.AlphaRepresentation == PixelAlphaRepresentation.Associated && maximumComponentPrecision <= (int)PixelComponentBitDepth.Bit8)
{
// Associated formats with at most eight bits per component can be canonicalized without loss by their pixel-specific conversion.
// Higher-precision formats retain their associated values because unassociation can lose representable data.
Vector4 vector = source.ToUnassociatedScaledVector4();
return new Color(vector, info.AlphaRepresentation, PixelAlphaRepresentation.Unassociated);
}
return new Color(source.ToScaledVector4(), info.AlphaRepresentation, info.AlphaRepresentation);
}
}
return new Color(source);
return new Color(source, info.AlphaRepresentation);
}
/// <summary>
/// Creates a <see cref="Color"/> from a generic scaled <see cref="Vector4"/>.
/// </summary>
/// <param name="source">The vector to load the pixel from.</param>
/// <param name="source">The unassociated vector to load the color from.</param>
/// <returns>The <see cref="Color"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromScaledVector(Vector4 source)
=> new(source, PixelAlphaRepresentation.Unassociated, PixelAlphaRepresentation.Unassociated);
/// <summary>
/// Creates a <see cref="Color"/> from a generic scaled <see cref="Vector4"/> with the specified alpha representation.
/// </summary>
/// <param name="source">The vector to load the color from.</param>
/// <param name="alphaRepresentation">The alpha representation of <paramref name="source"/>.</param>
/// <returns>The <see cref="Color"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromScaledVector(Vector4 source) => new(source);
public static Color FromScaledVector(Vector4 source, PixelAlphaRepresentation alphaRepresentation)
=> new(source, alphaRepresentation, alphaRepresentation);
/// <summary>
/// Bulk converts a span of generic scaled <see cref="Vector4"/> to a span of <see cref="Color"/>.
@ -103,11 +143,23 @@ public readonly partial struct Color : IEquatable<Color>
/// <param name="destination">The destination color span.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromScaledVector(ReadOnlySpan<Vector4> source, Span<Color> destination)
=> FromScaledVector(source, destination, PixelAlphaRepresentation.Unassociated);
/// <summary>
/// Bulk converts a span of generic scaled <see cref="Vector4"/> values with the specified alpha representation
/// to a span of <see cref="Color"/> values.
/// </summary>
/// <param name="source">The source vector span.</param>
/// <param name="destination">The destination color span.</param>
/// <param name="alphaRepresentation">The alpha representation of the source vectors.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromScaledVector(ReadOnlySpan<Vector4> source, Span<Color> destination, PixelAlphaRepresentation alphaRepresentation)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector(source[i]);
destination[i] = FromScaledVector(source[i], alphaRepresentation);
}
}
@ -125,19 +177,37 @@ public readonly partial struct Color : IEquatable<Color>
// Avoid boxing in case we can convert to Vector4 safely and efficiently
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
if (info.ComponentInfo.HasValue)
{
for (int i = 0; i < source.Length; i++)
int maximumComponentPrecision = info.ComponentInfo.Value.GetMaximumComponentPrecision();
if (maximumComponentPrecision <= (int)PixelComponentBitDepth.Bit32)
{
destination[i] = FromScaledVector(source[i].ToScaledVector4());
if (info.AlphaRepresentation == PixelAlphaRepresentation.Associated && maximumComponentPrecision <= (int)PixelComponentBitDepth.Bit8)
{
// Match the scalar conversion by retaining exact unassociated values from the format-specific operation.
for (int i = 0; i < source.Length; i++)
{
Vector4 vector = source[i].ToUnassociatedScaledVector4();
destination[i] = new Color(vector, info.AlphaRepresentation, PixelAlphaRepresentation.Unassociated);
}
return;
}
for (int i = 0; i < source.Length; i++)
{
destination[i] = new Color(source[i].ToScaledVector4(), info.AlphaRepresentation, info.AlphaRepresentation);
}
return;
}
}
else
for (int i = 0; i < source.Length; i++)
{
for (int i = 0; i < source.Length; i++)
{
destination[i] = new Color(source[i]);
}
destination[i] = new Color(source[i], info.AlphaRepresentation);
}
}
@ -276,13 +346,13 @@ public readonly partial struct Color : IEquatable<Color>
/// Alters the alpha channel of the color, returning a new instance.
/// </summary>
/// <param name="alpha">The new value of alpha [0..1].</param>
/// <returns>The color having it's alpha channel altered.</returns>
/// <returns>The color having its alpha channel altered.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Color WithAlpha(float alpha)
{
Vector4 v = this.ToScaledVector4();
v.W = alpha;
return FromScaledVector(v);
Vector4 vector = this.ToScaledVector4(PixelAlphaRepresentation.Unassociated);
vector.W = Numerics.Clamp(alpha, 0, 1);
return new Color(vector, this.AlphaRepresentation, PixelAlphaRepresentation.Unassociated);
}
/// <summary>
@ -296,9 +366,7 @@ public readonly partial struct Color : IEquatable<Color>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ToHex(ColorHexFormat format = ColorHexFormat.Rgba)
{
Rgba32 rgba = (this.boxedHighPrecisionPixel is not null)
? this.boxedHighPrecisionPixel.ToRgba32()
: Rgba32.FromScaledVector4(this.data);
Rgba32 rgba = this.ToPixel<Rgba32>();
uint hexOrder = format switch
{
@ -327,29 +395,76 @@ public readonly partial struct Color : IEquatable<Color>
return pixel;
}
if (this.boxedHighPrecisionPixel is null)
Vector4 vector = this.boxedHighPrecisionPixel?.ToScaledVector4() ?? this.data;
if (this.dataIsAssociated)
{
return TPixel.FromScaledVector4(this.data);
// Preserve associated components directly while allowing the destination to quantize alpha to its own storage grid.
return TPixel.FromAssociatedScaledVector4(vector);
}
return TPixel.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
// Unassociated input lets an associated destination quantize alpha before it multiplies the color components.
return TPixel.FromUnassociatedScaledVector4(vector);
}
/// <summary>
/// Expands the color into a generic ("scaled") <see cref="Vector4"/> representation
/// with values scaled and clamped between <value>0</value> and <value>1</value>.
/// Expands the color into a generic ("scaled") <see cref="Vector4"/> representation,
/// preserving the <see cref="AlphaRepresentation"/>, with values scaled and clamped between
/// <value>0</value> and <value>1</value>.
/// The vector components are typically expanded in least to greatest significance order.
/// </summary>
/// <returns>The <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4()
{
if (this.boxedHighPrecisionPixel is null)
Vector4 vector = this.boxedHighPrecisionPixel?.ToScaledVector4() ?? this.data;
if (this.dataIsAssociated == this.isAssociated)
{
return vector;
}
if (this.isAssociated)
{
Numerics.Premultiply(ref vector);
}
else
{
Numerics.UnPremultiply(ref vector);
}
return vector;
}
/// <summary>
/// Expands the color into a generic ("scaled") <see cref="Vector4"/> using the specified alpha representation,
/// with values scaled and clamped between <value>0</value> and <value>1</value>.
/// </summary>
/// <param name="alphaRepresentation">
/// The alpha representation to apply. <see cref="PixelAlphaRepresentation.Associated"/> returns color components
/// multiplied by alpha; other representations return color components independent of alpha.
/// </param>
/// <returns>The <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4(PixelAlphaRepresentation alphaRepresentation)
{
bool targetIsAssociated = alphaRepresentation == PixelAlphaRepresentation.Associated;
Vector4 vector = this.boxedHighPrecisionPixel?.ToScaledVector4() ?? this.data;
if (this.dataIsAssociated == targetIsAssociated)
{
return vector;
}
if (targetIsAssociated)
{
Numerics.Premultiply(ref vector);
}
else
{
return this.data;
Numerics.UnPremultiply(ref vector);
}
return this.boxedHighPrecisionPixel.ToScaledVector4();
return vector;
}
/// <summary>
@ -377,10 +492,11 @@ public readonly partial struct Color : IEquatable<Color>
{
if (this.boxedHighPrecisionPixel is null && other.boxedHighPrecisionPixel is null)
{
return this.data == other.data;
return this.isAssociated == other.isAssociated && this.ToScaledVector4() == other.ToScaledVector4();
}
return this.boxedHighPrecisionPixel?.Equals(other.boxedHighPrecisionPixel) == true;
return this.isAssociated == other.isAssociated
&& this.boxedHighPrecisionPixel?.Equals(other.boxedHighPrecisionPixel) == true;
}
/// <inheritdoc />
@ -392,10 +508,10 @@ public readonly partial struct Color : IEquatable<Color>
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.GetHashCode();
return HashCode.Combine(this.ToScaledVector4(), this.isAssociated);
}
return this.boxedHighPrecisionPixel.GetHashCode();
return HashCode.Combine(this.boxedHighPrecisionPixel.ToScaledVector4(), this.isAssociated);
}
/// <summary>

74
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -5,9 +5,11 @@ using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@ -658,38 +660,10 @@ internal static class ColorProfileConverterExtensionsIcc
private static void ClipNegative(Span<Vector4> source)
{
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported && Vector<float>.Count >= source.Length * 4)
{
// SIMD loop
int i = 0;
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
{
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
v = Vector.Max(v, Vector<float>.Zero);
// Write the vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref source[i]), v);
}
// Scalar fallback for remaining elements
for (; i < source.Length; i++)
{
ref Vector4 s = ref source[i];
s = Vector4.Max(s, Vector4.Zero);
}
}
else
{
// Scalar fallback if SIMD is not supported
for (int i = 0; i < source.Length; i++)
{
ref Vector4 s = ref source[i];
s = Vector4.Max(s, Vector4.Zero);
}
}
// Vector4 values are contiguous floats, so flattening preserves the component order
// while allowing one shared tensor traversal to process every channel and SIMD tail.
Span<float> values = MemoryMarshal.Cast<Vector4, float>(source);
TensorPrimitives_.Max(values, 0F, values);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -708,39 +682,9 @@ internal static class ColorProfileConverterExtensionsIcc
private static void LabToLab(Span<Vector4> source, Span<Vector4> destination, [ConstantExpected] float scale)
{
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported)
{
Vector<float> vScale = new(scale);
int i = 0;
// SIMD loop
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
{
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
// Scale the vector
v *= vScale;
// Write the scaled vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v);
}
// Scalar fallback for remaining elements
for (; i < source.Length; i++)
{
destination[i] = source[i] * scale;
}
}
else
{
// Scalar fallback if SIMD is not supported
for (int i = 0; i < source.Length; i++)
{
destination[i] = source[i] * scale;
}
}
// Reinterpreting both spans exposes all four components to one multiplication traversal;
// the source and destination retain their original Vector4 boundaries after the operation.
TensorPrimitives_.Multiply(MemoryMarshal.Cast<Vector4, float>(source), scale, MemoryMarshal.Cast<Vector4, float>(destination));
}
private class ConversionParams

17
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs

@ -5,14 +5,19 @@ namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
internal partial class LutABCalculator
{
/// <summary>
/// Identifies the transform direction for the configured LUT calculator.
/// </summary>
private enum CalculationType
{
AtoB = 1 << 3,
BtoA = 1 << 4,
/// <summary>
/// Converts from device space to PCS using ICC <c>mAB</c> stage order.
/// </summary>
AtoB,
SingleCurve = 1,
CurveMatrix = 2,
CurveClut = 3,
Full = 4,
/// <summary>
/// Converts from PCS to device space using ICC <c>mBA</c> stage order.
/// </summary>
BtoA,
}
}

141
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs

@ -17,67 +17,106 @@ internal partial class LutABCalculator : IVector4Calculator
private MatrixCalculator matrixCalculator;
private ClutCalculator clutCalculator;
/// <summary>
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mAB</c> transform.
/// </summary>
/// <param name="entry">The parsed A-to-B LUT entry.</param>
public LutABCalculator(IccLutAToBTagDataEntry entry)
{
Guard.NotNull(entry, nameof(entry));
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
this.type |= CalculationType.AtoB;
this.type = CalculationType.AtoB;
}
/// <summary>
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mBA</c> transform.
/// </summary>
/// <param name="entry">The parsed B-to-A LUT entry.</param>
public LutABCalculator(IccLutBToATagDataEntry entry)
{
Guard.NotNull(entry, nameof(entry));
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
this.type |= CalculationType.BtoA;
this.type = CalculationType.BtoA;
}
/// <summary>
/// Calculates the transformed value by applying the configured ICC LUT stages in specification order.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>The transformed value.</returns>
public Vector4 Calculate(Vector4 value)
{
switch (this.type)
{
case CalculationType.Full | CalculationType.AtoB:
value = this.curveACalculator.Calculate(value);
value = this.clutCalculator.Calculate(value);
value = this.curveMCalculator.Calculate(value);
value = this.matrixCalculator.Calculate(value);
return this.curveBCalculator.Calculate(value);
case CalculationType.Full | CalculationType.BtoA:
value = this.curveBCalculator.Calculate(value);
value = this.matrixCalculator.Calculate(value);
value = this.curveMCalculator.Calculate(value);
value = this.clutCalculator.Calculate(value);
return this.curveACalculator.Calculate(value);
case CalculationType.CurveClut | CalculationType.AtoB:
value = this.curveACalculator.Calculate(value);
value = this.clutCalculator.Calculate(value);
return this.curveBCalculator.Calculate(value);
case CalculationType.CurveClut | CalculationType.BtoA:
value = this.curveBCalculator.Calculate(value);
value = this.clutCalculator.Calculate(value);
return this.curveACalculator.Calculate(value);
case CalculationType.CurveMatrix | CalculationType.AtoB:
value = this.curveMCalculator.Calculate(value);
value = this.matrixCalculator.Calculate(value);
return this.curveBCalculator.Calculate(value);
case CalculationType.CurveMatrix | CalculationType.BtoA:
value = this.curveBCalculator.Calculate(value);
value = this.matrixCalculator.Calculate(value);
return this.curveMCalculator.Calculate(value);
case CalculationType.SingleCurve | CalculationType.AtoB:
case CalculationType.SingleCurve | CalculationType.BtoA:
return this.curveBCalculator.Calculate(value);
case CalculationType.AtoB:
// ICC mAB order: A, CLUT, M, Matrix, B.
if (this.curveACalculator != null)
{
value = this.curveACalculator.Calculate(value);
}
if (this.clutCalculator != null)
{
value = this.clutCalculator.Calculate(value);
}
if (this.curveMCalculator != null)
{
value = this.curveMCalculator.Calculate(value);
}
if (this.matrixCalculator != null)
{
value = this.matrixCalculator.Calculate(value);
}
if (this.curveBCalculator != null)
{
value = this.curveBCalculator.Calculate(value);
}
return value;
case CalculationType.BtoA:
// ICC mBA order: B, Matrix, M, CLUT, A.
if (this.curveBCalculator != null)
{
value = this.curveBCalculator.Calculate(value);
}
if (this.matrixCalculator != null)
{
value = this.matrixCalculator.Calculate(value);
}
if (this.curveMCalculator != null)
{
value = this.curveMCalculator.Calculate(value);
}
if (this.clutCalculator != null)
{
value = this.clutCalculator.Calculate(value);
}
if (this.curveACalculator != null)
{
value = this.curveACalculator.Calculate(value);
}
return value;
default:
throw new InvalidOperationException("Invalid calculation type");
}
}
/// <summary>
/// Creates calculators for the processing stages present in the LUT entry.
/// </summary>
/// <remarks>
/// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
/// </remarks>
private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut)
{
bool hasACurve = curveA != null;
@ -86,26 +125,10 @@ internal partial class LutABCalculator : IVector4Calculator
bool hasMatrix = matrix3x1 != null && matrix3x3 != null;
bool hasClut = clut != null;
if (hasBCurve && hasMatrix && hasMCurve && hasClut && hasACurve)
{
this.type = CalculationType.Full;
}
else if (hasBCurve && hasClut && hasACurve)
{
this.type = CalculationType.CurveClut;
}
else if (hasBCurve && hasMatrix && hasMCurve)
{
this.type = CalculationType.CurveMatrix;
}
else if (hasBCurve)
{
this.type = CalculationType.SingleCurve;
}
else
{
throw new InvalidIccProfileException("AToB or BToA tag has an invalid configuration");
}
Guard.IsTrue(
hasACurve || hasBCurve || hasMCurve || hasMatrix || hasClut,
"entry",
"AToB or BToA tag must contain at least one processing element");
if (hasACurve)
{

2
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs

@ -60,7 +60,7 @@ internal abstract partial class IccConverterBase
IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16),
IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB),
IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA),
_ => throw new InvalidIccProfileException("Invalid entry."),
_ => throw new InvalidIccProfileException($"Invalid entry {tag}."),
};
private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag)

1
src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs

@ -62,6 +62,7 @@ public sealed class GammaWorkingSpace : RgbWorkingSpace
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(
typeof(GammaWorkingSpace),
this.WhitePoint,
this.ChromaticityCoordinates,
this.Gamma);

6
src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs

@ -70,8 +70,10 @@ public abstract class RgbWorkingSpace
return true;
}
if (obj is RgbWorkingSpace other)
if (obj.GetType() == this.GetType())
{
RgbWorkingSpace other = (RgbWorkingSpace)obj;
return this.WhitePoint.Equals(other.WhitePoint)
&& this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates);
}
@ -81,5 +83,5 @@ public abstract class RgbWorkingSpace
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.WhitePoint, this.ChromaticityCoordinates);
=> HashCode.Combine(this.GetType(), this.WhitePoint, this.ChromaticityCoordinates);
}

67
src/ImageSharp/Common/Helpers/ColorNumerics.cs

@ -26,7 +26,7 @@ internal static class ColorNumerics
/// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images).
/// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBT709Luminance(ref Vector4 vector, int luminanceLevels)
public static int GetBT709Luminance(Vector4 vector, int luminanceLevels)
=> (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1));
/// <summary>
@ -97,7 +97,7 @@ internal static class ColorNumerics
/// Scales a value from a 16 bit <see cref="ushort"/> to an
/// 8 bit <see cref="byte"/> equivalent.
/// </summary>
/// <param name="component">The 8 bit component value.</param>
/// <param name="component">The 16 bit component value.</param>
/// <returns>The <see cref="byte"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte From16BitTo8Bit(ushort component) =>
@ -132,6 +132,49 @@ internal static class ColorNumerics
// (V * 255 + 32895) >> 16
(byte)(((component * 255) + 32895) >> 16);
/// <summary>
/// Scales a value from a 32 bit <see cref="uint"/> to an
/// 8 bit <see cref="byte"/> equivalent.
/// </summary>
/// <param name="component">The 32 bit component value.</param>
/// <returns>The <see cref="byte"/> value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte From32BitTo8Bit(uint component) =>
// To scale to 8 bits from a 32-bit value V the required value is:
//
// (V * 255) / 4294967295
//
// Since:
//
// 4294967295 = 255 * 16843009
//
// this reduces exactly to:
//
// V / 16843009
//
// To round to nearest using integer arithmetic we add half the divisor
// before dividing:
//
// (V + 16843009 / 2) / 16843009
//
// where:
//
// 16843009 / 2 = 8421504.5
//
// Using 8421504 ensures correct round-to-nearest behaviour:
//
// 8421504 -> 0
// 8421505 -> 1
//
// The addition must be performed in 64-bit to avoid overflow for large
// input values (for example uint.MaxValue).
//
// Final exact integer implementation:
//
// ((ulong)V + 8421504) / 16843009
(byte)((component + 8421504UL) / 16843009UL);
/// <summary>
/// Scales a value from an 8 bit <see cref="byte"/> to
/// an 16 bit <see cref="ushort"/> equivalent.
@ -142,6 +185,26 @@ internal static class ColorNumerics
public static ushort From8BitTo16Bit(byte component)
=> (ushort)(component * 257);
/// <summary>
/// Scales a value from an 16 bit <see cref="byte"/> to
/// an 16 bit <see cref="uint"/> equivalent.
/// </summary>
/// <param name="component">The 16 bit component value.</param>
/// <returns>The 32 bit <see cref="uint"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint From16BitTo32Bit(ushort component)
=> (uint)(component * 65537);
/// <summary>
/// Scales a value from an 8 bit <see cref="byte"/> to
/// an 32 bit <see cref="ushort"/> equivalent.
/// </summary>
/// <param name="component">The 8 bit component value.</param>
/// <returns>The 32 bit <see cref="uint"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint From8BitTo32Bit(byte component)
=> (uint)(component * 16843009);
/// <summary>
/// Returns how many bits are required to store the specified number of colors.
/// Performs a Log2() on the value.

404
src/ImageSharp/Common/Helpers/Numerics.cs

@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
@ -328,22 +329,7 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<byte> span, byte min, byte max)
{
Span<byte> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref byte remainderStart = ref MemoryMarshal.GetReference(remainder);
ref byte remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
=> TensorPrimitives_.Clamp(span, min, max, span);
/// <summary>
/// Clamps the span values to the inclusive range of min and max.
@ -353,22 +339,7 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<uint> span, uint min, uint max)
{
Span<uint> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref uint remainderStart = ref MemoryMarshal.GetReference(remainder);
ref uint remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
=> TensorPrimitives_.Clamp(span, min, max, span);
/// <summary>
/// Clamps the span values to the inclusive range of min and max.
@ -378,22 +349,7 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<int> span, int min, int max)
{
Span<int> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref int remainderStart = ref MemoryMarshal.GetReference(remainder);
ref int remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
=> TensorPrimitives_.Clamp(span, min, max, span);
/// <summary>
/// Clamps the span values to the inclusive range of min and max.
@ -403,22 +359,7 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<float> span, float min, float max)
{
Span<float> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref float remainderStart = ref MemoryMarshal.GetReference(remainder);
ref float remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
=> TensorPrimitives_.Clamp(span, min, max, span);
/// <summary>
/// Clamps the span values to the inclusive range of min and max.
@ -428,92 +369,12 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<double> span, double min, double max)
{
Span<double> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref double remainderStart = ref MemoryMarshal.GetReference(remainder);
ref double remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ClampReduce<T>(Span<T> span, T min, T max)
where T : unmanaged
{
if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count)
{
int remainder = ModuloP2(span.Length, Vector<T>.Count);
int adjustedCount = span.Length - remainder;
if (adjustedCount > 0)
{
ClampImpl(span[..adjustedCount], min, max);
}
return adjustedCount;
}
return 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClampImpl<T>(Span<T> span, T min, T max)
where T : unmanaged
{
ref T sRef = ref MemoryMarshal.GetReference(span);
Vector<T> vmin = new(min);
Vector<T> vmax = new(max);
nint n = (nint)(uint)span.Length / Vector<T>.Count;
nint m = Modulo4(n);
nint u = n - m;
ref Vector<T> vs0 = ref Unsafe.As<T, Vector<T>>(ref MemoryMarshal.GetReference(span));
ref Vector<T> vs1 = ref Unsafe.Add(ref vs0, 1);
ref Vector<T> vs2 = ref Unsafe.Add(ref vs0, 2);
ref Vector<T> vs3 = ref Unsafe.Add(ref vs0, 3);
ref Vector<T> vsEnd = ref Unsafe.Add(ref vs0, u);
while (Unsafe.IsAddressLessThan(ref vs0, ref vsEnd))
{
vs0 = Vector.Min(Vector.Max(vmin, vs0), vmax);
vs1 = Vector.Min(Vector.Max(vmin, vs1), vmax);
vs2 = Vector.Min(Vector.Max(vmin, vs2), vmax);
vs3 = Vector.Min(Vector.Max(vmin, vs3), vmax);
vs0 = ref Unsafe.Add(ref vs0, 4);
vs1 = ref Unsafe.Add(ref vs1, 4);
vs2 = ref Unsafe.Add(ref vs2, 4);
vs3 = ref Unsafe.Add(ref vs3, 4);
}
if (m > 0)
{
vs0 = ref vsEnd;
vsEnd = ref Unsafe.Add(ref vsEnd, m);
while (Unsafe.IsAddressLessThan(ref vs0, ref vsEnd))
{
vs0 = Vector.Min(Vector.Max(vmin, vs0), vmax);
vs0 = ref Unsafe.Add(ref vs0, 1);
}
}
}
=> TensorPrimitives_.Clamp(span, min, max, span);
/// <summary>
/// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact.
/// </summary>
/// <param name="source">The <see cref="Vector4"/> to premultiply</param>
/// <param name="source">The <see cref="Vector4"/> to premultiply.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Premultiply(ref Vector4 source)
{
@ -524,50 +385,98 @@ internal static class Numerics
}
/// <summary>
/// Bulk variant of <see cref="Premultiply(ref Vector4)"/>
/// Clamps associated color components to the alpha component while preserving alpha.
/// </summary>
/// <param name="vectors">The span of vectors</param>
/// <param name="source">The associated vector to clamp.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ClampRgbToAlpha(ref Vector4 source)
{
Vector4 alpha = PermuteW(source);
source = WithW(Vector4.Min(Vector4.Max(source, Vector4.Zero), alpha), alpha);
}
/// <summary>
/// Premultiplies the X, Y, and Z components of each vector by its W component while preserving W.
/// </summary>
/// <param name="vectors">The vectors to premultiply.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Premultiply(Span<Vector4> vectors)
{
if (Avx.IsSupported && vectors.Length >= 2)
if (Vector512.IsHardwareAccelerated)
{
int vectorsPerVector = Vector512<float>.Count / Vector128<float>.Count;
ref Vector512<float> vectorsBase = ref Unsafe.As<Vector4, Vector512<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector512<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
Vector128<float> alphaMask128 = Vector128.Create(0, 0, 0, -1).AsSingle();
Vector256<float> alphaMask256 = Vector256.Create(alphaMask128, alphaMask128);
Vector512<float> alphaMask = Vector512.Create(alphaMask256, alphaMask256);
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Vector512<float> source = vectorsBase;
Vector512<float> alpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl);
// Multiplication also squares W, so select the original W lanes to preserve alpha bit-for-bit.
vectorsBase = Vector512.ConditionalSelect(alphaMask, source, source * alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
vectors = vectors[(vectors.Length - (vectors.Length % vectorsPerVector))..];
}
if (Vector256.IsHardwareAccelerated)
{
// Divide by 2 as 4 elements per Vector4 and 8 per Vector256<float>
int vectorsPerVector = Vector256<float>.Count / Vector128<float>.Count;
ref Vector256<float> vectorsBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector256<float> vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
ref Vector256<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
Vector128<float> alphaMask128 = Vector128.Create(0, 0, 0, -1).AsSingle();
Vector256<float> alphaMask = Vector256.Create(alphaMask128, alphaMask128);
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Vector256<float> source = vectorsBase;
Vector256<float> alpha = Avx.Permute(source, ShuffleAlphaControl);
vectorsBase = Avx.Blend(Avx.Multiply(source, alpha), source, BlendAlphaControl);
Vector256<float> alpha = Vector256_.ShuffleNative(source, ShuffleAlphaControl);
// Multiplication also squares W, so select the original W lanes to preserve alpha bit-for-bit.
vectorsBase = Vector256.ConditionalSelect(alphaMask, source, source * alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
if (Modulo2(vectors.Length) != 0)
{
// Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
Premultiply(ref MemoryMarshal.GetReference(vectors[^1..]));
}
vectors = vectors[(vectors.Length - (vectors.Length % vectorsPerVector))..];
}
else
if (Vector128.IsHardwareAccelerated)
{
ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors);
ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsStart, (uint)vectors.Length);
ref Vector128<float> vectorsBase = ref Unsafe.As<Vector4, Vector128<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector128<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length);
Vector128<float> alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
while (Unsafe.IsAddressLessThan(ref vectorsStart, ref vectorsEnd))
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Premultiply(ref vectorsStart);
Vector128<float> source = vectorsBase;
Vector128<float> alpha = Vector128_.ShuffleNative(source, ShuffleAlphaControl);
vectorsStart = ref Unsafe.Add(ref vectorsStart, 1);
// Multiplication also squares W, so select the original W lane to preserve alpha bit-for-bit.
vectorsBase = Vector128.ConditionalSelect(alphaMask, source, source * alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
return;
}
ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors);
for (nuint i = 0; i < (uint)vectors.Length; i++)
{
Premultiply(ref Unsafe.Add(ref vectorsStart, i));
}
}
/// <summary>
/// Reverses the result of premultiplying a vector via <see cref="Premultiply(ref Vector4)"/>.
/// When alpha is zero, the RGB components remain unchanged because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The <see cref="Vector4"/> to premultiply</param>
/// <param name="source">The <see cref="Vector4"/> to unpremultiply.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnPremultiply(ref Vector4 source)
{
@ -575,74 +484,147 @@ internal static class Numerics
UnPremultiply(ref source, alpha);
}
/// <summary>
/// Unpremultiplies the X, Y, and Z components of a vector by the supplied alpha while preserving W.
/// When alpha is zero, the RGB components remain unchanged because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The vector to unpremultiply.</param>
/// <param name="alpha">The source alpha replicated to every component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnPremultiply(ref Vector4 source, Vector4 alpha)
{
// Zero alpha has no mathematical inverse, so preserve stored additive or hidden RGB data unchanged.
if (alpha == Vector4.Zero)
{
return;
}
// Divide source by alpha if alpha is nonzero, otherwise set all components to match the source value
// Blend the result with the alpha vector to ensure that the alpha component is unchanged
// Division would replace W with one, so restore the original alpha component exactly.
source = WithW(source / alpha, alpha);
}
/// <summary>
/// Bulk variant of <see cref="UnPremultiply(ref Vector4)"/>
/// Unpremultiplies the X, Y, and Z components of each vector by its W component while preserving W.
/// Vectors with zero W retain their RGB components because no unassociated value can be recovered.
/// </summary>
/// <param name="vectors">The span of vectors</param>
/// <param name="vectors">The vectors to unpremultiply.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnPremultiply(Span<Vector4> vectors)
{
if (Avx.IsSupported && vectors.Length >= 2)
if (Vector512.IsHardwareAccelerated)
{
// Divide by 2 as 4 elements per Vector4 and 8 per Vector256<float>
ref Vector256<float> vectorsBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector256<float> vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
Vector256<float> epsilon = Vector256.Create(Constants.Epsilon);
int vectorsPerVector = Vector512<float>.Count / Vector128<float>.Count;
ref Vector512<float> vectorsBase = ref Unsafe.As<Vector4, Vector512<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector512<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Vector256<float> source = vectorsBase;
Vector256<float> alpha = Avx.Permute(source, ShuffleAlphaControl);
Vector512<float> source = vectorsBase;
Vector512<float> alpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl);
vectorsBase = UnPremultiply(source, alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
if (Modulo2(vectors.Length) != 0)
vectors = vectors[(vectors.Length - (vectors.Length % vectorsPerVector))..];
}
if (Vector256.IsHardwareAccelerated)
{
int vectorsPerVector = Vector256<float>.Count / Vector128<float>.Count;
ref Vector256<float> vectorsBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector256<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
// Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
UnPremultiply(ref MemoryMarshal.GetReference(vectors[^1..]));
Vector256<float> source = vectorsBase;
Vector256<float> alpha = Vector256_.ShuffleNative(source, ShuffleAlphaControl);
vectorsBase = UnPremultiply(source, alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
vectors = vectors[(vectors.Length - (vectors.Length % vectorsPerVector))..];
}
else
if (Vector128.IsHardwareAccelerated)
{
ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors);
ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsStart, (uint)vectors.Length);
ref Vector128<float> vectorsBase = ref Unsafe.As<Vector4, Vector128<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector128<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length);
while (Unsafe.IsAddressLessThan(ref vectorsStart, ref vectorsEnd))
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
UnPremultiply(ref vectorsStart);
vectorsStart = ref Unsafe.Add(ref vectorsStart, 1);
Vector128<float> source = vectorsBase;
Vector128<float> alpha = Vector128_.ShuffleNative(source, ShuffleAlphaControl);
vectorsBase = UnPremultiply(source, alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
return;
}
ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors);
for (nuint i = 0; i < (uint)vectors.Length; i++)
{
UnPremultiply(ref Unsafe.Add(ref vectorsStart, i));
}
}
/// <summary>
/// Unpremultiplies the RGB lanes of a vector while preserving its alpha lane.
/// When alpha is zero, the RGB lanes remain unchanged because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The associated vector.</param>
/// <param name="alpha">The source alpha replicated to every lane.</param>
/// <returns>The unassociated vector.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> UnPremultiply(Vector128<float> source, Vector128<float> alpha)
{
// Zero alpha has no mathematical inverse, so select the source lanes to preserve stored additive or hidden RGB data.
Vector128<float> zeroMask = Vector128.Equals(alpha, Vector128<float>.Zero);
Vector128<float> result = Vector128.ConditionalSelect(zeroMask, source, source / alpha);
// Division would replace W with one, so restore the original alpha lane exactly.
Vector128<float> alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
return Vector128.ConditionalSelect(alphaMask, alpha, result);
}
/// <summary>
/// Unpremultiplies the RGB lanes of two vectors while preserving their alpha lanes.
/// Vectors with zero alpha retain their RGB lanes because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <param name="alpha">Each source alpha replicated across its four lanes.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> UnPremultiply(Vector256<float> source, Vector256<float> alpha)
{
// Check if alpha is zero to avoid division by zero
// Zero alpha has no mathematical inverse, so select the source lanes to preserve stored additive or hidden RGB data.
Vector256<float> zeroMask = Avx.CompareEqual(alpha, Vector256<float>.Zero);
// Divide source by alpha if alpha is nonzero, otherwise set all components to match the source value
Vector256<float> result = Avx.BlendVariable(Avx.Divide(source, alpha), source, zeroMask);
// Blend the result with the alpha vector to ensure that the alpha component is unchanged
// Division would replace W with one, so restore both original alpha lanes exactly.
return Avx.Blend(result, alpha, BlendAlphaControl);
}
/// <summary>
/// Unpremultiplies the RGB lanes of four vectors while preserving their alpha lanes.
/// Vectors with zero alpha retain their RGB lanes because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <param name="alpha">Each source alpha replicated across its four lanes.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> UnPremultiply(Vector512<float> source, Vector512<float> alpha)
{
// Zero alpha has no mathematical inverse, so select the source lanes to preserve stored additive or hidden RGB data.
Vector512<float> zeroMask = Vector512.Equals(alpha, Vector512<float>.Zero);
Vector512<float> result = Vector512.ConditionalSelect(zeroMask, source, source / alpha);
// Division would replace W with one, so restore all four original alpha lanes exactly.
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
return Vector512.ConditionalSelect(alphaMask, alpha, result);
}
/// <summary>
/// Permutes the given vector return a new instance with all the values set to <see cref="Vector4.W"/>.
/// </summary>
@ -690,7 +672,7 @@ internal static class Numerics
/// </summary>
/// <param name="vectors">The span of vectors</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void CubePowOnXYZ(Span<Vector4> vectors)
public static void CubePowOnXYZ(Span<Vector4> vectors)
{
ref Vector4 baseRef = ref MemoryMarshal.GetReference(vectors);
ref Vector4 endRef = ref Unsafe.Add(ref baseRef, (uint)vectors.Length);
@ -1088,39 +1070,5 @@ internal static class Numerics
/// <param name="sum">The sum of the values in <paramref name="span"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Normalize(Span<float> span, float sum)
{
if (Vector256.IsHardwareAccelerated)
{
ref float startRef = ref MemoryMarshal.GetReference(span);
ref float endRef = ref Unsafe.Add(ref startRef, span.Length & ~7);
Vector256<float> sum256 = Vector256.Create(sum);
while (Unsafe.IsAddressLessThan(ref startRef, ref endRef))
{
Unsafe.As<float, Vector256<float>>(ref startRef) /= sum256;
startRef = ref Unsafe.Add(ref startRef, (nuint)8);
}
if ((span.Length & 7) >= 4)
{
Unsafe.As<float, Vector128<float>>(ref startRef) /= sum256.GetLower();
startRef = ref Unsafe.Add(ref startRef, (nuint)4);
}
endRef = ref Unsafe.Add(ref startRef, span.Length & 3);
while (Unsafe.IsAddressLessThan(ref startRef, ref endRef))
{
startRef /= sum;
startRef = ref Unsafe.Add(ref startRef, (nuint)1);
}
}
else
{
for (int i = 0; i < span.Length; i++)
{
span[i] /= sum;
}
}
}
=> TensorPrimitives_.Divide(span, sum, span);
}

32
src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs

@ -1,36 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// The JIT can detect and optimize rotation idioms ROTL (Rotate Left)
// and ROTR (Rotate Right) emitting efficient CPU instructions:
// https://github.com/dotnet/coreclr/pull/1830
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp;
/// <summary>
/// Defines the contract for methods that allow the shuffling of pixel components.
/// Used for shuffling on platforms that do not support Hardware Intrinsics.
/// Defines a stateless operation over packed pixel components.
/// </summary>
internal interface IComponentShuffle
{
/// <summary>
/// Shuffles then slices 8-bit integers in <paramref name="source"/>
/// using a byte control and store the results in <paramref name="destination"/>.
/// If successful, this method will reduce the length of <paramref name="source"/> length
/// by the shuffle amount.
/// Reorders one packed pixel.
/// </summary>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination);
/// <param name="source">The source components, with the first component in the least-significant byte.</param>
/// <returns>The reordered packed components.</returns>
public static abstract uint Invoke(uint source);
/// <summary>
/// Shuffle 8-bit integers in <paramref name="source"/>
/// using the control and store the results in <paramref name="destination"/>.
/// Reorders the packed pixels in a 128-bit vector.
/// </summary>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <remarks>
/// Implementation can assume that source.Length is less or equal than destination.Length.
/// Loops should iterate using source.Length.
/// </remarks>
void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination);
/// <param name="source">The source pixels.</param>
/// <returns>The reordered pixels.</returns>
public static abstract Vector128<byte> Invoke(Vector128<byte> source);
}

136
src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs

@ -1,85 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.SimdUtils;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
/// <inheritdoc/>
/// <summary>
/// Defines a stateless operation that reorders a three-component pixel after adding opaque alpha.
/// </summary>
internal interface IPad3Shuffle4 : IComponentShuffle
{
}
internal readonly struct DefaultPad3Shuffle4([ConstantExpected] byte control) : IPad3Shuffle4
/// <summary>
/// Preserves XYZ order and appends opaque W.
/// </summary>
internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4
{
public byte Control { get; } = control;
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
#pragma warning disable CA1857 // A constant is expected for the parameter
=> HwIntrinsics.Pad3Shuffle4Reduce(ref source, ref destination, this.Control);
#pragma warning restore CA1857 // A constant is expected for the parameter
public static uint Invoke(uint source) => source;
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source) => source;
}
/// <summary>
/// Reorders padded XYZW components to WXYZ.
/// </summary>
internal readonly struct WXYZPad3Shuffle4 : IPad3Shuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out uint p3, out uint p2, out uint p1, out uint p0);
Span<byte> temp = stackalloc byte[4];
ref byte t = ref MemoryMarshal.GetReference(temp);
ref uint tu = ref Unsafe.As<byte, uint>(ref t);
for (nuint i = 0, j = 0; i < (uint)source.Length; i += 3, j += 4)
{
ref byte s = ref Unsafe.Add(ref sBase, i);
tu = Unsafe.As<byte, uint>(ref s) | 0xFF000000;
Unsafe.Add(ref dBase, j + 0) = Unsafe.Add(ref t, p0);
Unsafe.Add(ref dBase, j + 1) = Unsafe.Add(ref t, p1);
Unsafe.Add(ref dBase, j + 2) = Unsafe.Add(ref t, p2);
Unsafe.Add(ref dBase, j + 3) = Unsafe.Add(ref t, p3);
}
}
public static uint Invoke(uint source)
// The scalar pipeline has already appended opaque W, so the four-component
// WXYZ operator performs the complete remaining permutation.
=> WXYZShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel with opaque W. Selecting [3, 0, 1, 2]
// produces WXYZ, and offsets 4, 8, and 12 repeat that rotation for the next pixels.
=> Vector128_.ShuffleNative(source, Vector128.Create((byte)3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14));
}
internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4
/// <summary>
/// Reorders padded XYZW components to WZYX.
/// </summary>
internal readonly struct WZYXPad3Shuffle4 : IPad3Shuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Pad3Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3210);
public static uint Invoke(uint source)
// The scalar pipeline has already appended opaque W, so the four-component
// WZYX operator performs the complete remaining permutation.
=> WZYXShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel with opaque W. Selecting [3, 2, 1, 0]
// produces WZYX, and offsets 4, 8, and 12 repeat that reversal for the next pixels.
=> Vector128_.ShuffleNative(source, Vector128.Create((byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12));
}
/// <summary>
/// Reorders padded XYZW components to ZYXW.
/// </summary>
internal readonly struct ZYXWPad3Shuffle4 : IPad3Shuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
ref byte sEnd = ref Unsafe.Add(ref sBase, (uint)source.Length);
ref byte sLoopEnd = ref Unsafe.Subtract(ref sEnd, 4);
while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd))
{
Unsafe.As<byte, uint>(ref dBase) = Unsafe.As<byte, uint>(ref sBase) | 0xFF000000;
sBase = ref Unsafe.Add(ref sBase, 3);
dBase = ref Unsafe.Add(ref dBase, 4);
}
while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.Add(ref sBase, 0);
Unsafe.Add(ref dBase, 1) = Unsafe.Add(ref sBase, 1);
Unsafe.Add(ref dBase, 2) = Unsafe.Add(ref sBase, 2);
Unsafe.Add(ref dBase, 3) = byte.MaxValue;
sBase = ref Unsafe.Add(ref sBase, 3);
dBase = ref Unsafe.Add(ref dBase, 4);
}
}
public static uint Invoke(uint source)
// The scalar pipeline has already appended opaque W, so the four-component
// ZYXW operator performs the complete remaining permutation.
=> ZYXWShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel with opaque W. Selecting [2, 1, 0, 3]
// exchanges X and Z to produce ZYXW, with offsets 4, 8, and 12 covering the next pixels.
=> Vector128_.ShuffleNative(source, Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15));
}

45
src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs

@ -1,41 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.SimdUtils;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
/// <inheritdoc/>
/// <summary>
/// Identifies a stateless three-component shuffle operator.
/// </summary>
internal interface IShuffle3 : IComponentShuffle
{
}
internal readonly struct DefaultShuffle3([ConstantExpected] byte control) : IShuffle3
/// <summary>
/// Reorders XYZ components to ZYX.
/// </summary>
internal readonly struct ZYXShuffle3 : IShuffle3
{
public byte Control { get; } = control;
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
#pragma warning disable CA1857 // A constant is expected for the parameter
=> HwIntrinsics.Shuffle3Reduce(ref source, ref destination, this.Control);
#pragma warning restore CA1857 // A constant is expected for the parameter
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
// The scalar tail is staged as XYZW with an unused W byte. Reusing the four-component
// ZYXW operator produces ZYX in the low three bytes consumed by the caller.
=> ZYXWShuffle4.Invoke(source);
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out _, out uint p2, out uint p1, out uint p0);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
for (nuint i = 0; i < (uint)source.Length; i += 3)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
}
}
// Each four-byte group is a temporary XYZW pixel created by the shuffle pipeline.
// Selecting [2, 1, 0, 3] produces ZYXW, and offsets 4, 8, and 12 repeat that
// permutation for the next pixels. The pipeline subsequently discards every W byte.
=> Vector128_.ShuffleNative(source, Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15));
}

354
src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs

@ -2,177 +2,287 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.SimdUtils;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
/// <inheritdoc/>
/// <summary>
/// Defines a stateless operation over one packed four-component pixel.
/// </summary>
internal interface IShuffle4 : IComponentShuffle
{
}
internal readonly struct DefaultShuffle4([ConstantExpected] byte control) : IShuffle4
{
public byte Control { get; } = control;
/// <summary>
/// Reorders the packed pixels in a 256-bit vector.
/// </summary>
/// <param name="source">The source pixels.</param>
/// <returns>The reordered pixels.</returns>
public static abstract Vector256<byte> Invoke(Vector256<byte> source);
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
#pragma warning disable CA1857 // A constant is expected for the parameter
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, this.Control);
#pragma warning restore CA1857 // A constant is expected for the parameter
/// <summary>
/// Reorders the packed pixels in a 512-bit vector.
/// </summary>
/// <param name="source">The source pixels.</param>
/// <returns>The reordered pixels.</returns>
public static abstract Vector512<byte> Invoke(Vector512<byte> source);
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
/// <summary>
/// Expands one 128-bit lane mask into absolute indices for a 512-bit shuffle.
/// </summary>
/// <param name="laneMask">The indices, from zero through fifteen, for one 128-bit lane.</param>
/// <returns>The corresponding absolute indices for all four 128-bit lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> ExpandLaneMask(Vector128<byte> laneMask)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out uint p3, out uint p2, out uint p1, out uint p0);
for (nuint i = 0; i < (uint)source.Length; i += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
Unsafe.Add(ref dBase, i + 3) = Unsafe.Add(ref sBase, p3 + i);
}
// A 512-bit vector contains four 128-bit lanes, and each lane contains four packed
// XYZW pixels. The supplied mask addresses bytes 0..15 in the first lane. The managed
// Vector512.Shuffle fallback addresses the complete 64-byte vector, so the same
// permutation must address bytes 16..31, 32..47, and 48..63 in the remaining lanes.
//
// AVX-512BW VPSHUFB instead interprets indices independently within each 128-bit lane
// and uses only the low four bits to select a byte. Adding the lane offsets therefore
// satisfies the managed absolute-index contract without changing the native lane-local
// permutation.
Vector128<byte> lane1 = laneMask + Vector128.Create((byte)16);
Vector128<byte> lane2 = laneMask + Vector128.Create((byte)32);
Vector128<byte> lane3 = laneMask + Vector128.Create((byte)48);
return Vector512.Create(Vector256.Create(laneMask, lane1), Vector256.Create(lane2, lane3));
}
}
/// <summary>
/// Reorders XYZW components to WXYZ.
/// </summary>
internal readonly struct WXYZShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle2103);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// ROTL(8, source) = [Z Y X W]
=> BitOperations.RotateLeft(source, 8);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128_.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// ROTL(8, packed) = [Z Y X W]
Unsafe.Add(ref dBase, i) = (packed << 8) | (packed >> 24);
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that rotate each XYZW pixel to WXYZ within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [3, 0, 1, 2] produces
// WXYZ, and offsets 4, 8, and 12 repeat that permutation for the next pixels.
=> Vector128.Create((byte)3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14);
}
/// <summary>
/// Reorders XYZW components to WZYX.
/// </summary>
internal readonly struct WZYXShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle0123);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// REVERSE(source) = [X Y Z W]
=> BinaryPrimitives.ReverseEndianness(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128_.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// REVERSE(packedArgb) = [X Y Z W]
Unsafe.Add(ref dBase, i) = BinaryPrimitives.ReverseEndianness(packed);
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that reverse each XYZW pixel to WZYX within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [3, 2, 1, 0] produces
// WZYX, and offsets 4, 8, and 12 repeat that reversal for the next pixels.
=> Vector128.Create((byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12);
}
/// <summary>
/// Reorders XYZW components to YZWX.
/// </summary>
internal readonly struct YZWXShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle0321);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// ROTR(8, source) = [X W Z Y]
=> BitOperations.RotateRight(source, 8);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128_.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// ROTR(8, packedArgb) = [Y Z W X]
Unsafe.Add(ref dBase, i) = BitOperations.RotateRight(packed, 8);
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that rotate each XYZW pixel to YZWX within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [1, 2, 3, 0] produces
// YZWX, and offsets 4, 8, and 12 repeat that rotation for the next pixels.
=> Vector128.Create((byte)1, 2, 3, 0, 5, 6, 7, 4, 9, 10, 11, 8, 13, 14, 15, 12);
}
/// <summary>
/// Reorders XYZW components to ZYXW.
/// </summary>
internal readonly struct ZYXWShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3012);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// source & 0xFF00FF00 = [W 0 Y 0]
// ROTL(source & 0x00FF00FF) = [0 X 0 Z]
// combined = [W X Y Z]
=> (source & 0xFF00FF00) | BitOperations.RotateLeft(source & 0x00FF00FF, 16);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128_.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// tmp1 = [W 0 Y 0]
// tmp2 = [0 Z 0 X]
// tmp3=ROTL(16, tmp2) = [0 X 0 Z]
// tmp1 + tmp3 = [W X Y Z]
uint tmp1 = packed & 0xFF00FF00;
uint tmp2 = packed & 0x00FF00FF;
uint tmp3 = BitOperations.RotateLeft(tmp2, 16);
Unsafe.Add(ref dBase, i) = tmp1 + tmp3;
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that exchange X and Z in each XYZW pixel within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [2, 1, 0, 3] exchanges
// X and Z to produce ZYXW, with offsets 4, 8, and 12 covering the next pixels.
=> Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15);
}
/// <summary>
/// Reorders XYZW components to XWZY.
/// </summary>
internal readonly struct XWZYShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle1230);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// source & 0x00FF00FF = [0 Z 0 X]
// ROTL(source & 0xFF00FF00) = [Y 0 W 0]
// combined = [Y Z W X]
=> (source & 0x00FF00FF) | BitOperations.RotateLeft(source & 0xFF00FF00, 16);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128_.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// tmp1 = [0 Z 0 X]
// tmp2 = [W 0 Y 0]
// tmp3=ROTL(16, tmp2) = [Y 0 W 0]
// tmp1 + tmp3 = [Y Z W X]
uint tmp1 = packed & 0x00FF00FF;
uint tmp2 = packed & 0xFF00FF00;
uint tmp3 = BitOperations.RotateLeft(tmp2, 16);
Unsafe.Add(ref dBase, i) = tmp1 + tmp3;
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that exchange Y and W in each XYZW pixel within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [0, 3, 2, 1] exchanges
// Y and W to produce XWZY, with offsets 4, 8, and 12 covering the next pixels.
=> Vector128.Create((byte)0, 3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13);
}

137
src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs

@ -1,85 +1,106 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.SimdUtils;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
/// <inheritdoc/>
/// <summary>
/// Defines a stateless operation that reorders four packed components before retaining three.
/// </summary>
internal interface IShuffle4Slice3 : IComponentShuffle
{
}
internal readonly struct DefaultShuffle4Slice3([ConstantExpected] byte control) : IShuffle4Slice3
/// <summary>
/// Preserves XYZ order and discards W.
/// </summary>
internal readonly struct XYZWShuffle4Slice3 : IShuffle4Slice3
{
public byte Control { get; } = control;
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
#pragma warning disable CA1857 // A constant is expected for the parameter
=> HwIntrinsics.Shuffle4Slice3Reduce(ref source, ref destination, this.Control);
#pragma warning restore CA1857 // A constant is expected for the parameter
public static uint Invoke(uint source) => source;
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source) => source;
}
/// <summary>
/// Reorders XYZW components to YZW before discarding X.
/// </summary>
internal readonly struct YZWXShuffle4Slice3 : IShuffle4Slice3
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out _, out uint p2, out uint p1, out uint p0);
for (nuint i = 0, j = 0; i < (uint)destination.Length; i += 3, j += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + j);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + j);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + j);
}
}
public static uint Invoke(uint source)
// Reuse the four-component rotation; the caller stores only the low YZW
// bytes and therefore discards the rotated X byte.
=> YZWXShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel. Selecting [1, 2, 3, 0] produces
// YZWX, and offsets 4, 8, and 12 repeat that rotation for the next pixels.
// The surrounding pipeline subsequently removes every fourth byte.
=> Vector128_.ShuffleNative(source, Vector128.Create((byte)1, 2, 3, 0, 5, 6, 7, 4, 9, 10, 11, 8, 13, 14, 15, 12));
}
internal readonly struct XYZWShuffle4Slice3 : IShuffle4Slice3
/// <summary>
/// Reorders XYZW components to WZY before discarding X.
/// </summary>
internal readonly struct WZYXShuffle4Slice3 : IShuffle4Slice3
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Slice3Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3210);
public static uint Invoke(uint source)
// Reuse the four-component reversal; the caller stores only the low WZY
// bytes and therefore discards the reversed X byte.
=> WZYXShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel. Selecting [3, 2, 1, 0] produces
// WZYX, and offsets 4, 8, and 12 repeat that reversal for the next pixels.
// The surrounding pipeline subsequently removes every fourth byte.
=> Vector128_.ShuffleNative(source, Vector128.Create((byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12));
}
/// <summary>
/// Reorders XYZW components to ZYX before discarding W.
/// </summary>
internal readonly struct ZYXWShuffle4Slice3 : IShuffle4Slice3
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref Byte3 dBase = ref Unsafe.As<byte, Byte3>(ref MemoryMarshal.GetReference(destination));
nint n = (nint)(uint)source.Length / 4;
nint m = Numerics.Modulo4(n);
nint u = n - m;
ref uint sLoopEnd = ref Unsafe.Add(ref sBase, u);
ref uint sEnd = ref Unsafe.Add(ref sBase, n);
while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 0));
Unsafe.Add(ref dBase, 1) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 1));
Unsafe.Add(ref dBase, 2) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 2));
Unsafe.Add(ref dBase, 3) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 3));
sBase = ref Unsafe.Add(ref sBase, 4);
dBase = ref Unsafe.Add(ref dBase, 4);
}
while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 0));
sBase = ref Unsafe.Add(ref sBase, 1);
dBase = ref Unsafe.Add(ref dBase, 1);
}
}
public static uint Invoke(uint source)
// Reuse the four-component exchange; the caller stores only the low ZYX
// bytes and therefore discards the preserved W byte.
=> ZYXWShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel. Selecting [2, 1, 0, 3] produces
// ZYXW, and offsets 4, 8, and 12 repeat that exchange for the next pixels.
// The surrounding pipeline subsequently removes every fourth byte.
=> Vector128_.ShuffleNative(source, Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15));
}
/// <summary>
/// Represents one tightly packed three-byte value for scalar four-to-three component writes.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 3)]
internal readonly struct Byte3
{

31
src/ImageSharp/Common/Helpers/SimdUtils.Convert.cs

@ -41,11 +41,29 @@ internal static partial class SimdUtils
{
DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!");
HwIntrinsics.NormalizedFloatToByteSaturateReduce(ref source, ref destination);
HwIntrinsics.FloatToByteSaturateReduce(ref source, ref destination, byte.MaxValue);
if (source.Length > 0)
{
ConvertNormalizedFloatToByteRemainder(source, destination);
ConvertFloatToByteRemainder(source, destination, byte.MaxValue);
}
}
/// <summary>
/// Converts byte-magnitude floating-point values to bytes using saturating round-to-nearest with midpoint values away from zero.
/// </summary>
/// <param name="source">The source byte magnitudes.</param>
/// <param name="destination">The destination bytes.</param>
[MethodImpl(InliningOptions.ShortMethod)]
internal static void FloatToByteSaturate(ReadOnlySpan<float> source, Span<byte> destination)
{
DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!");
HwIntrinsics.FloatToByteSaturateReduce(ref source, ref destination, 1F);
if (source.Length > 0)
{
ConvertFloatToByteRemainder(source, destination, 1F);
}
}
@ -57,22 +75,23 @@ internal static partial class SimdUtils
for (int i = 0; i < source.Length; i++)
{
Unsafe.Add(ref dBase, (uint)i) = Unsafe.Add(ref sBase, (uint)i) / 255f;
// Match the SIMD conversion so one span cannot contain different float representations of the same byte value.
Unsafe.Add(ref dBase, (uint)i) = Unsafe.Add(ref sBase, (uint)i) / (float)byte.MaxValue;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ConvertNormalizedFloatToByteRemainder(ReadOnlySpan<float> source, Span<byte> destination)
private static void ConvertFloatToByteRemainder(ReadOnlySpan<float> source, Span<byte> destination, float scale)
{
ref float sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
for (int i = 0; i < source.Length; i++)
{
Unsafe.Add(ref dBase, (uint)i) = ConvertToByte(Unsafe.Add(ref sBase, (uint)i));
Unsafe.Add(ref dBase, (uint)i) = ConvertToByte(Unsafe.Add(ref sBase, (uint)i), scale);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ConvertToByte(float f) => (byte)Numerics.Clamp((f * 255f) + 0.5f, 0, 255f);
private static byte ConvertToByte(float value, float scale) => (byte)Numerics.Clamp((value * scale) + .5F, 0, byte.MaxValue);
}

172
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -601,51 +601,6 @@ internal static partial class SimdUtils
}
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector256{Single}"/>.
/// TODO: Fix. The arguments are in a different order to the FMA intrinsic.
/// </summary>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector256<float> MultiplyAdd(
Vector256<float> va,
Vector256<float> vm0,
Vector256<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(vm1, vm0, va);
}
return va + (vm0 * vm1);
}
/// <summary>
/// Performs a multiplication and a negated addition of the <see cref="Vector256{Single}"/>.
/// </summary>
/// <remarks>ret = c - (a * b)</remarks>
/// <param name="a">The first vector to multiply.</param>
/// <param name="b">The second vector to multiply.</param>
/// <param name="c">The vector to add negated to the intermediate result.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Vector256<float> MultiplyAddNegated(
Vector256<float> a,
Vector256<float> b,
Vector256<float> c)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAddNegated(a, b, c);
}
return Avx.Subtract(c, Avx.Multiply(a, b));
}
/// <summary>
/// Blend packed 8-bit integers from <paramref name="left"/> and <paramref name="right"/> using <paramref name="mask"/>.
/// The high bit of each corresponding <paramref name="mask"/> byte determines the selection.
@ -752,10 +707,14 @@ internal static partial class SimdUtils
/// Implementation is based on MagicScaler code:
/// https://github.com/saucecontrol/PhotoSauce/blob/b5811908041200488aa18fdfd17df5fc457415dc/src/MagicScaler/Magic/Processors/ConvertersFloat.cs#L80-L182
/// </remarks>
internal static unsafe void ByteToNormalizedFloat(
internal static void ByteToNormalizedFloat(
ReadOnlySpan<byte> source,
Span<float> destination)
{
const double reciprocal = 1D / byte.MaxValue;
const float reciprocalHigh = (float)reciprocal;
const float reciprocalLow = (float)(reciprocal - reciprocalHigh);
if (Vector512.IsHardwareAccelerated && Avx512F.IsSupported)
{
DebugVerifySpanInput(source, destination, Vector512<byte>.Count);
@ -764,6 +723,8 @@ internal static partial class SimdUtils
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref Vector512<float> destinationBase = ref Unsafe.As<float, Vector512<float>>(ref MemoryMarshal.GetReference(destination));
Vector512<float> high = Vector512.Create(reciprocalHigh);
Vector512<float> low = Vector512.Create(reciprocalLow);
for (nuint i = 0; i < n; i++)
{
@ -773,11 +734,16 @@ internal static partial class SimdUtils
Vector512<int> i2 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector512<int>.Count * 2)));
Vector512<int> i3 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector512<int>.Count * 3)));
// Declare multiplier on each line. Codegen is better.
Vector512<float> f0 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i0);
Vector512<float> f1 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i1);
Vector512<float> f2 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i2);
Vector512<float> f3 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i3);
Vector512<float> f0 = Avx512F.ConvertToVector512Single(i0);
Vector512<float> f1 = Avx512F.ConvertToVector512Single(i1);
Vector512<float> f2 = Avx512F.ConvertToVector512Single(i2);
Vector512<float> f3 = Avx512F.ConvertToVector512Single(i3);
// The residual term restores the correctly rounded byte / 255F result without paying for vector division.
f0 = Vector512_.FusedMultiplyAdd(f0, high, f0 * low);
f1 = Vector512_.FusedMultiplyAdd(f1, high, f1 * low);
f2 = Vector512_.FusedMultiplyAdd(f2, high, f2 * low);
f3 = Vector512_.FusedMultiplyAdd(f3, high, f3 * low);
ref Vector512<float> d = ref Unsafe.Add(ref destinationBase, i * 4);
@ -795,6 +761,8 @@ internal static partial class SimdUtils
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref Vector256<float> destinationBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(destination));
Vector256<float> high = Vector256.Create(reciprocalHigh);
Vector256<float> low = Vector256.Create(reciprocalLow);
for (nuint i = 0; i < n; i++)
{
@ -807,11 +775,15 @@ internal static partial class SimdUtils
ref ulong refULong = ref Unsafe.As<byte, ulong>(ref Unsafe.Add(ref sourceBase, si));
Vector256<int> i3 = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(Unsafe.Add(ref refULong, 3)).AsByte());
// Declare multiplier on each line. Codegen is better.
Vector256<float> f0 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i0);
Vector256<float> f1 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i1);
Vector256<float> f2 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i2);
Vector256<float> f3 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i3);
Vector256<float> f0 = Avx.ConvertToVector256Single(i0);
Vector256<float> f1 = Avx.ConvertToVector256Single(i1);
Vector256<float> f2 = Avx.ConvertToVector256Single(i2);
Vector256<float> f3 = Avx.ConvertToVector256Single(i3);
f0 = Vector256_.FusedMultiplyAdd(f0, high, f0 * low);
f1 = Vector256_.FusedMultiplyAdd(f1, high, f1 * low);
f2 = Vector256_.FusedMultiplyAdd(f2, high, f2 * low);
f3 = Vector256_.FusedMultiplyAdd(f3, high, f3 * low);
ref Vector256<float> d = ref Unsafe.Add(ref destinationBase, i * 4);
@ -830,7 +802,8 @@ internal static partial class SimdUtils
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref Vector128<float> destinationBase = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(destination));
Vector128<float> scale = Vector128.Create(1 / (float)byte.MaxValue);
Vector128<float> high = Vector128.Create(reciprocalHigh);
Vector128<float> low = Vector128.Create(reciprocalLow);
for (nuint i = 0; i < n; i++)
{
@ -855,10 +828,15 @@ internal static partial class SimdUtils
(i2, i3) = Vector128.Widen(s1.AsInt16());
}
Vector128<float> f0 = scale * Vector128.ConvertToSingle(i0);
Vector128<float> f1 = scale * Vector128.ConvertToSingle(i1);
Vector128<float> f2 = scale * Vector128.ConvertToSingle(i2);
Vector128<float> f3 = scale * Vector128.ConvertToSingle(i3);
Vector128<float> f0 = Vector128.ConvertToSingle(i0);
Vector128<float> f1 = Vector128.ConvertToSingle(i1);
Vector128<float> f2 = Vector128.ConvertToSingle(i2);
Vector128<float> f3 = Vector128.ConvertToSingle(i3);
f0 = Vector128_.FusedMultiplyAdd(f0, high, f0 * low);
f1 = Vector128_.FusedMultiplyAdd(f1, high, f1 * low);
f2 = Vector128_.FusedMultiplyAdd(f2, high, f2 * low);
f3 = Vector128_.FusedMultiplyAdd(f3, high, f3 * low);
ref Vector128<float> d = ref Unsafe.Add(ref destinationBase, i * 4);
@ -879,6 +857,19 @@ internal static partial class SimdUtils
internal static void NormalizedFloatToByteSaturateReduce(
ref ReadOnlySpan<float> source,
ref Span<byte> destination)
=> FloatToByteSaturateReduce(ref source, ref destination, byte.MaxValue);
/// <summary>
/// Converts as many scaled floating-point values as possible to bytes and retains the unconverted remainder.
/// </summary>
/// <param name="source">The source buffer.</param>
/// <param name="destination">The destination buffer.</param>
/// <param name="scaleFactor">The factor applied before conversion.</param>
[MethodImpl(InliningOptions.ShortMethod)]
internal static void FloatToByteSaturateReduce(
ref ReadOnlySpan<float> source,
ref Span<byte> destination,
float scaleFactor)
{
DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!");
@ -903,9 +894,10 @@ internal static partial class SimdUtils
if (adjustedCount > 0)
{
NormalizedFloatToByteSaturate(
FloatToByteSaturate(
source[..adjustedCount],
destination[..adjustedCount]);
destination[..adjustedCount],
scaleFactor);
source = source[adjustedCount..];
destination = destination[adjustedCount..];
@ -925,6 +917,18 @@ internal static partial class SimdUtils
internal static void NormalizedFloatToByteSaturate(
ReadOnlySpan<float> source,
Span<byte> destination)
=> FloatToByteSaturate(source, destination, byte.MaxValue);
/// <summary>
/// Converts scaled floating-point values to bytes using saturating round-to-nearest with midpoint values away from zero.
/// </summary>
/// <param name="source">The source buffer.</param>
/// <param name="destination">The destination buffer.</param>
/// <param name="scaleFactor">The factor applied before conversion.</param>
internal static void FloatToByteSaturate(
ReadOnlySpan<float> source,
Span<byte> destination,
float scaleFactor)
{
if (Vector512.IsHardwareAccelerated && Avx512BW.IsSupported)
{
@ -935,7 +939,7 @@ internal static partial class SimdUtils
ref Vector512<float> sourceBase = ref Unsafe.As<float, Vector512<float>>(ref MemoryMarshal.GetReference(source));
ref Vector512<byte> destinationBase = ref Unsafe.As<byte, Vector512<byte>>(ref MemoryMarshal.GetReference(destination));
Vector512<float> scale = Vector512.Create((float)byte.MaxValue);
Vector512<float> scale = Vector512.Create(scaleFactor);
Vector512<int> mask = PermuteMaskDeinterleave16x32();
for (nuint i = 0; i < n; i++)
@ -947,10 +951,10 @@ internal static partial class SimdUtils
Vector512<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector512<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector512<int> w0 = Vector512_.ConvertToInt32RoundToEven(f0);
Vector512<int> w1 = Vector512_.ConvertToInt32RoundToEven(f1);
Vector512<int> w2 = Vector512_.ConvertToInt32RoundToEven(f2);
Vector512<int> w3 = Vector512_.ConvertToInt32RoundToEven(f3);
Vector512<int> w0 = Vector512_.ConvertToInt32RoundAwayFromZero(f0);
Vector512<int> w1 = Vector512_.ConvertToInt32RoundAwayFromZero(f1);
Vector512<int> w2 = Vector512_.ConvertToInt32RoundAwayFromZero(f2);
Vector512<int> w3 = Vector512_.ConvertToInt32RoundAwayFromZero(f3);
Vector512<short> u0 = Avx512BW.PackSignedSaturate(w0, w1);
Vector512<short> u1 = Avx512BW.PackSignedSaturate(w2, w3);
@ -969,7 +973,7 @@ internal static partial class SimdUtils
ref Vector256<float> sourceBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(source));
ref Vector256<byte> destinationBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(destination));
Vector256<float> scale = Vector256.Create((float)byte.MaxValue);
Vector256<float> scale = Vector256.Create(scaleFactor);
Vector256<int> mask = PermuteMaskDeinterleave8x32();
for (nuint i = 0; i < n; i++)
@ -981,10 +985,10 @@ internal static partial class SimdUtils
Vector256<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector256<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector256<int> w0 = Vector256_.ConvertToInt32RoundToEven(f0);
Vector256<int> w1 = Vector256_.ConvertToInt32RoundToEven(f1);
Vector256<int> w2 = Vector256_.ConvertToInt32RoundToEven(f2);
Vector256<int> w3 = Vector256_.ConvertToInt32RoundToEven(f3);
Vector256<int> w0 = Vector256_.ConvertToInt32RoundAwayFromZero(f0);
Vector256<int> w1 = Vector256_.ConvertToInt32RoundAwayFromZero(f1);
Vector256<int> w2 = Vector256_.ConvertToInt32RoundAwayFromZero(f2);
Vector256<int> w3 = Vector256_.ConvertToInt32RoundAwayFromZero(f3);
Vector256<short> u0 = Avx2.PackSignedSaturate(w0, w1);
Vector256<short> u1 = Avx2.PackSignedSaturate(w2, w3);
@ -1004,7 +1008,7 @@ internal static partial class SimdUtils
ref Vector128<float> sourceBase = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(source));
ref Vector128<byte> destinationBase = ref Unsafe.As<byte, Vector128<byte>>(ref MemoryMarshal.GetReference(destination));
Vector128<float> scale = Vector128.Create((float)byte.MaxValue);
Vector128<float> scale = Vector128.Create(scaleFactor);
Vector128<int> min = Vector128<int>.Zero;
Vector128<int> max = Vector128.Create((int)byte.MaxValue);
@ -1017,10 +1021,10 @@ internal static partial class SimdUtils
Vector128<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector128<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector128<int> w0 = Vector128_.ConvertToInt32RoundToEven(f0);
Vector128<int> w1 = Vector128_.ConvertToInt32RoundToEven(f1);
Vector128<int> w2 = Vector128_.ConvertToInt32RoundToEven(f2);
Vector128<int> w3 = Vector128_.ConvertToInt32RoundToEven(f3);
Vector128<int> w0 = Vector128_.ConvertToInt32RoundAwayFromZero(f0);
Vector128<int> w1 = Vector128_.ConvertToInt32RoundAwayFromZero(f1);
Vector128<int> w2 = Vector128_.ConvertToInt32RoundAwayFromZero(f2);
Vector128<int> w3 = Vector128_.ConvertToInt32RoundAwayFromZero(f3);
w0 = Vector128_.Clamp(w0, min, max);
w1 = Vector128_.Clamp(w1, min, max);
@ -1172,8 +1176,10 @@ internal static partial class SimdUtils
Vector256<byte> rgb, rg, bx;
Vector256<float> r, g, b;
// Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load,
// so we need 3 extra pixels of addressable slack beyond the vectorized chunk.
const int bytesPerRgbStride = 24;
nuint count = (uint)source.Length / 8;
nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0;
for (nuint i = 0; i < count; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte();
@ -1193,10 +1199,10 @@ internal static partial class SimdUtils
}
int sliceCount = (int)(count * 8);
redChannel = redChannel.Slice(sliceCount);
greenChannel = greenChannel.Slice(sliceCount);
blueChannel = blueChannel.Slice(sliceCount);
source = source.Slice(sliceCount);
redChannel = redChannel[sliceCount..];
greenChannel = greenChannel[sliceCount..];
blueChannel = blueChannel[sliceCount..];
source = source[sliceCount..];
}
}
}

372
src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs

@ -3,8 +3,11 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
@ -41,22 +44,89 @@ internal static partial class SimdUtils
/// <typeparam name="TShuffle">The type of shuffle struct.</typeparam>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <param name="shuffle">The type of shuffle to perform.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Shuffle4<TShuffle>(
ReadOnlySpan<byte> source,
Span<byte> destination,
TShuffle shuffle)
Span<byte> destination)
where TShuffle : struct, IShuffle4
{
VerifyShuffle4SpanInput(source, destination);
shuffle.ShuffleReduce(ref source, ref destination);
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
int length = source.Length;
int i = 0;
// Deal with the remainder:
if (source.Length > 0)
// The same offset flows through descending widths. This keeps a single traversal while
// allowing a row that is not a multiple of the widest register to retain a vectorized tail.
if (Vector512.IsHardwareAccelerated)
{
int fourVectorsFromEnd = length - (Vector512<byte>.Count * 4);
for (; i <= fourVectorsFromEnd; i += Vector512<byte>.Count * 4)
{
// Four independent vectors amortize loop control and expose enough work for the CPU
// to overlap loads, byte shuffles, and stores without changing pixel ordering.
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<byte>.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector512<byte>.Count));
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector512<byte>.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector512<byte>.Count * 2)));
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector512<byte>.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector512<byte>.Count * 3)));
}
int oneVectorFromEnd = length - Vector512<byte>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<byte>.Count)
{
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
int fourVectorsFromEnd = length - (Vector256<byte>.Count * 4);
for (; i <= fourVectorsFromEnd; i += Vector256<byte>.Count * 4)
{
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<byte>.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector256<byte>.Count));
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector256<byte>.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector256<byte>.Count * 2)));
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector256<byte>.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector256<byte>.Count * 3)));
}
int oneVectorFromEnd = length - Vector256<byte>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<byte>.Count)
{
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
shuffle.Shuffle(source, destination);
int fourVectorsFromEnd = length - (Vector128<byte>.Count * 4);
for (; i <= fourVectorsFromEnd; i += Vector128<byte>.Count * 4)
{
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<byte>.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector128<byte>.Count));
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector128<byte>.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector128<byte>.Count * 2)));
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector128<byte>.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector128<byte>.Count * 3)));
}
int oneVectorFromEnd = length - Vector128<byte>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<byte>.Count)
{
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
// The vector cascade leaves fewer than four pixels. A full uint load keeps each pixel
// in a register while the closed operator resolves to its rotate, reverse, or mask sequence.
for (; i < length; i += 4)
{
uint packed = Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)i));
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = TShuffle.Invoke(packed);
}
}
@ -67,23 +137,114 @@ internal static partial class SimdUtils
/// <typeparam name="TShuffle">The type of shuffle struct.</typeparam>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <param name="shuffle">The type of shuffle to perform.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Shuffle3<TShuffle>(
ReadOnlySpan<byte> source,
Span<byte> destination,
TShuffle shuffle)
Span<byte> destination)
where TShuffle : struct, IShuffle3
{
// Source length should be smaller than destination length, and divisible by 3.
VerifyShuffle3SpanInput(source, destination);
shuffle.ShuffleReduce(ref source, ref destination);
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
int length = source.Length;
int i = 0;
// Deal with the remainder:
if (source.Length > 0)
if (Vector128.IsHardwareAccelerated)
{
// Each group contains sixteen XYZ pixels in three registers. For a register beginning
// [X0,Y0,Z0,X1,Y1,Z1,...], the indices [0,1,2,0x80,3,4,5,0x80,...]
// produce four [X,Y,Z,0] pixels. Index 0x80 selects zero on the native byte-shuffle
// instructions and on the portable helper, creating a temporary W lane for TShuffle.
Vector128<byte> padMask = Vector128.Create((byte)0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80);
// After TShuffle places the retained components in bytes 0..2 of each four-byte pixel,
// [0,1,2,4,5,6,8,9,10,12,13,14] packs four triplets into twelve bytes. Rotating that
// mask by twelve positions moves the packed bytes four positions right. Alternating the
// two alignments lets AlignRight stitch four twelve-byte results into three full registers.
Vector128<byte> sliceMask = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80);
Vector128<byte> sliceEndMask = Vector128_.AlignRight(sliceMask, sliceMask, 12);
ref Vector128<byte> sourceVectors = ref Unsafe.As<byte, Vector128<byte>>(ref sourceBase);
ref Vector128<byte> destinationVectors = ref Unsafe.As<byte, Vector128<byte>>(ref destinationBase);
nuint sourceVectorCount = (uint)length / (uint)Vector128<byte>.Count;
nuint vectorIndex = 0;
for (; vectorIndex + 2 < sourceVectorCount; vectorIndex += 3)
{
// Realign the three source registers into four registers holding four complete
// triplets apiece. All source registers are captured before any destination store.
ref Vector128<byte> source0 = ref Unsafe.Add(ref sourceVectors, vectorIndex);
Vector128<byte> v0 = source0;
Vector128<byte> v1 = Unsafe.Add(ref source0, 1);
Vector128<byte> v2 = Unsafe.Add(ref source0, 2);
Vector128<byte> v3 = Vector128_.ShiftRightBytesInVector(v2, 4);
v2 = Vector128_.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v1, v0, 12);
v0 = TShuffle.Invoke(Vector128_.ShuffleNative(v0, padMask));
v1 = TShuffle.Invoke(Vector128_.ShuffleNative(v1, padMask));
v2 = TShuffle.Invoke(Vector128_.ShuffleNative(v2, padMask));
v3 = TShuffle.Invoke(Vector128_.ShuffleNative(v3, padMask));
v0 = Vector128_.ShuffleNative(v0, sliceEndMask);
v1 = Vector128_.ShuffleNative(v1, sliceMask);
v2 = Vector128_.ShuffleNative(v2, sliceEndMask);
v3 = Vector128_.ShuffleNative(v3, sliceMask);
Vector128<byte> destination0 = Vector128_.AlignRight(v1, v0, 4);
Vector128<byte> destination2 = Vector128_.AlignRight(v3, v2, 12);
v1 = Vector128_.ShiftLeftBytesInVector(v1, 4);
v2 = Vector128_.ShiftRightBytesInVector(v2, 4);
Vector128<byte> destination1 = Vector128_.AlignRight(v2, v1, 8);
ref Vector128<byte> destination0Ref = ref Unsafe.Add(ref destinationVectors, vectorIndex);
destination0Ref = destination0;
Unsafe.Add(ref destination0Ref, 1) = destination1;
Unsafe.Add(ref destination0Ref, 2) = destination2;
}
i = (int)(vectorIndex * (uint)Vector128<byte>.Count);
int oneTailVectorFromEnd = length - Vector128<byte>.Count;
for (; i <= oneTailVectorFromEnd; i += 12)
{
// A single readable register contains four complete triplets plus four bytes from
// the following pixels. The pad mask ignores those extra bytes before the operator
// runs, and the slice mask packs the four results into the low twelve bytes.
Vector128<byte> result = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
result = Vector128_.ShuffleNative(result, padMask);
result = TShuffle.Invoke(result);
result = Vector128_.ShuffleNative(result, sliceMask);
// Store exactly twelve bytes so an in-place shuffle does not overwrite the next
// source triplet captured by the following iteration.
Unsafe.As<byte, Vector64<byte>>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = result.GetLower();
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)(i + 8))) = result.AsUInt32().GetElement(2);
}
}
int widenedReadEnd = length - 3;
for (; i < widenedReadEnd; i += 3)
{
shuffle.Shuffle(source, destination);
// The fourth byte belongs to the following pixel, but the operator only contributes the
// low three result bytes. This unaligned read replaces three dependent byte loads safely.
uint packed = Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)i));
uint shuffled = TShuffle.Invoke(packed);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = Unsafe.As<uint, Byte3>(ref shuffled);
}
if (i < length)
{
// The final triplet has no fourth readable byte, so construct only this terminal pixel.
uint packed =
Unsafe.Add(ref sourceBase, (nuint)i) |
((uint)Unsafe.Add(ref sourceBase, (nuint)(i + 1)) << 8) |
((uint)Unsafe.Add(ref sourceBase, (nuint)(i + 2)) << 16);
uint shuffled = TShuffle.Invoke(packed);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = Unsafe.As<uint, Byte3>(ref shuffled);
}
}
@ -94,22 +255,81 @@ internal static partial class SimdUtils
/// <typeparam name="TShuffle">The type of shuffle struct.</typeparam>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <param name="shuffle">The type of shuffle to perform.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Pad3Shuffle4<TShuffle>(
ReadOnlySpan<byte> source,
Span<byte> destination,
TShuffle shuffle)
Span<byte> destination)
where TShuffle : struct, IPad3Shuffle4
{
VerifyPad3Shuffle4SpanInput(source, destination);
shuffle.ShuffleReduce(ref source, ref destination);
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
int sourceLength = source.Length;
int sourceOffset = 0;
int destinationOffset = 0;
// Deal with the remainder:
if (source.Length > 0)
if (Vector128.IsHardwareAccelerated)
{
// For source bytes [X0,Y0,Z0,X1,Y1,Z1,...], the indices
// [0,1,2,0x80,3,4,5,0x80,...] form four [X,Y,Z,0] pixels. The native
// and portable shuffle paths both interpret 0x80 as a zero-producing index.
Vector128<byte> padMask = Vector128.Create((byte)0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80);
// The broadcast ulong has 0xFF in bytes 3 and 7; repeating it across 128 bits
// fills W at byte positions 3, 7, 11, and 15 without modifying X, Y, or Z.
Vector128<byte> opaqueAlpha = Vector128.Create(0xFF000000FF000000UL).AsByte();
ref Vector128<byte> sourceVectors = ref Unsafe.As<byte, Vector128<byte>>(ref sourceBase);
ref Vector128<byte> destinationVectors = ref Unsafe.As<byte, Vector128<byte>>(ref destinationBase);
nuint sourceVectorCount = (uint)sourceLength / (uint)Vector128<byte>.Count;
nuint sourceVectorIndex = 0;
nuint destinationVectorIndex = 0;
for (; sourceVectorIndex + 2 < sourceVectorCount;
sourceVectorIndex += 3, destinationVectorIndex += 4)
{
// Three source registers contain sixteen packed triplets. Aligning at 12, 8, and
// 4-byte boundaries produces four registers whose low twelve bytes each hold four pixels.
ref Vector128<byte> source0 = ref Unsafe.Add(ref sourceVectors, sourceVectorIndex);
Vector128<byte> v0 = source0;
Vector128<byte> v1 = Unsafe.Add(ref source0, 1);
Vector128<byte> v2 = Unsafe.Add(ref source0, 2);
Vector128<byte> v3 = Vector128_.ShiftRightBytesInVector(v2, 4);
v2 = Vector128_.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v1, v0, 12);
ref Vector128<byte> destination0 = ref Unsafe.Add(ref destinationVectors, destinationVectorIndex);
destination0 = TShuffle.Invoke(Vector128_.ShuffleNative(v0, padMask) | opaqueAlpha);
Unsafe.Add(ref destination0, 1) = TShuffle.Invoke(Vector128_.ShuffleNative(v1, padMask) | opaqueAlpha);
Unsafe.Add(ref destination0, 2) = TShuffle.Invoke(Vector128_.ShuffleNative(v2, padMask) | opaqueAlpha);
Unsafe.Add(ref destination0, 3) = TShuffle.Invoke(Vector128_.ShuffleNative(v3, padMask) | opaqueAlpha);
}
sourceOffset = (int)(sourceVectorIndex * (uint)Vector128<byte>.Count);
destinationOffset = (int)(destinationVectorIndex * (uint)Vector128<byte>.Count);
}
int widenedReadEnd = sourceLength - 3;
for (; sourceOffset < widenedReadEnd; sourceOffset += 3, destinationOffset += 4)
{
// The widened load intentionally includes the next pixel's first byte. Replacing that
// high byte with opaque alpha yields the complete XYZW value with one unaligned read.
uint packed = Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset)) | 0xFF000000;
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = TShuffle.Invoke(packed);
}
if (sourceOffset < sourceLength)
{
shuffle.Shuffle(source, destination);
// The final triplet cannot use the widened load because no following byte is in range.
uint packed =
Unsafe.Add(ref sourceBase, (nuint)sourceOffset) |
((uint)Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 1)) << 8) |
((uint)Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 2)) << 16) |
0xFF000000;
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = TShuffle.Invoke(packed);
}
}
@ -120,22 +340,103 @@ internal static partial class SimdUtils
/// <typeparam name="TShuffle">The type of shuffle struct.</typeparam>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <param name="shuffle">The type of shuffle to perform.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Shuffle4Slice3<TShuffle>(
ReadOnlySpan<byte> source,
Span<byte> destination,
TShuffle shuffle)
Span<byte> destination)
where TShuffle : struct, IShuffle4Slice3
{
VerifyShuffle4Slice3SpanInput(source, destination);
shuffle.ShuffleReduce(ref source, ref destination);
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
int sourceLength = source.Length;
int sourceOffset = 0;
int destinationOffset = 0;
// Deal with the remainder:
if (source.Length > 0)
if (Vector128.IsHardwareAccelerated)
{
// Each operator first places the retained components in bytes 0..2 of every four-byte
// pixel. The indices [0,1,2,4,5,6,8,9,10,12,13,14] delete each fourth byte and
// pack four triplets into the low twelve bytes. Indices 0x80 zero the unused bytes.
Vector128<byte> sliceMask = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80);
// Rotating the mask by twelve moves its packed result four bytes right. Alternating
// shifted and unshifted results gives AlignRight the overlap needed to concatenate
// four twelve-byte groups into three complete destination registers.
Vector128<byte> sliceEndMask = Vector128_.AlignRight(sliceMask, sliceMask, 12);
ref Vector128<byte> sourceVectors = ref Unsafe.As<byte, Vector128<byte>>(ref sourceBase);
ref Vector128<byte> destinationVectors = ref Unsafe.As<byte, Vector128<byte>>(ref destinationBase);
nuint sourceVectorCount = (uint)sourceLength / (uint)Vector128<byte>.Count;
nuint sourceVectorIndex = 0;
nuint destinationVectorIndex = 0;
for (; sourceVectorIndex + 3 < sourceVectorCount; sourceVectorIndex += 4, destinationVectorIndex += 3)
{
// Load and transform all sixteen source pixels before writing the shorter output group.
// This preserves forward progress when source and destination begin at the same address.
ref Vector128<byte> source0 = ref Unsafe.Add(ref sourceVectors, sourceVectorIndex);
Vector128<byte> v0 = TShuffle.Invoke(source0);
Vector128<byte> v1 = TShuffle.Invoke(Unsafe.Add(ref source0, 1));
Vector128<byte> v2 = TShuffle.Invoke(Unsafe.Add(ref source0, 2));
Vector128<byte> v3 = TShuffle.Invoke(Unsafe.Add(ref source0, 3));
v0 = Vector128_.ShuffleNative(v0, sliceEndMask);
v1 = Vector128_.ShuffleNative(v1, sliceMask);
v2 = Vector128_.ShuffleNative(v2, sliceEndMask);
v3 = Vector128_.ShuffleNative(v3, sliceMask);
Vector128<byte> destination0 = Vector128_.AlignRight(v1, v0, 4);
Vector128<byte> destination2 = Vector128_.AlignRight(v3, v2, 12);
v1 = Vector128_.ShiftLeftBytesInVector(v1, 4);
v2 = Vector128_.ShiftRightBytesInVector(v2, 4);
Vector128<byte> destination1 = Vector128_.AlignRight(v2, v1, 8);
ref Vector128<byte> destination0Ref = ref Unsafe.Add(ref destinationVectors, destinationVectorIndex);
destination0Ref = destination0;
Unsafe.Add(ref destination0Ref, 1) = destination1;
Unsafe.Add(ref destination0Ref, 2) = destination2;
}
sourceOffset = (int)(sourceVectorIndex * (uint)Vector128<byte>.Count);
destinationOffset = (int)(destinationVectorIndex * (uint)Vector128<byte>.Count);
int oneTailVectorFromEnd = sourceLength - Vector128<byte>.Count;
for (; sourceOffset <= oneTailVectorFromEnd; sourceOffset += 16, destinationOffset += 12)
{
// The operator arranges the three retained components at the front of each pixel.
// One fixed shuffle then compacts four pixels into the low twelve vector bytes.
Vector128<byte> result = TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)sourceOffset));
result = Vector128_.ShuffleNative(result, sliceMask);
// The split store writes the exact 12-byte result and remains safe for in-place shrinking.
Unsafe.As<byte, Vector64<byte>>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = result.GetLower();
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 8))) = result.AsUInt32().GetElement(2);
}
}
int fourPixelsFromEnd = sourceLength - 16;
for (; sourceOffset <= fourPixelsFromEnd; sourceOffset += 16, destinationOffset += 12)
{
// Transform four complete pixels before the first three-byte store. Keeping the source
// values in registers avoids reloads after an in-place shrinking destination advances.
uint packed0 = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset)));
uint packed1 = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 4))));
uint packed2 = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 8))));
uint packed3 = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 12))));
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = Unsafe.As<uint, Byte3>(ref packed0);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 3))) = Unsafe.As<uint, Byte3>(ref packed1);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 6))) = Unsafe.As<uint, Byte3>(ref packed2);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 9))) = Unsafe.As<uint, Byte3>(ref packed3);
}
for (; sourceOffset < sourceLength; sourceOffset += 4, destinationOffset += 3)
{
shuffle.Shuffle(source, destination);
uint packed = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset)));
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = Unsafe.As<uint, Byte3>(ref packed);
}
}
@ -150,10 +451,15 @@ internal static partial class SimdUtils
for (nuint i = 0; i < (uint)source.Length; i += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
Unsafe.Add(ref dBase, i + 3) = Unsafe.Add(ref sBase, p3 + i);
// Stage the scalar tail in a local Vector4 so p0..p3 index source
// values that were captured before any overlapping destination writes.
Vector4 v = Unsafe.As<float, Vector4>(ref Unsafe.Add(ref sBase, i));
ref float pBase = ref Unsafe.As<Vector4, float>(ref v);
Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0);
Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1);
Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2);
Unsafe.Add(ref dBase, i + 3u) = Unsafe.Add(ref pBase, p3);
}
}

93
src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs

@ -0,0 +1,93 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Common.Helpers;
internal static partial class TensorPrimitives_
{
/// <summary>
/// Computes the element-wise sum of the values in <paramref name="x"/> and <paramref name="y"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The first addends.</param>
/// <param name="y">The second addends.</param>
/// <param name="destination">The destination for the sums.</param>
/// <exception cref="ArgumentException"><paramref name="x"/> and <paramref name="y"/> do not have the same length.</exception>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than the input spans.</exception>
/// <exception cref="ArgumentException">
/// An input and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<T>(ReadOnlySpan<T> x, ReadOnlySpan<T> y, Span<T> destination)
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
=> InvokeSpanSpanIntoSpan<T, AddOperator<T>>(x, y, destination);
/// <summary>
/// Computes the element-wise sum of the values in <paramref name="x"/> and the scalar <paramref name="y"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The first addends.</param>
/// <param name="y">The scalar second addend.</param>
/// <param name="destination">The destination for the sums.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<T>(ReadOnlySpan<T> x, T y, Span<T> destination)
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
=> InvokeSpanScalarIntoSpan<T, AddOperator<T>>(x, y, destination);
/// <summary>
/// Adds corresponding values.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private readonly struct AddOperator<T> : IBinaryOperator<T>
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
/// <summary>
/// Gets a value indicating whether this operation supports vector execution.
/// </summary>
public static bool Vectorizable => true;
/// <summary>
/// Adds scalar values.
/// </summary>
/// <param name="x">The first addend.</param>
/// <param name="y">The second addend.</param>
/// <returns>The sum.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Invoke(T x, T y) => x + y;
/// <summary>
/// Adds 128-bit vectors.
/// </summary>
/// <param name="x">The first addends.</param>
/// <param name="y">The second addends.</param>
/// <returns>The sums.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<T> Invoke(Vector128<T> x, Vector128<T> y) => x + y;
/// <summary>
/// Adds 256-bit vectors.
/// </summary>
/// <param name="x">The first addends.</param>
/// <param name="y">The second addends.</param>
/// <returns>The sums.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Invoke(Vector256<T> x, Vector256<T> y) => x + y;
/// <summary>
/// Adds 512-bit vectors.
/// </summary>
/// <param name="x">The first addends.</param>
/// <param name="y">The second addends.</param>
/// <returns>The sums.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Invoke(Vector512<T> x, Vector512<T> y) => x + y;
}
}

322
src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs

@ -0,0 +1,322 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Common.Helpers;
internal static partial class TensorPrimitives_
{
/// <summary>
/// Computes the element-wise result of clamping <paramref name="x"/> to the inclusive range specified
/// by <paramref name="min"/> and <paramref name="max"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The values to clamp.</param>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <param name="destination">The destination for the clamped values.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp<T>(ReadOnlySpan<T> x, T min, T max, Span<T> destination)
where T : INumber<T>
=> InvokeSpanScalarScalarIntoSpan<T, ClampOperator<T>>(x, min, max, destination);
/// <summary>
/// Clamps single-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="value">The values to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ClampSingle(
Vector128<float> value,
Vector128<float> min,
Vector128<float> max)
{
// Unlike the native x86 min/max instructions, the normalized runtime operations propagate a NaN in the
// first operand and select negative zero when equal values have different signs.
Vector128<float> maximum = Vector128.ConditionalSelect(
Vector128.LessThan(min, value)
| ~Vector128.Equals(value, value)
| (Vector128.Equals(value, min) & (min.AsInt32() >> 31).AsSingle()),
value,
min);
return Vector128.ConditionalSelect(
Vector128.LessThan(maximum, max)
| ~Vector128.Equals(maximum, maximum)
| (Vector128.Equals(maximum, max) & (maximum.AsInt32() >> 31).AsSingle()),
maximum,
max);
}
/// <summary>
/// Clamps single-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="value">The values to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ClampSingle(
Vector256<float> value,
Vector256<float> min,
Vector256<float> max)
{
Vector256<float> maximum = Vector256.ConditionalSelect(
Vector256.LessThan(min, value)
| ~Vector256.Equals(value, value)
| (Vector256.Equals(value, min) & (min.AsInt32() >> 31).AsSingle()),
value,
min);
return Vector256.ConditionalSelect(
Vector256.LessThan(maximum, max)
| ~Vector256.Equals(maximum, maximum)
| (Vector256.Equals(maximum, max) & (maximum.AsInt32() >> 31).AsSingle()),
maximum,
max);
}
/// <summary>
/// Clamps single-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="value">The values to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ClampSingle(
Vector512<float> value,
Vector512<float> min,
Vector512<float> max)
{
Vector512<float> maximum = Vector512.ConditionalSelect(
Vector512.LessThan(min, value)
| ~Vector512.Equals(value, value)
| (Vector512.Equals(value, min) & (min.AsInt32() >> 31).AsSingle()),
value,
min);
return Vector512.ConditionalSelect(
Vector512.LessThan(maximum, max)
| ~Vector512.Equals(maximum, maximum)
| (Vector512.Equals(maximum, max) & (maximum.AsInt32() >> 31).AsSingle()),
maximum,
max);
}
/// <summary>
/// Clamps double-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="value">The values to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<double> ClampDouble(
Vector128<double> value,
Vector128<double> min,
Vector128<double> max)
{
Vector128<double> maximum = Vector128.ConditionalSelect(
Vector128.LessThan(min, value)
| ~Vector128.Equals(value, value)
| (Vector128.Equals(value, min) & (min.AsInt64() >> 63).AsDouble()),
value,
min);
return Vector128.ConditionalSelect(
Vector128.LessThan(maximum, max)
| ~Vector128.Equals(maximum, maximum)
| (Vector128.Equals(maximum, max) & (maximum.AsInt64() >> 63).AsDouble()),
maximum,
max);
}
/// <summary>
/// Clamps double-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="value">The values to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<double> ClampDouble(
Vector256<double> value,
Vector256<double> min,
Vector256<double> max)
{
Vector256<double> maximum = Vector256.ConditionalSelect(
Vector256.LessThan(min, value)
| ~Vector256.Equals(value, value)
| (Vector256.Equals(value, min) & (min.AsInt64() >> 63).AsDouble()),
value,
min);
return Vector256.ConditionalSelect(
Vector256.LessThan(maximum, max)
| ~Vector256.Equals(maximum, maximum)
| (Vector256.Equals(maximum, max) & (maximum.AsInt64() >> 63).AsDouble()),
maximum,
max);
}
/// <summary>
/// Clamps double-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="value">The values to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<double> ClampDouble(
Vector512<double> value,
Vector512<double> min,
Vector512<double> max)
{
Vector512<double> maximum = Vector512.ConditionalSelect(
Vector512.LessThan(min, value)
| ~Vector512.Equals(value, value)
| (Vector512.Equals(value, min) & (min.AsInt64() >> 63).AsDouble()),
value,
min);
return Vector512.ConditionalSelect(
Vector512.LessThan(maximum, max)
| ~Vector512.Equals(maximum, maximum)
| (Vector512.Equals(maximum, max) & (maximum.AsInt64() >> 63).AsDouble()),
maximum,
max);
}
/// <summary>
/// Clamps values using the complete runtime tensor contract, including signed-zero correction.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private readonly struct ClampOperator<T> : ITernaryOperator<T>
where T : INumber<T>
{
/// <summary>
/// Gets a value indicating whether this operation supports vector execution.
/// </summary>
public static bool Vectorizable => true;
/// <summary>
/// Clamps a scalar value.
/// </summary>
/// <param name="x">The value.</param>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <returns>The clamped value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Invoke(T x, T min, T max)
=> Vector128<T>.IsSupported ? T.Min(T.Max(x, min), max) : T.Clamp(x, min, max);
/// <summary>
/// Clamps a 128-bit vector.
/// </summary>
/// <param name="x">The values.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<T> Invoke(Vector128<T> x, Vector128<T> min, Vector128<T> max)
{
if (typeof(T) == typeof(float))
{
Vector128<float> result = ClampSingle(
Unsafe.As<Vector128<T>, Vector128<float>>(ref x),
Unsafe.As<Vector128<T>, Vector128<float>>(ref min),
Unsafe.As<Vector128<T>, Vector128<float>>(ref max));
return Unsafe.As<Vector128<float>, Vector128<T>>(ref result);
}
if (typeof(T) == typeof(double))
{
Vector128<double> result = ClampDouble(
Unsafe.As<Vector128<T>, Vector128<double>>(ref x),
Unsafe.As<Vector128<T>, Vector128<double>>(ref min),
Unsafe.As<Vector128<T>, Vector128<double>>(ref max));
return Unsafe.As<Vector128<double>, Vector128<T>>(ref result);
}
return Vector128_.Clamp(x, min, max);
}
/// <summary>
/// Clamps a 256-bit vector.
/// </summary>
/// <param name="x">The values.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Invoke(Vector256<T> x, Vector256<T> min, Vector256<T> max)
{
if (typeof(T) == typeof(float))
{
Vector256<float> result = ClampSingle(
Unsafe.As<Vector256<T>, Vector256<float>>(ref x),
Unsafe.As<Vector256<T>, Vector256<float>>(ref min),
Unsafe.As<Vector256<T>, Vector256<float>>(ref max));
return Unsafe.As<Vector256<float>, Vector256<T>>(ref result);
}
if (typeof(T) == typeof(double))
{
Vector256<double> result = ClampDouble(
Unsafe.As<Vector256<T>, Vector256<double>>(ref x),
Unsafe.As<Vector256<T>, Vector256<double>>(ref min),
Unsafe.As<Vector256<T>, Vector256<double>>(ref max));
return Unsafe.As<Vector256<double>, Vector256<T>>(ref result);
}
return Vector256_.Clamp(x, min, max);
}
/// <summary>
/// Clamps a 512-bit vector.
/// </summary>
/// <param name="x">The values.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Invoke(Vector512<T> x, Vector512<T> min, Vector512<T> max)
{
if (typeof(T) == typeof(float))
{
Vector512<float> result = ClampSingle(
Unsafe.As<Vector512<T>, Vector512<float>>(ref x),
Unsafe.As<Vector512<T>, Vector512<float>>(ref min),
Unsafe.As<Vector512<T>, Vector512<float>>(ref max));
return Unsafe.As<Vector512<float>, Vector512<T>>(ref result);
}
if (typeof(T) == typeof(double))
{
Vector512<double> result = ClampDouble(
Unsafe.As<Vector512<T>, Vector512<double>>(ref x),
Unsafe.As<Vector512<T>, Vector512<double>>(ref min),
Unsafe.As<Vector512<T>, Vector512<double>>(ref max));
return Unsafe.As<Vector512<double>, Vector512<T>>(ref result);
}
return Vector512_.Clamp(x, min, max);
}
}
}

87
src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs

@ -0,0 +1,87 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Common.Helpers;
internal static partial class TensorPrimitives_
{
/// <summary>
/// Computes the element-wise result of dividing the values in <paramref name="x"/> by <paramref name="y"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The dividend values.</param>
/// <param name="y">The divisor.</param>
/// <param name="destination">The destination for the quotient values.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Divide<T>(ReadOnlySpan<T> x, T y, Span<T> destination)
where T : IDivisionOperators<T, T, T>
=> InvokeSpanScalarIntoSpanForDivision<T, DivideOperator<T>>(x, y, destination);
/// <summary>
/// Determines whether <typeparamref name="T"/> has the same vector division support as <see cref="int"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <returns><see langword="true"/> when <typeparamref name="T"/> is a 32-bit signed native integer type.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsInt32Like<T>()
=> typeof(T) == typeof(int) || (IntPtr.Size == 4 && typeof(T) == typeof(nint));
/// <summary>
/// Divides values by a scalar.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private readonly struct DivideOperator<T> : IBinaryOperator<T>
where T : IDivisionOperators<T, T, T>
{
/// <summary>
/// Gets a value indicating whether this operation supports vector execution.
/// </summary>
public static bool Vectorizable => typeof(T) == typeof(float)
|| typeof(T) == typeof(double)
|| (Vector256.IsHardwareAccelerated && IsInt32Like<T>());
/// <summary>
/// Divides scalar values.
/// </summary>
/// <param name="x">The dividend.</param>
/// <param name="y">The divisor.</param>
/// <returns>The quotient.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Invoke(T x, T y) => x / y;
/// <summary>
/// Divides 128-bit vectors.
/// </summary>
/// <param name="x">The dividends.</param>
/// <param name="y">The divisors.</param>
/// <returns>The quotients.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<T> Invoke(Vector128<T> x, Vector128<T> y) => x / y;
/// <summary>
/// Divides 256-bit vectors.
/// </summary>
/// <param name="x">The dividends.</param>
/// <param name="y">The divisors.</param>
/// <returns>The quotients.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Invoke(Vector256<T> x, Vector256<T> y) => x / y;
/// <summary>
/// Divides 512-bit vectors.
/// </summary>
/// <param name="x">The dividends.</param>
/// <param name="y">The divisors.</param>
/// <returns>The quotients.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Invoke(Vector512<T> x, Vector512<T> y) => x / y;
}
}

900
src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs

@ -0,0 +1,900 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Common.Helpers;
/// <summary>
/// Provides compatibility implementations for tensor operations that are not available on every target framework.
/// </summary>
/// <remarks>
/// The API shape follows <c>System.Numerics.Tensors.TensorPrimitives</c> so call sites can move to the runtime
/// implementation when ImageSharp no longer supports target frameworks that predate it.
/// </remarks>
internal static partial class TensorPrimitives_
{
/// <summary>
/// Defines an element-wise binary operation.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private interface IBinaryOperator<T>
{
/// <summary>
/// Gets a value indicating whether the operation supports vector execution.
/// </summary>
public static abstract bool Vectorizable { get; }
/// <summary>
/// Applies the operation to scalar values.
/// </summary>
/// <param name="x">The first value.</param>
/// <param name="y">The second value.</param>
/// <returns>The operation result.</returns>
public static abstract T Invoke(T x, T y);
/// <summary>
/// Applies the operation to 128-bit vectors.
/// </summary>
/// <param name="x">The first vector.</param>
/// <param name="y">The second vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector128<T> Invoke(Vector128<T> x, Vector128<T> y);
/// <summary>
/// Applies the operation to 256-bit vectors.
/// </summary>
/// <param name="x">The first vector.</param>
/// <param name="y">The second vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector256<T> Invoke(Vector256<T> x, Vector256<T> y);
/// <summary>
/// Applies the operation to 512-bit vectors.
/// </summary>
/// <param name="x">The first vector.</param>
/// <param name="y">The second vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector512<T> Invoke(Vector512<T> x, Vector512<T> y);
}
/// <summary>
/// Defines an element-wise ternary operation.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private interface ITernaryOperator<T>
{
/// <summary>
/// Gets a value indicating whether the operation supports vector execution.
/// </summary>
public static abstract bool Vectorizable { get; }
/// <summary>
/// Applies the operation to scalar values.
/// </summary>
/// <param name="x">The first value.</param>
/// <param name="y">The second value.</param>
/// <param name="z">The third value.</param>
/// <returns>The operation result.</returns>
public static abstract T Invoke(T x, T y, T z);
/// <summary>
/// Applies the operation to 128-bit vectors.
/// </summary>
/// <param name="x">The first vector.</param>
/// <param name="y">The second vector.</param>
/// <param name="z">The third vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector128<T> Invoke(Vector128<T> x, Vector128<T> y, Vector128<T> z);
/// <summary>
/// Applies the operation to 256-bit vectors.
/// </summary>
/// <param name="x">The first vector.</param>
/// <param name="y">The second vector.</param>
/// <param name="z">The third vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector256<T> Invoke(Vector256<T> x, Vector256<T> y, Vector256<T> z);
/// <summary>
/// Applies the operation to 512-bit vectors.
/// </summary>
/// <param name="x">The first vector.</param>
/// <param name="y">The second vector.</param>
/// <param name="z">The third vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector512<T> Invoke(Vector512<T> x, Vector512<T> y, Vector512<T> z);
}
/// <summary>
/// Validates that an input and destination are either disjoint or begin at the same memory location.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="input">The input values.</param>
/// <param name="destination">The destination values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidateInputOutputSpanNonOverlapping<T>(ReadOnlySpan<T> input, Span<T> destination)
{
// Runtime TensorPrimitives permits exact same-start overlap for in-place operation. A shifted overlap is
// rejected because forward SIMD stores could overwrite input elements before a later load consumes them.
if (!Unsafe.AreSame(ref MemoryMarshal.GetReference(input), ref MemoryMarshal.GetReference(destination))
&& input.Overlaps(destination))
{
ThrowInputAndDestinationSpanMustNotOverlap();
}
}
/// <summary>
/// Throws when input spans do not have the same length.
/// </summary>
[DoesNotReturn]
private static void ThrowSpansMustHaveSameLength()
=> throw new ArgumentException("Input span arguments must all have the same length.");
/// <summary>
/// Throws when the destination cannot hold every result.
/// </summary>
[DoesNotReturn]
private static void ThrowDestinationTooShort()
=> throw new ArgumentException("Destination is too short.", "destination");
/// <summary>
/// Throws when an input and destination overlap without beginning at the same memory location.
/// </summary>
[DoesNotReturn]
private static void ThrowInputAndDestinationSpanMustNotOverlap()
=> throw new ArgumentException(
"The destination span may only overlap with an input span if the two spans start at the same memory location.",
"destination");
/// <summary>
/// Performs an element-wise binary operation between two spans.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="x">The first input values.</param>
/// <param name="y">The second input values.</param>
/// <param name="destination">The destination values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeSpanSpanIntoSpan<T, TOperator>(
ReadOnlySpan<T> x,
ReadOnlySpan<T> y,
Span<T> destination)
where TOperator : struct, IBinaryOperator<T>
{
if (x.Length != y.Length)
{
ThrowSpansMustHaveSameLength();
}
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ValidateInputOutputSpanNonOverlapping(y, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T yRef = ref MemoryMarshal.GetReference(y);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
// Runtime main selects the widest supported pipeline once one complete vector is available.
// Each pipeline preloads its final inputs when a tail overlaps so same-start in-place operation remains correct.
if (TOperator.Vectorizable
&& Vector512.IsHardwareAccelerated
&& Vector512<T>.IsSupported
&& length >= (uint)Vector512<T>.Count)
{
InvokeVectorized512<T, TOperator>(ref xRef, ref yRef, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported && length >= (uint)Vector256<T>.Count)
{
InvokeVectorized256<T, TOperator>(ref xRef, ref yRef, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported && length >= (uint)Vector128<T>.Count)
{
InvokeVectorized128<T, TOperator>(ref xRef, ref yRef, ref destinationRef, length);
return;
}
for (nuint i = 0; i < length; i++)
{
Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), Unsafe.Add(ref yRef, i));
}
}
/// <summary>
/// Performs an element-wise binary operation between a span and a scalar.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="x">The input values.</param>
/// <param name="y">The scalar input.</param>
/// <param name="destination">The destination values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeSpanScalarIntoSpan<T, TOperator>(
ReadOnlySpan<T> x,
T y,
Span<T> destination)
where TOperator : struct, IBinaryOperator<T>
{
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
// Runtime main selects the widest supported pipeline once one complete vector is available.
if (TOperator.Vectorizable
&& Vector512.IsHardwareAccelerated
&& Vector512<T>.IsSupported
&& length >= (uint)Vector512<T>.Count)
{
InvokeVectorized512<T, TOperator>(ref xRef, y, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported && length >= (uint)Vector256<T>.Count)
{
InvokeVectorized256<T, TOperator>(ref xRef, y, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported && length >= (uint)Vector128<T>.Count)
{
InvokeVectorized128<T, TOperator>(ref xRef, y, ref destinationRef, length);
return;
}
for (nuint i = 0; i < length; i++)
{
Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), y);
}
}
/// <summary>
/// Performs element-wise division using the runtime tensor width-selection order.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The division operation to apply.</typeparam>
/// <param name="x">The input values.</param>
/// <param name="y">The scalar divisor.</param>
/// <param name="destination">The destination values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeSpanScalarIntoSpanForDivision<T, TOperator>(
ReadOnlySpan<T> x,
T y,
Span<T> destination)
where TOperator : struct, IBinaryOperator<T>
{
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
// Runtime main selects the widest supported pipeline once one complete vector is available.
if (TOperator.Vectorizable
&& Vector512.IsHardwareAccelerated
&& Vector512<T>.IsSupported
&& length >= (uint)Vector512<T>.Count)
{
InvokeVectorized512<T, TOperator>(ref xRef, y, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported && length >= (uint)Vector256<T>.Count)
{
InvokeVectorized256<T, TOperator>(ref xRef, y, ref destinationRef, length);
return;
}
// Four values fill one 128-bit float vector. Processing exactly one packed prefix before the scalar
// remainder avoids the overlapping second vector that regresses the common seven-element normalization.
if (TOperator.Vectorizable
&& Vector128.IsHardwareAccelerated
&& Vector128<T>.IsSupported
&& length >= (uint)Vector128<T>.Count)
{
nuint vectorCount = (uint)Vector128<T>.Count;
Vector128<T> yVector = Vector128.Create(y);
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef), yVector).StoreUnsafe(ref destinationRef);
for (nuint i = vectorCount; i < length; i++)
{
Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), y);
}
return;
}
for (nuint i = 0; i < length; i++)
{
Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), y);
}
}
/// <summary>
/// Performs an element-wise ternary operation between a span and two scalars.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="x">The input values.</param>
/// <param name="y">The first scalar input.</param>
/// <param name="z">The second scalar input.</param>
/// <param name="destination">The destination values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeSpanScalarScalarIntoSpan<T, TOperator>(
ReadOnlySpan<T> x,
T y,
T z,
Span<T> destination)
where TOperator : struct, ITernaryOperator<T>
{
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
// This dispatch mirrors the runtime pipeline: large inputs use the widest available registers while
// short inputs fall through to a width that fits, keeping the operator contract identical at every length.
if (TOperator.Vectorizable && Vector512.IsHardwareAccelerated && Vector512<T>.IsSupported && length >= (uint)Vector512<T>.Count)
{
InvokeVectorized512<T, TOperator>(ref xRef, y, z, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported && length >= (uint)Vector256<T>.Count)
{
InvokeVectorized256<T, TOperator>(ref xRef, y, z, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported && length >= (uint)Vector128<T>.Count)
{
InvokeVectorized128<T, TOperator>(ref xRef, y, z, ref destinationRef, length);
return;
}
for (nuint i = 0; i < length; i++)
{
Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), y, z);
}
}
/// <summary>
/// Applies a binary operation between two spans with 128-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first element of the first input.</param>
/// <param name="yRef">The first element of the second input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized128<T, TOperator>(
ref T xRef,
ref T yRef,
ref T destinationRef,
nuint length)
where TOperator : struct, IBinaryOperator<T>
{
nuint vectorCount = (uint)Vector128<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
// When a tail exists, both final inputs are loaded before any stores. This permits either source to also
// be the destination when the tail starts inside the range written by the preceding full vector.
Vector128<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector128.LoadUnsafe(ref xRef, length - vectorCount),
Vector128.LoadUnsafe(ref yRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index), Vector128.LoadUnsafe(ref yRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a binary operation between two spans with 256-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first element of the first input.</param>
/// <param name="yRef">The first element of the second input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized256<T, TOperator>(
ref T xRef,
ref T yRef,
ref T destinationRef,
nuint length)
where TOperator : struct, IBinaryOperator<T>
{
nuint vectorCount = (uint)Vector256<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector256<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector256.LoadUnsafe(ref xRef, length - vectorCount),
Vector256.LoadUnsafe(ref yRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index), Vector256.LoadUnsafe(ref yRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a binary operation between two spans with 512-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first element of the first input.</param>
/// <param name="yRef">The first element of the second input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized512<T, TOperator>(
ref T xRef,
ref T yRef,
ref T destinationRef,
nuint length)
where TOperator : struct, IBinaryOperator<T>
{
nuint vectorCount = (uint)Vector512<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector512<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector512.LoadUnsafe(ref xRef, length - vectorCount),
Vector512.LoadUnsafe(ref yRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index), Vector512.LoadUnsafe(ref yRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a binary operation between a span and a scalar with 128-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="y">The scalar input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized128<T, TOperator>(
ref T xRef,
T y,
ref T destinationRef,
nuint length)
where TOperator : struct, IBinaryOperator<T>
{
nuint vectorCount = (uint)Vector128<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector128<T> yVector = Vector128.Create(y);
// When a tail exists, preloading its final vector is required for in-place operation because it must
// observe the original values before an earlier overlapping store writes them.
Vector128<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector128.LoadUnsafe(ref xRef, length - vectorCount),
yVector);
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index), yVector).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a binary operation with 256-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="y">The scalar input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized256<T, TOperator>(
ref T xRef,
T y,
ref T destinationRef,
nuint length)
where TOperator : struct, IBinaryOperator<T>
{
nuint vectorCount = (uint)Vector256<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector256<T> yVector = Vector256.Create(y);
Vector256<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector256.LoadUnsafe(ref xRef, length - vectorCount),
yVector);
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index), yVector).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a binary operation with 512-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="y">The scalar input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized512<T, TOperator>(
ref T xRef,
T y,
ref T destinationRef,
nuint length)
where TOperator : struct, IBinaryOperator<T>
{
nuint vectorCount = (uint)Vector512<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector512<T> yVector = Vector512.Create(y);
Vector512<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector512.LoadUnsafe(ref xRef, length - vectorCount),
yVector);
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index), yVector).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a ternary operation with 128-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="y">The first scalar input.</param>
/// <param name="z">The second scalar input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized128<T, TOperator>(
ref T xRef,
T y,
T z,
ref T destinationRef,
nuint length)
where TOperator : struct, ITernaryOperator<T>
{
nuint vectorCount = (uint)Vector128<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector128<T> yVector = Vector128.Create(y);
Vector128<T> zVector = Vector128.Create(z);
Vector128<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector128.LoadUnsafe(ref xRef, length - vectorCount),
yVector,
zVector);
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index), yVector, zVector).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a ternary operation with 256-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="y">The first scalar input.</param>
/// <param name="z">The second scalar input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized256<T, TOperator>(
ref T xRef,
T y,
T z,
ref T destinationRef,
nuint length)
where TOperator : struct, ITernaryOperator<T>
{
nuint vectorCount = (uint)Vector256<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector256<T> yVector = Vector256.Create(y);
Vector256<T> zVector = Vector256.Create(z);
Vector256<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector256.LoadUnsafe(ref xRef, length - vectorCount),
yVector,
zVector);
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index), yVector, zVector).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a ternary operation with 512-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="y">The first scalar input.</param>
/// <param name="z">The second scalar input.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeVectorized512<T, TOperator>(
ref T xRef,
T y,
T z,
ref T destinationRef,
nuint length)
where TOperator : struct, ITernaryOperator<T>
{
nuint vectorCount = (uint)Vector512<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector512<T> yVector = Vector512.Create(y);
Vector512<T> zVector = Vector512.Create(z);
Vector512<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(
Vector512.LoadUnsafe(ref xRef, length - vectorCount),
yVector,
zVector);
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index), yVector, zVector).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
}

249
src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs

@ -0,0 +1,249 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Common.Helpers;
internal static partial class TensorPrimitives_
{
/// <summary>
/// Computes the element-wise maximum of the values in <paramref name="x"/> and <paramref name="y"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The values to compare.</param>
/// <param name="y">The value to compare with each element.</param>
/// <param name="destination">The destination for the maximum values.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Max<T>(ReadOnlySpan<T> x, T y, Span<T> destination)
where T : INumber<T>
=> InvokeSpanScalarIntoSpan<T, MaxOperator<T>>(x, y, destination);
/// <summary>
/// Selects maximum single-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> MaxSingle(Vector128<float> x, Vector128<float> y)
{
// The .NET 8 operation already handles ordered unequal values. Correct its second-operand result for a
// first-operand NaN, then use bitwise AND for equal values so positive zero wins regardless of operand order.
Vector128<float> result = Vector128.Max(x, y);
result = Vector128.ConditionalSelect(~Vector128.Equals(x, x), x, result);
return Vector128.ConditionalSelect(
Vector128.Equals(x, y),
x & y,
result);
}
/// <summary>
/// Selects maximum single-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> MaxSingle(Vector256<float> x, Vector256<float> y)
{
Vector256<float> result = Vector256.Max(x, y);
result = Vector256.ConditionalSelect(~Vector256.Equals(x, x), x, result);
return Vector256.ConditionalSelect(
Vector256.Equals(x, y),
x & y,
result);
}
/// <summary>
/// Selects maximum single-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> MaxSingle(Vector512<float> x, Vector512<float> y)
{
Vector512<float> result = Vector512.Max(x, y);
result = Vector512.ConditionalSelect(~Vector512.Equals(x, x), x, result);
return Vector512.ConditionalSelect(
Vector512.Equals(x, y),
x & y,
result);
}
/// <summary>
/// Selects maximum double-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<double> MaxDouble(Vector128<double> x, Vector128<double> y)
{
Vector128<double> result = Vector128.Max(x, y);
result = Vector128.ConditionalSelect(~Vector128.Equals(x, x), x, result);
return Vector128.ConditionalSelect(
Vector128.Equals(x, y),
x & y,
result);
}
/// <summary>
/// Selects maximum double-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<double> MaxDouble(Vector256<double> x, Vector256<double> y)
{
Vector256<double> result = Vector256.Max(x, y);
result = Vector256.ConditionalSelect(~Vector256.Equals(x, x), x, result);
return Vector256.ConditionalSelect(
Vector256.Equals(x, y),
x & y,
result);
}
/// <summary>
/// Selects maximum double-precision values with the normalized runtime semantics.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<double> MaxDouble(Vector512<double> x, Vector512<double> y)
{
Vector512<double> result = Vector512.Max(x, y);
result = Vector512.ConditionalSelect(~Vector512.Equals(x, x), x, result);
return Vector512.ConditionalSelect(
Vector512.Equals(x, y),
x & y,
result);
}
/// <summary>
/// Selects the maximum corresponding values.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private readonly struct MaxOperator<T> : IBinaryOperator<T>
where T : INumber<T>
{
/// <summary>
/// Gets a value indicating whether this operation supports vector execution.
/// </summary>
public static bool Vectorizable => true;
/// <summary>
/// Selects the maximum scalar value.
/// </summary>
/// <param name="x">The first value.</param>
/// <param name="y">The second value.</param>
/// <returns>The maximum value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Invoke(T x, T y) => T.Max(x, y);
/// <summary>
/// Selects the maximum values from 128-bit vectors.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<T> Invoke(Vector128<T> x, Vector128<T> y)
{
if (typeof(T) == typeof(float))
{
Vector128<float> result = MaxSingle(
Unsafe.As<Vector128<T>, Vector128<float>>(ref x),
Unsafe.As<Vector128<T>, Vector128<float>>(ref y));
return Unsafe.As<Vector128<float>, Vector128<T>>(ref result);
}
if (typeof(T) == typeof(double))
{
Vector128<double> result = MaxDouble(
Unsafe.As<Vector128<T>, Vector128<double>>(ref x),
Unsafe.As<Vector128<T>, Vector128<double>>(ref y));
return Unsafe.As<Vector128<double>, Vector128<T>>(ref result);
}
return Vector128.Max(x, y);
}
/// <summary>
/// Selects the maximum values from 256-bit vectors.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Invoke(Vector256<T> x, Vector256<T> y)
{
if (typeof(T) == typeof(float))
{
Vector256<float> result = MaxSingle(
Unsafe.As<Vector256<T>, Vector256<float>>(ref x),
Unsafe.As<Vector256<T>, Vector256<float>>(ref y));
return Unsafe.As<Vector256<float>, Vector256<T>>(ref result);
}
if (typeof(T) == typeof(double))
{
Vector256<double> result = MaxDouble(
Unsafe.As<Vector256<T>, Vector256<double>>(ref x),
Unsafe.As<Vector256<T>, Vector256<double>>(ref y));
return Unsafe.As<Vector256<double>, Vector256<T>>(ref result);
}
return Vector256.Max(x, y);
}
/// <summary>
/// Selects the maximum values from 512-bit vectors.
/// </summary>
/// <param name="x">The first values.</param>
/// <param name="y">The second values.</param>
/// <returns>The maximum values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Invoke(Vector512<T> x, Vector512<T> y)
{
if (typeof(T) == typeof(float))
{
Vector512<float> result = MaxSingle(
Unsafe.As<Vector512<T>, Vector512<float>>(ref x),
Unsafe.As<Vector512<T>, Vector512<float>>(ref y));
return Unsafe.As<Vector512<float>, Vector512<T>>(ref result);
}
if (typeof(T) == typeof(double))
{
Vector512<double> result = MaxDouble(
Unsafe.As<Vector512<T>, Vector512<double>>(ref x),
Unsafe.As<Vector512<T>, Vector512<double>>(ref y));
return Unsafe.As<Vector512<double>, Vector512<T>>(ref result);
}
return Vector512.Max(x, y);
}
}
}

76
src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs

@ -0,0 +1,76 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Common.Helpers;
internal static partial class TensorPrimitives_
{
/// <summary>
/// Computes the element-wise product of the values in <paramref name="x"/> and <paramref name="y"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The multiplicands.</param>
/// <param name="y">The multiplier.</param>
/// <param name="destination">The destination for the products.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Multiply<T>(ReadOnlySpan<T> x, T y, Span<T> destination)
where T : IMultiplyOperators<T, T, T>, IMultiplicativeIdentity<T, T>
=> InvokeSpanScalarIntoSpan<T, MultiplyOperator<T>>(x, y, destination);
/// <summary>
/// Multiplies corresponding values.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private readonly struct MultiplyOperator<T> : IBinaryOperator<T>
where T : IMultiplyOperators<T, T, T>, IMultiplicativeIdentity<T, T>
{
/// <summary>
/// Gets a value indicating whether this operation supports vector execution.
/// </summary>
public static bool Vectorizable => true;
/// <summary>
/// Multiplies scalar values.
/// </summary>
/// <param name="x">The multiplicand.</param>
/// <param name="y">The multiplier.</param>
/// <returns>The product.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Invoke(T x, T y) => x * y;
/// <summary>
/// Multiplies 128-bit vectors.
/// </summary>
/// <param name="x">The multiplicands.</param>
/// <param name="y">The multipliers.</param>
/// <returns>The products.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<T> Invoke(Vector128<T> x, Vector128<T> y) => x * y;
/// <summary>
/// Multiplies 256-bit vectors.
/// </summary>
/// <param name="x">The multiplicands.</param>
/// <param name="y">The multipliers.</param>
/// <returns>The products.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Invoke(Vector256<T> x, Vector256<T> y) => x * y;
/// <summary>
/// Multiplies 512-bit vectors.
/// </summary>
/// <param name="x">The multiplicands.</param>
/// <param name="y">The multipliers.</param>
/// <returns>The products.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Invoke(Vector512<T> x, Vector512<T> y) => x * y;
}
}

332
src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs

@ -0,0 +1,332 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Common.Helpers;
internal static partial class TensorPrimitives_
{
/// <summary>
/// Defines an element-wise unary operation.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private interface IUnaryOperator<T>
{
/// <summary>
/// Gets a value indicating whether the operation supports vector execution.
/// </summary>
public static abstract bool Vectorizable { get; }
/// <summary>
/// Applies the operation to a scalar value.
/// </summary>
/// <param name="x">The input value.</param>
/// <returns>The operation result.</returns>
public static abstract T Invoke(T x);
/// <summary>
/// Applies the operation to a 128-bit vector.
/// </summary>
/// <param name="x">The input vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector128<T> Invoke(Vector128<T> x);
/// <summary>
/// Applies the operation to a 256-bit vector.
/// </summary>
/// <param name="x">The input vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector256<T> Invoke(Vector256<T> x);
/// <summary>
/// Applies the operation to a 512-bit vector.
/// </summary>
/// <param name="x">The input vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector512<T> Invoke(Vector512<T> x);
}
/// <summary>
/// Computes the element-wise negation of the values in <paramref name="x"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The values to negate.</param>
/// <param name="destination">The destination for the negated values.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Negate<T>(ReadOnlySpan<T> x, Span<T> destination)
where T : IUnaryNegationOperators<T, T>
=> InvokeSpanIntoSpan<T, NegateOperator<T>>(x, destination);
/// <summary>
/// Performs an element-wise unary operation over a span.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="x">The input values.</param>
/// <param name="destination">The destination values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeSpanIntoSpan<T, TOperator>(ReadOnlySpan<T> x, Span<T> destination)
where TOperator : struct, IUnaryOperator<T>
{
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
// Runtime main selects the widest supported pipeline once one complete vector is available.
if (TOperator.Vectorizable
&& Vector512.IsHardwareAccelerated
&& Vector512<T>.IsSupported
&& length >= (uint)Vector512<T>.Count)
{
InvokeUnaryVectorized512<T, TOperator>(ref xRef, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported && length >= (uint)Vector256<T>.Count)
{
InvokeUnaryVectorized256<T, TOperator>(ref xRef, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported && length >= (uint)Vector128<T>.Count)
{
InvokeUnaryVectorized128<T, TOperator>(ref xRef, ref destinationRef, length);
return;
}
for (nuint i = 0; i < length; i++)
{
Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i));
}
}
/// <summary>
/// Applies a unary operation with 128-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeUnaryVectorized128<T, TOperator>(ref T xRef, ref T destinationRef, nuint length)
where TOperator : struct, IUnaryOperator<T>
{
nuint vectorCount = (uint)Vector128<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
// The final vector overlaps the preceding store when the length is not a vector multiple. Loading it
// before any stores preserves same-start in-place operation because it captures the original tail.
Vector128<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a unary operation with 256-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeUnaryVectorized256<T, TOperator>(ref T xRef, ref T destinationRef, nuint length)
where TOperator : struct, IUnaryOperator<T>
{
nuint vectorCount = (uint)Vector256<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector256<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a unary operation with 512-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeUnaryVectorized512<T, TOperator>(ref T xRef, ref T destinationRef, nuint length)
where TOperator : struct, IUnaryOperator<T>
{
nuint vectorCount = (uint)Vector512<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector512<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Implements element-wise negation for scalar and SIMD inputs.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private readonly struct NegateOperator<T> : IUnaryOperator<T>
where T : IUnaryNegationOperators<T, T>
{
/// <inheritdoc />
public static bool Vectorizable => true;
/// <inheritdoc />
public static T Invoke(T x) => -x;
/// <inheritdoc />
public static Vector128<T> Invoke(Vector128<T> x)
{
if (typeof(T) == typeof(float))
{
// IEEE-754 negation toggles the sign bit. Expressing that operation explicitly avoids the
// subtraction-based ARM64 code generated by .NET 8 for generic vector negation, which loses
// the sign when +0F is negated and therefore differs from both scalar and runtime-main behavior.
return x ^ Vector128.Create(-0F).As<float, T>();
}
if (typeof(T) == typeof(double))
{
// Double-precision values use the same sign-bit representation, with the sign in bit 63.
return x ^ Vector128.Create(-0D).As<double, T>();
}
return -x;
}
/// <inheritdoc />
public static Vector256<T> Invoke(Vector256<T> x)
{
if (typeof(T) == typeof(float))
{
// Keep the operation bitwise at every width so ARM64 preserves signed zero exactly.
return x ^ Vector256.Create(-0F).As<float, T>();
}
if (typeof(T) == typeof(double))
{
return x ^ Vector256.Create(-0D).As<double, T>();
}
return -x;
}
/// <inheritdoc />
public static Vector512<T> Invoke(Vector512<T> x)
{
if (typeof(T) == typeof(float))
{
// Vector512 can be hardware accelerated directly or decomposed by the runtime; the explicit
// bit operation provides identical IEEE-754 behavior in either case.
return x ^ Vector512.Create(-0F).As<float, T>();
}
if (typeof(T) == typeof(double))
{
return x ^ Vector512.Create(-0D).As<double, T>();
}
return -x;
}
}
}

101
src/ImageSharp/Common/Helpers/Vector128Utilities.cs

@ -307,11 +307,37 @@ internal static class Vector128_
return Vector128.ConvertToInt32(val_2p23_f32 | sign);
}
/// <summary>
/// Converts all values in <paramref name="vector"/> to signed 32-bit integers, rounding midpoint values away from zero.
/// </summary>
/// <param name="vector">The values to convert.</param>
/// <returns>The converted integer values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> ConvertToInt32RoundAwayFromZero(Vector128<float> vector)
{
if (Sse2.IsSupported)
{
// The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero.
Vector128<float> x86Adjustment = Vector128.Create(.5F) | (vector & Vector128.Create(-0F));
return Sse2.ConvertToVector128Int32WithTruncation(vector + x86Adjustment);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.ConvertToInt32RoundAwayFromZero(vector);
}
Vector128<float> sign = vector & Vector128.Create(-0F);
Vector128<float> fallbackAdjustment = Vector128.Create(.5F) | sign;
return Vector128.ConvertToInt32(vector + fallbackAdjustment);
}
/// <summary>
/// Rounds all values in <paramref name="vector"/> to the nearest integer
/// following <see cref="MidpointRounding.ToEven"/> semantics.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The vector.</param>
/// <returns>The vector with each value rounded to the nearest integer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> RoundToNearestInteger(Vector128<float> vector)
{
@ -338,30 +364,58 @@ internal static class Vector128_
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector128{Single}"/>.
/// Computes an estimate of (<paramref name="left"/> * <paramref name="right"/>) + <paramref name="addend"/>.
/// </summary>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
/// <param name="left">The first vector to multiply.</param>
/// <param name="right">The second vector to multiply.</param>
/// <param name="addend">The vector to add to the product.</param>
/// <returns>An estimate of the multiplication and addition result.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> MultiplyAdd(
Vector128<float> va,
Vector128<float> vm0,
Vector128<float> vm1)
public static Vector128<float> MultiplyAddEstimate(Vector128<float> left, Vector128<float> right, Vector128<float> addend)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(vm1, vm0, va);
return Fma.MultiplyAdd(left, right, addend);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.FusedMultiplyAdd(va, vm0, vm1);
return AdvSimd.FusedMultiplyAdd(addend, left, right);
}
return va + (vm0 * vm1);
return (left * right) + addend;
}
/// <summary>
/// Computes (<paramref name="left"/> * <paramref name="right"/>) + <paramref name="addend"/>, rounded as one ternary operation.
/// </summary>
/// <param name="left">The first vector to multiply.</param>
/// <param name="right">The second vector to multiply.</param>
/// <param name="addend">The vector to add to the product.</param>
/// <returns>The fused multiplication and addition result.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> FusedMultiplyAdd(Vector128<float> left, Vector128<float> right, Vector128<float> addend)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(left, right, addend);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.FusedMultiplyAdd(addend, left, right);
}
// WebAssembly SIMD has no exact fused multiply-add, so match the runtime fallback by preserving fused rounding per element.
Vector64<float> lower = Vector64.Create(
MathF.FusedMultiplyAdd(left.GetElement(0), right.GetElement(0), addend.GetElement(0)),
MathF.FusedMultiplyAdd(left.GetElement(1), right.GetElement(1), addend.GetElement(1)));
Vector64<float> upper = Vector64.Create(
MathF.FusedMultiplyAdd(left.GetElement(2), right.GetElement(2), addend.GetElement(2)),
MathF.FusedMultiplyAdd(left.GetElement(3), right.GetElement(3), addend.GetElement(3)));
return Vector128.Create(lower, upper);
}
/// <summary>
@ -1242,22 +1296,9 @@ internal static class Vector128_
return PackedSimd.SubtractSaturate(left, right);
}
// Widen inputs to 16-bit
(Vector128<ushort> leftLo, Vector128<ushort> leftHi) = Vector128.Widen(left);
(Vector128<ushort> rightLo, Vector128<ushort> rightHi) = Vector128.Widen(right);
// Subtract
Vector128<ushort> diffLo = leftLo - rightLo;
Vector128<ushort> diffHi = leftHi - rightHi;
// Clamp to signed 8-bit range
Vector128<ushort> max = Vector128.Create((ushort)byte.MaxValue);
diffLo = Clamp(diffLo, Vector128<ushort>.Zero, max);
diffHi = Clamp(diffHi, Vector128<ushort>.Zero, max);
// Narrow back to bytes
return Vector128.Narrow(diffLo, diffHi);
// Subtracting the smaller operand implements the .NET 10 unsigned contract:
// lanes where right exceeds left subtract left from itself and therefore saturate at zero.
return left - Vector128.Min(left, right);
}
/// <summary>

86
src/ImageSharp/Common/Helpers/Vector256Utilities.cs

@ -73,11 +73,32 @@ internal static class Vector256_
return Vector256.ConvertToInt32(val_2p23_f32 | sign);
}
/// <summary>
/// Converts all values in <paramref name="vector"/> to signed 32-bit integers, rounding midpoint values away from zero.
/// </summary>
/// <param name="vector">The values to convert.</param>
/// <returns>The converted integer values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> ConvertToInt32RoundAwayFromZero(Vector256<float> vector)
{
if (Avx.IsSupported)
{
// The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero.
Vector256<float> x86Adjustment = Vector256.Create(.5F) | (vector & Vector256.Create(-0F));
return Avx.ConvertToVector256Int32WithTruncation(vector + x86Adjustment);
}
Vector256<float> sign = vector & Vector256.Create(-0F);
Vector256<float> fallbackAdjustment = Vector256.Create(.5F) | sign;
return Vector256.ConvertToInt32(vector + fallbackAdjustment);
}
/// <summary>
/// Rounds all values in <paramref name="vector"/> to the nearest integer
/// following <see cref="MidpointRounding.ToEven"/> semantics.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The vector.</param>
/// <returns>The vector with each value rounded to the nearest integer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> RoundToNearestInteger(Vector256<float> vector)
{
@ -94,25 +115,68 @@ internal static class Vector256_
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector256{Single}"/>.
/// Computes an estimate of (<paramref name="left"/> * <paramref name="right"/>) + <paramref name="addend"/>.
/// </summary>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <param name="left">The first vector to multiply.</param>
/// <param name="right">The second vector to multiply.</param>
/// <param name="addend">The vector to add to the product.</param>
/// <returns>An estimate of the multiplication and addition result.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> MultiplyAddEstimate(Vector256<float> left, Vector256<float> right, Vector256<float> addend)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(left, right, addend);
}
Vector128<float> lower = Vector128_.MultiplyAddEstimate(left.GetLower(), right.GetLower(), addend.GetLower());
Vector128<float> upper = Vector128_.MultiplyAddEstimate(left.GetUpper(), right.GetUpper(), addend.GetUpper());
return Vector256.Create(lower, upper);
}
/// <summary>
/// Computes (<paramref name="left"/> * <paramref name="right"/>) + <paramref name="addend"/>, rounded as one ternary operation.
/// </summary>
/// <param name="left">The first vector to multiply.</param>
/// <param name="right">The second vector to multiply.</param>
/// <param name="addend">The vector to add to the product.</param>
/// <returns>The fused multiplication and addition result.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> FusedMultiplyAdd(Vector256<float> left, Vector256<float> right, Vector256<float> addend)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(left, right, addend);
}
// Match the runtime fallback by recursively applying the same fused contract to both halves.
Vector128<float> lower = Vector128_.FusedMultiplyAdd(left.GetLower(), right.GetLower(), addend.GetLower());
Vector128<float> upper = Vector128_.FusedMultiplyAdd(left.GetUpper(), right.GetUpper(), addend.GetUpper());
return Vector256.Create(lower, upper);
}
/// <summary>
/// Performs a multiplication and a negated addition of the <see cref="Vector256{Single}"/>.
/// </summary>
/// <remarks>ret = va - (vm0 * vm1)</remarks>
/// <param name="va">The vector to add to the negated intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> MultiplyAdd(
[MethodImpl(InliningOptions.ShortMethod)]
public static Vector256<float> MultiplyAddNegated(
Vector256<float> va,
Vector256<float> vm0,
Vector256<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(vm0, vm1, va);
return Fma.MultiplyAddNegated(vm0, vm1, va);
}
return va + (vm0 * vm1);
return va - (vm0 * vm1);
}
/// <summary>
@ -431,9 +495,9 @@ internal static class Vector256_
return Avx2.SubtractSaturate(left, right);
}
return Vector256.Create(
Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()),
Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper()));
// The .NET 10 portable implementation applies the same saturated operation to
// both 128-bit halves, allowing each half to select its native instruction set.
return Vector256.Create(Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()), Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper()));
}
/// <summary>

99
src/ImageSharp/Common/Helpers/Vector512Utilities.cs

@ -59,33 +59,110 @@ internal static class Vector512_
public static Vector512<int> ConvertToInt32RoundToEven(Vector512<float> vector)
=> Avx512F.ConvertToVector512Int32(vector);
/// <summary>
/// Converts all values in <paramref name="vector"/> to signed 32-bit integers, rounding midpoint values away from zero.
/// </summary>
/// <param name="vector">The values to convert.</param>
/// <returns>The converted integer values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<int> ConvertToInt32RoundAwayFromZero(Vector512<float> vector)
{
// The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero.
Vector512<float> half = Vector512.Create(.5F) | (vector & Vector512.Create(-0F));
return Avx512F.ConvertToVector512Int32WithTruncation(vector + half);
}
/// <summary>
/// Rounds all values in <paramref name="vector"/> to the nearest integer
/// following <see cref="MidpointRounding.ToEven"/> semantics.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The vector.</param>
/// <returns>The vector with each value rounded to the nearest integer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> RoundToNearestInteger(Vector512<float> vector)
// imm8 = 0b1000:
// imm8[7:4] = 0b0000 -> preserve 0 fractional bits (round to whole numbers)
// imm8[3:0] = 0b1000 -> _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC (round to nearest even, suppress exceptions)
=> Avx512F.RoundScale(vector, 0b0000_1000);
// imm8 = 0b1000:
// imm8[7:4] = 0b0000 -> preserve 0 fractional bits (round to whole numbers)
// imm8[3:0] = 0b1000 -> _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC (round to nearest even, suppress exceptions)
=> Avx512F.RoundScale(vector, 0b0000_1000);
/// <summary>
/// Computes an estimate of (<paramref name="left"/> * <paramref name="right"/>) + <paramref name="addend"/>.
/// </summary>
/// <param name="left">The first vector to multiply.</param>
/// <param name="right">The second vector to multiply.</param>
/// <param name="addend">The vector to add to the product.</param>
/// <returns>An estimate of the multiplication and addition result.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> MultiplyAddEstimate(Vector512<float> left, Vector512<float> right, Vector512<float> addend)
{
if (Avx512F.IsSupported)
{
return Avx512F.FusedMultiplyAdd(left, right, addend);
}
Vector256<float> lower = Vector256_.MultiplyAddEstimate(left.GetLower(), right.GetLower(), addend.GetLower());
Vector256<float> upper = Vector256_.MultiplyAddEstimate(left.GetUpper(), right.GetUpper(), addend.GetUpper());
return Vector512.Create(lower, upper);
}
/// <summary>
/// Computes (<paramref name="left"/> * <paramref name="right"/>) + <paramref name="addend"/>, rounded as one ternary operation.
/// </summary>
/// <param name="left">The first vector to multiply.</param>
/// <param name="right">The second vector to multiply.</param>
/// <param name="addend">The vector to add to the product.</param>
/// <returns>The fused multiplication and addition result.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> FusedMultiplyAdd(Vector512<float> left, Vector512<float> right, Vector512<float> addend)
{
if (Avx512F.IsSupported)
{
return Avx512F.FusedMultiplyAdd(left, right, addend);
}
// Match the runtime fallback by recursively applying the same fused contract to both halves.
Vector256<float> lower = Vector256_.FusedMultiplyAdd(left.GetLower(), right.GetLower(), addend.GetLower());
Vector256<float> upper = Vector256_.FusedMultiplyAdd(left.GetUpper(), right.GetUpper(), addend.GetUpper());
return Vector512.Create(lower, upper);
}
/// <summary>
/// Subtracts packed unsigned 8-bit integers in <paramref name="right"/> from
/// <paramref name="left"/>, saturating negative lane results to zero.
/// </summary>
/// <param name="left">The vector from which <paramref name="right"/> is subtracted.</param>
/// <param name="right">The vector to subtract from <paramref name="left"/>.</param>
/// <returns>The element-wise saturated differences.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> SubtractSaturate(Vector512<byte> left, Vector512<byte> right)
{
if (Avx512BW.IsSupported)
{
return Avx512BW.SubtractSaturate(left, right);
}
// This mirrors the .NET 10 portable implementation: recursively processing both
// 256-bit halves preserves lane order and lets each half select its available ISA.
return Vector512.Create(Vector256_.SubtractSaturate(left.GetLower(), right.GetLower()), Vector256_.SubtractSaturate(left.GetUpper(), right.GetUpper()));
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector512{Single}"/>.
/// Performs a multiplication and a negated addition of the <see cref="Vector512{Single}"/>.
/// </summary>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <remarks>ret = va - (vm0 * vm1)</remarks>
/// <param name="va">The vector to add to the negated intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
/// <returns>The <see cref="Vector512{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> MultiplyAdd(
public static Vector512<float> MultiplyAddNegated(
Vector512<float> va,
Vector512<float> vm0,
Vector512<float> vm1)
=> Avx512F.FusedMultiplyAdd(vm0, vm1, va);
=> Avx512F.FusedMultiplyAddNegated(vm0, vm1, va);
/// <summary>
/// Restricts a vector between a minimum and a maximum value.

72
src/ImageSharp/Common/Tuples/Octet{T}.cs

@ -1,72 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Tuples;
/// <summary>
/// Contains 8 element value tuples of various types.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct Octet<T>
where T : unmanaged
{
public T V0;
public T V1;
public T V2;
public T V3;
public T V4;
public T V5;
public T V6;
public T V7;
/// <inheritdoc/>
public override readonly string ToString()
{
return $"Octet<{typeof(T)}>({this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7})";
}
}
/// <summary>
/// Extension methods for the <see cref="Octet{T}"/> type.
/// </summary>
internal static class OctetExtensions
{
/// <summary>
/// Loads the fields in a target <see cref="Octet{T}"/> of <see cref="uint"/> from one of <see cref="byte"/> type.
/// </summary>
/// <param name="destination">The target <see cref="Octet{T}"/> of <see cref="uint"/> instance.</param>
/// <param name="source">The source <see cref="Octet{T}"/> of <see cref="byte"/> instance.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void LoadFrom(ref this Octet<uint> destination, ref Octet<byte> source)
{
destination.V0 = source.V0;
destination.V1 = source.V1;
destination.V2 = source.V2;
destination.V3 = source.V3;
destination.V4 = source.V4;
destination.V5 = source.V5;
destination.V6 = source.V6;
destination.V7 = source.V7;
}
/// <summary>
/// Loads the fields in a target <see cref="Octet{T}"/> of <see cref="byte"/> from one of <see cref="uint"/> type.
/// </summary>
/// <param name="destination">The target <see cref="Octet{T}"/> of <see cref="byte"/> instance.</param>
/// <param name="source">The source <see cref="Octet{T}"/> of <see cref="uint"/> instance.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void LoadFrom(ref this Octet<byte> destination, ref Octet<uint> source)
{
destination.V0 = (byte)source.V0;
destination.V1 = (byte)source.V1;
destination.V2 = (byte)source.V2;
destination.V3 = (byte)source.V3;
destination.V4 = (byte)source.V4;
destination.V5 = (byte)source.V5;
destination.V6 = (byte)source.V6;
destination.V7 = (byte)source.V7;
}
}

119
src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs

@ -0,0 +1,119 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// A read-only stream over a sequence of length-delimited segments. Bytes are
/// pulled from the inner stream up to the current segment's remaining length;
/// when the segment is exhausted the supplied delegate is invoked to advance
/// to the next segment and return its length. The inner stream is not owned
/// and is not disposed.
/// </summary>
internal sealed class ChunkedReadStream : Stream
{
private static readonly Func<int> GetDataNoOp = () => 0;
private readonly BufferedReadStream innerStream;
private readonly Func<int> getData;
private int currentDataRemaining;
public ChunkedReadStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
{
}
public ChunkedReadStream(BufferedReadStream innerStream, Func<int> getData)
{
this.innerStream = innerStream;
this.getData = getData;
}
/// <inheritdoc/>
public override bool CanRead => this.innerStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => throw new NotSupportedException();
/// <inheritdoc/>
public override long Length => throw new NotSupportedException();
/// <inheritdoc/>
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
/// <summary>
/// Sets the number of bytes available to read from the current segment.
/// Must be called before reading each segment.
/// </summary>
public void SetCurrentSegmentLength(int bytes) => this.currentDataRemaining = bytes;
/// <inheritdoc/>
public override void Flush() => throw new NotSupportedException();
/// <inheritdoc/>
public override int ReadByte()
{
if (this.currentDataRemaining is 0)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return -1;
}
}
int value = this.innerStream.ReadByte();
if (value is not -1)
{
this.currentDataRemaining--;
}
return value;
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
// Decrement currentDataRemaining only by bytes actually returned by
// innerStream.Read; a short read otherwise underflows the segment
// counter and triggers getData() before the segment is truly drained.
int totalBytesRead = 0;
while (totalBytesRead < count)
{
if (this.currentDataRemaining is 0)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
break;
}
}
int bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
int bytesRead = this.innerStream.Read(buffer, offset + totalBytesRead, bytesToRead);
if (bytesRead is 0)
{
break;
}
this.currentDataRemaining -= bytesRead;
totalBytesRead += bytesRead;
}
return totalBytesRead;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

125
src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs

@ -0,0 +1,125 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Reads chunked input, parses the zlib CMF/FLG header, and exposes a
/// <see cref="DeflateStream"/> over the remaining DEFLATE payload. The
/// Adler-32 trailer is not validated.
/// </summary>
internal sealed class ZlibInflateReader : IDisposable
{
/// <summary>
/// Used to read the Adler-32 and Crc-32 checksums.
/// We don't actually use this for anything so it doesn't
/// have to be threadsafe.
/// </summary>
private static readonly byte[] ChecksumBuffer = new byte[4];
private readonly ChunkedReadStream segmentStream;
public ZlibInflateReader(BufferedReadStream innerStream)
=> this.segmentStream = new ChunkedReadStream(innerStream);
public ZlibInflateReader(BufferedReadStream innerStream, Func<int> getData)
=> this.segmentStream = new ChunkedReadStream(innerStream, getData);
/// <summary>
/// Gets the compressed stream over the deframed inner stream.
/// </summary>
public DeflateStream? CompressedStream { get; private set; }
/// <summary>
/// Sets the length of the next segment of compressed input and, on first
/// call, parses the zlib header.
/// </summary>
/// <param name="bytes">The remaining data length for the current segment.</param>
/// <param name="isCriticalChunk">Whether to throw on a malformed zlib header.</param>
/// <returns>The <see cref="bool"/>.</returns>
[MemberNotNullWhen(true, nameof(CompressedStream))]
public bool AllocateNewBytes(int bytes, bool isCriticalChunk)
{
this.segmentStream.SetCurrentSegmentLength(bytes);
if (this.CompressedStream is null)
{
return this.InitializeInflateStream(isCriticalChunk);
}
return true;
}
public void Dispose()
{
this.CompressedStream?.Dispose();
this.segmentStream?.Dispose();
}
[MemberNotNullWhen(true, nameof(CompressedStream))]
private bool InitializeInflateStream(bool isCriticalChunk)
{
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
// 4-bit information field depending on the compression method.
// bits 0 to 3 CM Compression method
// bits 4 to 7 CINFO Compression info
//
// 0 1
// +---+---+
// |CMF|FLG|
// +---+---+
int cmf = this.segmentStream.ReadByte();
int flag = this.segmentStream.ReadByte();
if (cmf == -1 || flag == -1)
{
return false;
}
if ((cmf & 0x0F) == 8)
{
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
int cinfo = (cmf & 0xF0) >> 4;
if (cinfo > 7)
{
if (isCriticalChunk)
{
// Values of CINFO above 7 are not allowed in RFC1950.
// CINFO is not defined in this specification for CM not equal to 8.
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}");
}
return false;
}
}
else if (isCriticalChunk)
{
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
}
else
{
return false;
}
// The preset dictionary.
bool fdict = (flag & 32) != 0;
if (fdict)
{
// We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6
if (this.segmentStream.Read(ChecksumBuffer, 0, 4) != 4)
{
return false;
}
}
this.CompressedStream = new DeflateStream(this.segmentStream, CompressionMode.Decompress, leaveOpen: true);
return true;
}
}

277
src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -1,277 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Provides methods and properties for deframing streams from PNGs.
/// </summary>
internal sealed class ZlibInflateStream : Stream
{
/// <summary>
/// Used to read the Adler-32 and Crc-32 checksums.
/// We don't actually use this for anything so it doesn't
/// have to be threadsafe.
/// </summary>
private static readonly byte[] ChecksumBuffer = new byte[4];
/// <summary>
/// A default delegate to get more data from the inner stream.
/// </summary>
private static readonly Func<int> GetDataNoOp = () => 0;
/// <summary>
/// The inner raw memory stream.
/// </summary>
private readonly BufferedReadStream innerStream;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
/// <summary>
/// The current data remaining to be read.
/// </summary>
private int currentDataRemaining;
/// <summary>
/// Delegate to get more data once we've exhausted the current data remaining.
/// </summary>
private readonly Func<int> getData;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData)
{
this.innerStream = innerStream;
this.getData = getData;
}
/// <inheritdoc/>
public override bool CanRead => this.innerStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => throw new NotSupportedException();
/// <inheritdoc/>
public override long Length => throw new NotSupportedException();
/// <inheritdoc/>
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
/// <summary>
/// Gets the compressed stream over the deframed inner stream.
/// </summary>
public DeflateStream? CompressedStream { get; private set; }
/// <summary>
/// Adds new bytes from a frame found in the original stream.
/// </summary>
/// <param name="bytes">The current remaining data according to the chunk length.</param>
/// <param name="isCriticalChunk">Whether the chunk to be inflated is a critical chunk.</param>
/// <returns>The <see cref="bool"/>.</returns>
[MemberNotNullWhen(true, nameof(CompressedStream))]
public bool AllocateNewBytes(int bytes, bool isCriticalChunk)
{
this.currentDataRemaining = bytes;
if (this.CompressedStream is null)
{
return this.InitializeInflateStream(isCriticalChunk);
}
return true;
}
/// <inheritdoc/>
public override void Flush() => throw new NotSupportedException();
/// <inheritdoc/>
public override int ReadByte()
{
this.currentDataRemaining--;
return this.innerStream.ReadByte();
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
if (this.currentDataRemaining is 0)
{
// Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks.
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return 0;
}
}
int bytesToRead = Math.Min(count, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
int totalBytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
long innerStreamLength = this.innerStream.Length;
// Keep reading data until we've reached the end of the stream or filled the buffer.
int bytesRead = 0;
offset += totalBytesRead;
while (this.currentDataRemaining is 0 && totalBytesRead < count)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return totalBytesRead;
}
offset += bytesRead;
if (offset >= innerStreamLength || offset >= count)
{
return totalBytesRead;
}
bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
bytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
if (bytesRead == 0)
{
return totalBytesRead;
}
totalBytesRead += bytesRead;
}
return totalBytesRead;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
// Dispose managed resources.
if (this.CompressedStream != null)
{
this.CompressedStream.Dispose();
this.CompressedStream = null;
}
}
base.Dispose(disposing);
// Call the appropriate methods to clean up
// unmanaged resources here.
// Note disposing is done.
this.isDisposed = true;
}
[MemberNotNullWhen(true, nameof(CompressedStream))]
private bool InitializeInflateStream(bool isCriticalChunk)
{
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
// 4-bit information field depending on the compression method.
// bits 0 to 3 CM Compression method
// bits 4 to 7 CINFO Compression info
//
// 0 1
// +---+---+
// |CMF|FLG|
// +---+---+
int cmf = this.innerStream.ReadByte();
int flag = this.innerStream.ReadByte();
this.currentDataRemaining -= 2;
if (cmf == -1 || flag == -1)
{
return false;
}
if ((cmf & 0x0F) == 8)
{
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
int cinfo = (cmf & 0xF0) >> 4;
if (cinfo > 7)
{
if (isCriticalChunk)
{
// Values of CINFO above 7 are not allowed in RFC1950.
// CINFO is not defined in this specification for CM not equal to 8.
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}");
}
return false;
}
}
else if (isCriticalChunk)
{
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
}
else
{
return false;
}
// The preset dictionary.
bool fdict = (flag & 32) != 0;
if (fdict)
{
// We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6
if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4)
{
return false;
}
this.currentDataRemaining -= 4;
}
// Initialize the deflate BufferedReadStream.
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true);
return true;
}
}

14
src/ImageSharp/Configuration.cs

@ -2,9 +2,12 @@
// Licensed under the Six Labors Split License.
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
@ -37,6 +40,9 @@ public sealed class Configuration
/// <summary>
/// Initializes a new instance of the <see cref="Configuration" /> class.
/// </summary>
// Every image operation requires a Configuration. Attaching the dependency to its constructors keeps the compile-only
// seed graph visible to modern .NET trimmers without executing that graph or adding module-initialization work.
[DynamicDependency(nameof(AotCompilerTools.SeedPixelOperations), typeof(AotCompilerTools))]
public Configuration()
{
}
@ -45,6 +51,8 @@ public sealed class Configuration
/// Initializes a new instance of the <see cref="Configuration" /> class.
/// </summary>
/// <param name="configurationModules">A collection of configuration modules to register.</param>
// The default Configuration is created through this overload, while callers may use either constructor.
[DynamicDependency(nameof(AotCompilerTools.SeedPixelOperations), typeof(AotCompilerTools))]
public Configuration(params IImageFormatConfigurationModule[] configurationModules)
{
if (configurationModules != null)
@ -64,7 +72,9 @@ public sealed class Configuration
/// <summary>
/// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
/// configured with this <see cref="Configuration"/> instance.
/// Initialized with <see cref="Environment.ProcessorCount"/> by default.
/// A positive value limits the number of concurrent operations to the set value.
/// If set to <c>-1</c>, there is no limit on the number of concurrently running operations.
/// Defaults to <see cref="Environment.ProcessorCount"/>.
/// </summary>
public int MaxDegreeOfParallelism
{
@ -212,6 +222,7 @@ public sealed class Configuration
/// <see cref="TgaConfigurationModule"/>.
/// <see cref="TiffConfigurationModule"/>.
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="ExrConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
@ -224,6 +235,7 @@ public sealed class Configuration
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule());

2
src/ImageSharp/Formats/Bmp/BmpConstants.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Bmp;

71
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -131,6 +131,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
try
{
int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
ushort bitsPerPixel = this.infoHeader.BitsPerPixel;
image = new Image<TPixel>(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata);
@ -138,23 +139,27 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
switch (this.infoHeader.Compression)
{
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3:
case BmpCompression.RGB when bitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3:
this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32:
case BmpCompression.RGB when bitsPerPixel is 32:
this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 24:
case BmpCompression.RGB when bitsPerPixel is 24:
this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 16:
case BmpCompression.RGB when bitsPerPixel is 16:
this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8 && this.processedAlphaMask:
case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8 && this.processedAlphaMask:
this.ReadRgbPaletteWithAlphaMask(
stream,
pixels,
@ -166,7 +171,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8:
case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8:
this.ReadRgbPalette(
stream,
pixels,
@ -179,6 +185,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
break;
case BmpCompression.RGB when bitsPerPixel is <= 0 or > 32:
BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}");
break;
case BmpCompression.RLE24:
this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
@ -340,10 +350,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
pixelRow[x] = this.rleSkippedPixelHandling switch
{
RleSkippedPixelHandling.FirstColorOfPalette => TPixel.FromBgr24(Unsafe.As<byte, Bgr24>(ref colors[colorIdx * 4])),
RleSkippedPixelHandling.Transparent => TPixel.FromScaledVector4(Vector4.Zero),
RleSkippedPixelHandling.Transparent => TPixel.FromUnassociatedScaledVector4(Vector4.Zero),
// Default handling for skipped pixels is black (which is what System.Drawing is also doing).
_ => TPixel.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)),
_ => TPixel.FromUnassociatedScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)),
};
}
else
@ -401,10 +411,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
pixelRow[x] = this.rleSkippedPixelHandling switch
{
RleSkippedPixelHandling.FirstColorOfPalette => TPixel.FromBgr24(Unsafe.As<byte, Bgr24>(ref bufferSpan[idx])),
RleSkippedPixelHandling.Transparent => TPixel.FromScaledVector4(Vector4.Zero),
RleSkippedPixelHandling.Transparent => TPixel.FromUnassociatedScaledVector4(Vector4.Zero),
// Default handling for skipped pixels is black (which is what System.Drawing is also doing).
_ => TPixel.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)),
_ => TPixel.FromUnassociatedScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)),
};
}
else
@ -1262,7 +1272,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
g * invMaxValueGreen,
b * invMaxValueBlue,
alpha);
pixelRow[x] = TPixel.FromScaledVector4(vector4);
pixelRow[x] = TPixel.FromUnassociatedScaledVector4(vector4);
}
else
{
@ -1421,12 +1431,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.infoHeader = BmpInfoHeader.ParseV5(buffer);
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
{
// Read color profile.
long streamPosition = stream.Position;
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
stream.Read(iccProfileData);
this.metadata.IccProfile = new IccProfile(iccProfileData);
this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart));
stream.Position = streamPosition;
}
}
@ -1460,6 +1466,33 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height);
}
/// <summary>
/// Reads the embedded ICC profile from the BMP V5 info header.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="imageMetadata">The image metadata.</param>
/// <param name="infoHeaderStart">The stream position where the info header begins.</param>
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart)
{
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
if (stream.Read(iccProfileData) != iccProfileData.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile.");
}
IccProfile profile = new(iccProfileData);
if (profile.CheckIsValid())
{
imageMetadata.IccProfile = profile;
}
else
{
throw new InvalidIccProfileException("Invalid BMP ICC profile.");
}
}
/// <summary>
/// Reads the <see cref="BmpFileHeader"/> from the stream.
/// </summary>
@ -1538,6 +1571,12 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
case BmpFileMarkerType.Bitmap:
if (this.fileHeader.HasValue)
{
if (this.fileHeader.Value.Offset > stream.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException(
$"Pixel data offset {this.fileHeader.Value.Offset} exceeds file size {stream.Length}.");
}
colorMapSizeBytes = this.fileHeader.Value.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
}
else

2
src/ImageSharp/Formats/Bmp/BmpEncoder.cs

@ -13,7 +13,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoder"/> class.
/// </summary>
public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree;
public BmpEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree;
/// <summary>
/// Gets the number of bits per pixel.

2
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -116,7 +116,7 @@ internal sealed class BmpEncoderCore
this.bitsPerPixel = encoder.BitsPerPixel;
// TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.transparentColorMode = encoder.TransparentColorMode;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;

2
src/ImageSharp/Formats/Bmp/BmpFormat.cs

@ -30,5 +30,5 @@ public sealed class BmpFormat : IImageFormat<BmpMetadata>
public IEnumerable<string> FileExtensions => BmpConstants.FileExtensions;
/// <inheritdoc/>
public BmpMetadata CreateDefaultFormatMetadata() => new();
public BmpMetadata CreateDefaultFormatMetadata() => new BmpMetadata();
}

2
src/ImageSharp/Formats/DecoderOptions.cs

@ -60,7 +60,7 @@ public sealed class DecoderOptions
/// <summary>
/// Gets the segment error handling strategy to use during decoding.
/// </summary>
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical;
public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreAncillary;
/// <summary>
/// Gets a value that controls how ICC profiles are handled during decode.

4
src/ImageSharp/Formats/EncodingUtilities.cs

@ -65,9 +65,9 @@ internal static class EncodingUtilities
for (int y = 0; y < region.Height; y++)
{
Span<TPixel> span = region.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
ReplaceTransparentPixels(vectorsSpan);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
}
}

38
src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs

@ -0,0 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;
/// <summary>
/// Compressor for EXR image data which does not use any compression method.
/// </summary>
internal class NoneExrCompressor : ExrBaseCompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="NoneExrCompressor"/> class.
/// </summary>
/// <param name="output">The output stream to write the compressed image data to.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">Bytes per row block.</param>
/// <param name="bytesPerRow">Bytes per pixel row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
}
/// <inheritdoc/>
public override uint CompressRowBlock(Span<byte> rows, int rowCount)
{
this.Output.Write(rows);
return (uint)rows.Length;
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}

86
src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs

@ -0,0 +1,86 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;
/// <summary>
/// Compressor for EXR image data using the ZIP compression.
/// </summary>
internal class ZipExrCompressor : ExrBaseCompressor
{
private readonly DeflateCompressionLevel compressionLevel;
private readonly MemoryStream memoryStream;
private readonly System.Buffers.IMemoryOwner<byte> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="ZipExrCompressor"/> class.
/// </summary>
/// <param name="output">The stream to write the compressed data to.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
/// <param name="compressionLevel">The compression level for deflate compression.</param>
public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, DeflateCompressionLevel compressionLevel)
: base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
this.compressionLevel = compressionLevel;
this.buffer = allocator.Allocate<byte>((int)bytesPerBlock);
this.memoryStream = new();
}
/// <inheritdoc/>
public override uint CompressRowBlock(Span<byte> rows, int rowCount)
{
// Re-oder pixel values.
Span<byte> reordered = this.buffer.GetSpan()[..(int)(rowCount * this.BytesPerRow)];
int n = reordered.Length;
int t1 = 0;
int t2 = (n + 1) >> 1;
for (int i = 0; i < n; i++)
{
bool isOdd = (i & 1) == 1;
reordered[isOdd ? t2++ : t1++] = rows[i];
}
// Predictor.
Span<byte> predicted = reordered;
byte p = predicted[0];
for (int i = 1; i < predicted.Length; i++)
{
int d = (predicted[i] - p + 128 + 256) & 255;
p = predicted[i];
predicted[i] = (byte)d;
}
this.memoryStream.Seek(0, SeekOrigin.Begin);
using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel))
{
stream.Write(predicted);
stream.Flush();
}
int size = (int)this.memoryStream.Position;
byte[] buffer = this.memoryStream.GetBuffer();
this.Output.Write(buffer, 0, size);
// Reset memory stream for next pixel row.
this.memoryStream.Seek(0, SeekOrigin.Begin);
this.memoryStream.SetLength(0);
return (uint)size;
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
this.buffer.Dispose();
this.memoryStream?.Dispose();
}
}

205
src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs

@ -0,0 +1,205 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
/// <summary>
/// Implementation of B44 decompressor for EXR image data.
/// </summary>
internal class B44ExrCompression : ExrBaseDecompressor
{
private readonly int channelCount;
private readonly byte[] scratch = new byte[14];
private readonly ushort[] s = new ushort[16];
private readonly IMemoryOwner<ushort> tmpBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="B44ExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The width of a pixel row in pixels.</param>
/// <param name="channelCount">The number of channels of the image.</param>
public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
this.channelCount = channelCount;
this.tmpBuffer = allocator.Allocate<ushort>((int)(width * rowsPerBlock * channelCount));
}
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<ushort> outputBuffer = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<ushort> decompressed = this.tmpBuffer.GetSpan();
int outputOffset = 0;
int bytesLeft = (int)compressedBytes;
for (int i = 0; i < this.channelCount && bytesLeft > 0; i++)
{
for (int y = 0; y < this.RowsPerBlock; y += 4)
{
Span<ushort> row0 = decompressed.Slice(outputOffset, this.Width);
outputOffset += this.Width;
Span<ushort> row1 = decompressed.Slice(outputOffset, this.Width);
outputOffset += this.Width;
Span<ushort> row2 = decompressed.Slice(outputOffset, this.Width);
outputOffset += this.Width;
Span<ushort> row3 = decompressed.Slice(outputOffset, this.Width);
outputOffset += this.Width;
int rowOffset = 0;
for (int x = 0; x < this.Width && bytesLeft > 0; x += 4)
{
int bytesRead = stream.Read(this.scratch, 0, 3);
if (bytesRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!");
}
// Check if 3-byte encoded flat field.
if (this.scratch[2] >= 13 << 2)
{
Unpack3(this.scratch, this.s);
bytesLeft -= 3;
}
else
{
bytesRead = stream.Read(this.scratch, 3, 11);
if (bytesRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!");
}
Unpack14(this.scratch, this.s);
bytesLeft -= 14;
}
int n = x + 3 < this.Width ? 4 : this.Width - x;
if (y + 3 < this.RowsPerBlock)
{
this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]);
this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]);
this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]);
this.s.AsSpan(12, n).CopyTo(row3[rowOffset..]);
}
else
{
this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]);
if (y + 1 < this.RowsPerBlock)
{
this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]);
}
if (y + 2 < this.RowsPerBlock)
{
this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]);
}
}
rowOffset += 4;
}
if (bytesLeft <= 0)
{
break;
}
}
}
// Rearrange the decompressed data such that the data for each scan line form a contiguous block.
int offsetDecompressed = 0;
int offsetOutput = 0;
int blockSize = (int)(this.Width * this.RowsPerBlock);
for (int y = 0; y < this.RowsPerBlock; y++)
{
for (int i = 0; i < this.channelCount; i++)
{
decompressed.Slice(offsetDecompressed + (i * blockSize), this.Width).CopyTo(outputBuffer[offsetOutput..]);
offsetOutput += this.Width;
}
offsetDecompressed += this.Width;
}
}
/// <summary>
/// Unpack a 14-byte block into 4 by 4 16-bit pixels.
/// </summary>
/// <param name="b">The source byte data to unpack.</param>
/// <param name="s">Destintation buffer.</param>
private static void Unpack14(Span<byte> b, Span<ushort> s)
{
s[0] = (ushort)((b[0] << 8) | b[1]);
ushort shift = (ushort)(b[2] >> 2);
ushort bias = (ushort)(0x20u << shift);
s[4] = (ushort)(s[0] + ((((b[2] << 4) | (b[3] >> 4)) & 0x3fu) << shift) - bias);
s[8] = (ushort)(s[4] + ((((b[3] << 2) | (b[4] >> 6)) & 0x3fu) << shift) - bias);
s[12] = (ushort)(s[8] + ((b[4] & 0x3fu) << shift) - bias);
s[1] = (ushort)(s[0] + ((uint)(b[5] >> 2) << shift) - bias);
s[5] = (ushort)(s[4] + ((((b[5] << 4) | (b[6] >> 4)) & 0x3fu) << shift) - bias);
s[9] = (ushort)(s[8] + ((((b[6] << 2) | (b[7] >> 6)) & 0x3fu) << shift) - bias);
s[13] = (ushort)(s[12] + ((b[7] & 0x3fu) << shift) - bias);
s[2] = (ushort)(s[1] + ((uint)(b[8] >> 2) << shift) - bias);
s[6] = (ushort)(s[5] + ((((b[8] << 4) | (b[9] >> 4)) & 0x3fu) << shift) - bias);
s[10] = (ushort)(s[9] + ((((b[9] << 2) | (b[10] >> 6)) & 0x3fu) << shift) - bias);
s[14] = (ushort)(s[13] + ((b[10] & 0x3fu) << shift) - bias);
s[3] = (ushort)(s[2] + ((uint)(b[11] >> 2) << shift) - bias);
s[7] = (ushort)(s[6] + ((((b[11] << 4) | (b[12] >> 4)) & 0x3fu) << shift) - bias);
s[11] = (ushort)(s[10] + ((((b[12] << 2) | (b[13] >> 6)) & 0x3fu) << shift) - bias);
s[15] = (ushort)(s[14] + ((b[13] & 0x3fu) << shift) - bias);
for (int i = 0; i < 16; ++i)
{
if ((s[i] & 0x8000) != 0)
{
s[i] &= 0x7fff;
}
else
{
s[i] = (ushort)~s[i];
}
}
}
/// <summary>
/// // Unpack a 3-byte block into 4 by 4 identical 16-bit pixels.
/// </summary>
/// <param name="b">The source byte data to unpack.</param>
/// <param name="s">The destination buffer.</param>
private static void Unpack3(Span<byte> b, Span<ushort> s)
{
s[0] = (ushort)((b[0] << 8) | b[1]);
if ((s[0] & 0x8000) != 0)
{
s[0] &= 0x7fff;
}
else
{
s[0] = (ushort)~s[0];
}
for (int i = 1; i < 16; ++i)
{
s[i] = s[0];
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

41
src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs

@ -0,0 +1,41 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
/// <summary>
/// Decompressor for EXR image data which do not use any compression.
/// </summary>
internal class NoneExrCompression : ExrBaseDecompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="NoneExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per pixel row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The number of pixels per row.</param>
public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
}
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock));
if (bytesRead != (int)this.BytesPerBlock)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data from the stream!");
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}

153
src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs

@ -0,0 +1,153 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
/// <summary>
/// Implementation of PXR24 decompressor for EXR image data.
/// </summary>
internal class Pxr24Compression : ExrBaseDecompressor
{
private readonly IMemoryOwner<byte> tmpBuffer;
private readonly int channelCount;
private readonly ExrPixelType pixelType;
/// <summary>
/// Initializes a new instance of the <see cref="Pxr24Compression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per pixel row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
/// <param name="channelCount">The number of channels for a pixel.</param>
/// <param name="pixelType">The pixel type.</param>
public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount, ExrPixelType pixelType)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
this.channelCount = channelCount;
this.pixelType = pixelType;
}
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<byte> uncompressed = this.tmpBuffer.GetSpan();
Span<ushort> outputBufferHalf = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<uint> outputBufferFloat = MemoryMarshal.Cast<byte, uint>(buffer);
Span<uint> outputBufferUint = MemoryMarshal.Cast<byte, uint>(buffer);
uint uncompressedBytes = this.BytesPerBlock;
UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes);
int lastIn = 0;
int outputOffset = 0;
for (int y = 0; y < this.RowsPerBlock; y++)
{
for (int c = 0; c < this.channelCount; c++)
{
switch (this.pixelType)
{
case ExrPixelType.UnsignedInt:
{
int offsetT0 = lastIn;
lastIn += this.Width;
int offsetT1 = lastIn;
lastIn += this.Width;
int offsetT2 = lastIn;
lastIn += this.Width;
int offsetT3 = lastIn;
lastIn += this.Width;
uint pixel = 0;
for (int x = 0; x < this.Width; x++)
{
uint t0 = uncompressed[offsetT0];
uint t1 = uncompressed[offsetT1];
uint t2 = uncompressed[offsetT2];
uint t3 = uncompressed[offsetT3];
uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8) | t3;
pixel += diff;
outputBufferUint[outputOffset] = pixel;
offsetT0++;
offsetT1++;
offsetT2++;
offsetT3++;
outputOffset++;
}
break;
}
case ExrPixelType.Half:
{
int offsetT0 = lastIn;
lastIn += this.Width;
int offsetT1 = lastIn;
lastIn += this.Width;
uint pixel = 0;
for (int x = 0; x < this.Width; x++)
{
uint t0 = uncompressed[offsetT0];
uint t1 = uncompressed[offsetT1];
uint diff = (t0 << 8) | t1;
pixel += diff;
outputBufferHalf[outputOffset] = (ushort)pixel;
offsetT0++;
offsetT1++;
outputOffset++;
}
break;
}
case ExrPixelType.Float:
{
int offsetT0 = lastIn;
lastIn += this.Width;
int offsetT1 = lastIn;
lastIn += this.Width;
int offsetT2 = lastIn;
lastIn += this.Width;
uint pixel = 0;
for (int x = 0; x < this.Width; x++)
{
uint t0 = uncompressed[offsetT0];
uint t1 = uncompressed[offsetT1];
uint t2 = uncompressed[offsetT2];
uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8);
pixel += diff;
outputBufferFloat[outputOffset] = pixel;
offsetT0++;
offsetT1++;
offsetT2++;
outputOffset++;
}
break;
}
}
}
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

98
src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs

@ -0,0 +1,98 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
/// <summary>
/// Implementation of RLE decompressor for EXR images.
/// </summary>
internal class RunLengthExrCompression : ExrBaseDecompressor
{
private readonly IMemoryOwner<byte> tmpBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="RunLengthExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<byte> uncompressed = this.tmpBuffer.GetSpan();
int maxLength = (int)this.BytesPerBlock;
int offset = 0;
while (compressedBytes > 0)
{
byte nextByte = ReadNextByte(stream);
sbyte input = (sbyte)nextByte;
if (input < 0)
{
int count = -input;
compressedBytes -= (uint)(count + 1);
if ((maxLength -= count) < 0)
{
return;
}
for (int i = 0; i < count; i++)
{
uncompressed[offset + i] = ReadNextByte(stream);
}
offset += count;
}
else
{
int count = input;
byte value = ReadNextByte(stream);
compressedBytes -= 2;
if ((maxLength -= count + 1) < 0)
{
return;
}
for (int i = 0; i < count + 1; i++)
{
uncompressed[offset + i] = value;
}
offset += count + 1;
}
}
Reconstruct(uncompressed, this.BytesPerBlock);
Interleave(uncompressed, this.BytesPerBlock, buffer);
}
/// <summary>
/// Reads the next byte from the stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns>The next byte.</returns>
private static byte ReadNextByte(BufferedReadStream stream)
{
int nextByte = stream.ReadByte();
if (nextByte == -1)
{
ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to decompress RLE encoded EXR image!");
}
return (byte)nextByte;
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

42
src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs

@ -0,0 +1,42 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
/// <summary>
/// Implementation of zhe Zip decompressor for EXR image data.
/// </summary>
internal class ZipExrCompression : ExrBaseDecompressor
{
private readonly IMemoryOwner<byte> tmpBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="ZipExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per pixel row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<byte> uncompressed = this.tmpBuffer.GetSpan();
uint uncompressedBytes = (uint)buffer.Length;
int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes);
Reconstruct(uncompressed, (uint)totalRead);
Interleave(uncompressed, (uint)totalRead, buffer);
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

75
src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs

@ -0,0 +1,75 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression;
/// <summary>
/// Base class for EXR compression.
/// </summary>
internal abstract class ExrBaseCompression : IDisposable
{
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="ExrBaseCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The number of pixel rows per block.</param>
/// <param name="width">The number of pixels of a row.</param>
protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
{
this.Allocator = allocator;
this.BytesPerBlock = bytesPerBlock;
this.BytesPerRow = bytesPerRow;
this.RowsPerBlock = rowsPerBlock;
this.Width = width;
}
/// <summary>
/// Gets the memory allocator.
/// </summary>
protected MemoryAllocator Allocator { get; }
/// <summary>
/// Gets the bits per pixel.
/// </summary>
public int BitsPerPixel { get; }
/// <summary>
/// Gets the bytes per row.
/// </summary>
public uint BytesPerRow { get; }
/// <summary>
/// Gets the uncompressed bytes per block.
/// </summary>
public uint BytesPerBlock { get; }
/// <summary>
/// Gets the number of pixel rows per block.
/// </summary>
public uint RowsPerBlock { get; }
/// <summary>
/// Gets the image width.
/// </summary>
public int Width { get; }
/// <inheritdoc />
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.isDisposed = true;
this.Dispose(true);
}
protected abstract void Dispose(bool disposing);
}

112
src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs

@ -0,0 +1,112 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.IO.Compression;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression;
/// <summary>
/// The base EXR decompressor class.
/// </summary>
internal abstract class ExrBaseDecompressor : ExrBaseCompression
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrBaseDecompressor" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The number of pixels per row.</param>
protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
}
/// <summary>
/// Decompresses the specified stream.
/// </summary>
/// <param name="stream">The buffered stream to decompress.</param>
/// <param name="compressedBytes">The compressed bytes.</param>
/// <param name="buffer">The buffer to write the decompressed data to.</param>
public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer);
/// <summary>
/// Decompresses zip compressed data.
/// </summary>
/// <param name="stream">The buffered stream to decompress.</param>
/// <param name="compressedBytes">The compressed bytes.</param>
/// <param name="uncompressed">The buffer to write the uncompressed data to.</param>
/// <param name="uncompressedBytes">The uncompressed bytes.</param>
/// <returns>The total bytes read from the stream.</returns>
protected static int UndoZipCompression(BufferedReadStream stream, uint compressedBytes, Span<byte> uncompressed, uint uncompressedBytes)
{
long pos = stream.Position;
using ZlibInflateReader inflateStream = new(
stream,
() =>
{
int left = (int)(compressedBytes - (stream.Position - pos));
return left > 0 ? left : 0;
});
inflateStream.AllocateNewBytes((int)compressedBytes, true);
using DeflateStream dataStream = inflateStream.CompressedStream!;
int totalRead = 0;
while (totalRead < uncompressedBytes)
{
int bytesRead = dataStream.Read(uncompressed, totalRead, (int)uncompressedBytes - totalRead);
if (bytesRead <= 0)
{
break;
}
totalRead += bytesRead;
}
if (totalRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed EXR image data!");
}
return totalRead;
}
/// <summary>
/// Integrate over all differences to the previous value in order to
/// reconstruct sample values.
/// </summary>
/// <param name="buffer">The buffer with the data.</param>
/// <param name="unCompressedBytes">The un compressed bytes.</param>
protected static void Reconstruct(Span<byte> buffer, uint unCompressedBytes)
{
int offset = 0;
for (int i = 0; i < unCompressedBytes - 1; i++)
{
byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128));
buffer[offset + 1] = d;
offset++;
}
}
/// <summary>
/// Interleaves the input data.
/// </summary>
/// <param name="source">The source data.</param>
/// <param name="unCompressedBytes">The uncompressed bytes.</param>
/// <param name="output">The output to write to.</param>
protected static void Interleave(Span<byte> source, uint unCompressedBytes, Span<byte> output)
{
int sourceOffset = 0;
int offset0 = 0;
int offset1 = (int)((unCompressedBytes + 1) / 2);
while (sourceOffset < unCompressedBytes)
{
output[sourceOffset++] = source[offset0++];
output[sourceOffset++] = source[offset1++];
}
}
}

43
src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;
using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression;
/// <summary>
/// Factory class for creating a compressor for EXR image data.
/// </summary>
internal static class ExrCompressorFactory
{
/// <summary>
/// Creates the specified exr data compressor.
/// </summary>
/// <param name="method">The compression method.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="output">The output stream.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
/// <param name="compressionLevel">The deflate compression level.</param>
/// <returns>A compressor for EXR image data.</returns>
public static ExrBaseCompressor Create(
ExrCompression method,
MemoryAllocator allocator,
Stream output,
uint bytesPerBlock,
uint bytesPerRow,
uint rowsPerBlock,
int width,
DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) => method switch
{
ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel),
ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel),
_ => throw ExrThrowHelper.NotSupportedCompressor(method.ToString()),
};
}

45
src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression;
/// <summary>
/// The Factory class for creating a EXR data decompressor.
/// </summary>
internal static class ExrDecompressorFactory
{
/// <summary>
/// Creates a decomprssor for a specific EXR compression type.
/// </summary>
/// <param name="method">The compression method.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The width in pixels of the image.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The rows per block.</param>
/// <param name="channelCount">The number of image channels.</param>
/// <param name="pixelType">The pixel type.</param>
/// <returns>Decompressor for EXR image data.</returns>
public static ExrBaseDecompressor Create(
ExrCompression method,
MemoryAllocator memoryAllocator,
int width,
uint bytesPerBlock,
uint bytesPerRow,
uint rowsPerBlock,
int channelCount,
ExrPixelType pixelType) => method switch
{
ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount),
ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount, pixelType),
_ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)),
};
}

63
src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs

@ -0,0 +1,63 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr.Constants;
/// <summary>
/// Enumeration representing the compression formats defined by the EXR file-format.
/// </summary>
public enum ExrCompression
{
/// <summary>
/// Pixel data is not compressed.
/// </summary>
None = 0,
/// <summary>
/// Differences between horizontally adjacent pixels are run-length encoded.
/// This method is fast, and works well for images with large flat areas, but for photographic images,
/// the compressed file size is usually between 60 and 75 percent of the uncompressed size.
/// Compression is lossless.
/// </summary>
RunLengthEncoded = 1,
/// <summary>
/// Uses the open source zlib library for compression. Unlike ZIP compression, this operates one scan line at a time.
/// Compression is lossless.
/// </summary>
Zips = 2,
/// <summary>
/// Differences between horizontally adjacent pixels are compressed using the open source zlib library.
/// Unlike ZIPS compression, this operates in in blocks of 16 scan lines.
/// Compression is lossless.
/// </summary>
Zip = 3,
/// <summary>
/// A wavelet transform is applied to the pixel data, and the result is Huffman-encoded.
/// Compression is lossless.
/// </summary>
Piz = 4,
/// <summary>
/// After reducing 32-bit floating-point data to 24 bits by rounding, differences between horizontally adjacent pixels are compressed with zlib,
/// similar to ZIP. PXR24 compression preserves image channels of type HALF and UINT exactly, but the relative error of FLOAT data increases to about 3×10-5.
/// Compression is lossy.
/// </summary>
Pxr24 = 5,
/// <summary>
/// Channels of type HALF are split into blocks of four by four pixels or 32 bytes. Each block is then packed into 14 bytes,
/// reducing the data to 44 percent of their uncompressed size.
/// Compression is lossy.
/// </summary>
B44 = 6,
/// <summary>
/// Like B44, except for blocks of four by four pixels where all pixels have the same value, which are packed into 3 instead of 14 bytes.
/// For images with large uniform areas, B44A produces smaller files than B44 compression.
/// Compression is lossy.
/// </summary>
B44A = 7
}

30
src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr.Constants;
/// <summary>
/// This enum represents the type of pixel data in the EXR image.
/// </summary>
public enum ExrImageDataType
{
/// <summary>
/// The pixel data is unknown.
/// </summary>
Unknown = 0,
/// <summary>
/// The pixel data has 3 channels: red, green and blue.
/// </summary>
Rgb = 1,
/// <summary>
/// The pixel data has four channels: red, green, blue and a alpha channel.
/// </summary>
Rgba = 2,
/// <summary>
/// There is only one channel with the luminance.
/// </summary>
Gray = 3,
}

21
src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr.Constants;
/// <summary>
/// Enum for the differnt exr image type.
/// </summary>
internal enum ExrImageType
{
/// <summary>
/// The image data is stored in scan lines.
/// </summary>
ScanLine = 0,
/// <summary>
/// The image data is stored in tile.
/// This is not yet supported.
/// </summary>
Tiled = 1
}

25
src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr.Constants;
/// <summary>
/// Enum for the different scan line ordering.
/// </summary>
internal enum ExrLineOrder : byte
{
/// <summary>
/// The scan lines are written from top-to-bottom.
/// </summary>
IncreasingY = 0,
/// <summary>
/// The scan lines are written from bottom-to-top.
/// </summary>
DecreasingY = 1,
/// <summary>
/// The Scan lines are written in no particular oder.
/// </summary>
RandomY = 2
}

25
src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr.Constants;
/// <summary>
/// The different pixel formats for a OpenEXR image.
/// </summary>
public enum ExrPixelType
{
/// <summary>
/// unsigned int (32 bit).
/// </summary>
UnsignedInt = 0,
/// <summary>
/// half (16 bit floating point).
/// </summary>
Half = 1,
/// <summary>
/// float (32 bit floating point).
/// </summary>
Float = 2
}

43
src/ImageSharp/Formats/Exr/ExrAttribute.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Repressents an exr image attribute.
/// </summary>
[DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")]
internal class ExrAttribute
{
public static readonly ExrAttribute EmptyAttribute = new(string.Empty, string.Empty, 0);
/// <summary>
/// Initializes a new instance of the <see cref="ExrAttribute"/> class.
/// </summary>
/// <param name="name">The name of the attribute.</param>
/// <param name="type">The type of the attribute.</param>
/// <param name="length">The length in bytes.</param>
public ExrAttribute(string name, string type, int length)
{
this.Name = name;
this.Type = type;
this.Length = length;
}
/// <summary>
/// Gets the name of the attribute.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the type of the attribute.
/// </summary>
public string Type { get; }
/// <summary>
/// Gets the length in bytes of the attribute.
/// </summary>
public int Length { get; }
}

35
src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression;
internal abstract class ExrBaseCompressor : ExrBaseCompression
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrBaseCompressor"/> class.
/// </summary>
/// <param name="output">The output stream to write the compressed image to.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">Bytes per row block.</param>
/// <param name="bytesPerRow">Bytes per pixel row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The number of pixels per row.</param>
protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
=> this.Output = output;
/// <summary>
/// Gets the output stream to write the compressed image to.
/// </summary>
public Stream Output { get; }
/// <summary>
/// Compresses a block of rows of the image.
/// </summary>
/// <param name="rows">Image rows to compress.</param>
/// <param name="rowCount">The number of rows to compress.</param>
/// <returns>Number of bytes of of the compressed data.</returns>
public abstract uint CompressRowBlock(Span<byte> rows, int rowCount);
}

48
src/ImageSharp/Formats/Exr/ExrBox2i.cs

@ -0,0 +1,48 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Integer region definition.
/// </summary>
[DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")]
internal readonly struct ExrBox2i
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrBox2i"/> struct.
/// </summary>
/// <param name="xMin">The minimum x value.</param>
/// <param name="yMin">The minimum y value.</param>
/// <param name="xMax">The maximum x value.</param>
/// <param name="yMax">The maximum y value.</param>
public ExrBox2i(int xMin, int yMin, int xMax, int yMax)
{
this.XMin = xMin;
this.YMin = yMin;
this.XMax = xMax;
this.YMax = yMax;
}
/// <summary>
/// Gets the minimum x value.
/// </summary>
public int XMin { get; }
/// <summary>
/// Gets the minimum y value.
/// </summary>
public int YMin { get; }
/// <summary>
/// Gets the maximum x value.
/// </summary>
public int XMax { get; }
/// <summary>
/// Gets the maximum y value.
/// </summary>
public int YMax { get; }
}

60
src/ImageSharp/Formats/Exr/ExrChannelInfo.cs

@ -0,0 +1,60 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Exr.Constants;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Information about a pixel channel.
/// </summary>
[DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal readonly struct ExrChannelInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrChannelInfo" /> struct.
/// </summary>
/// <param name="channelName">Name of the channel.</param>
/// <param name="pixelType">The type of the pixel data.</param>
/// <param name="linear">Linear flag, possible values are 0 and 1.</param>
/// <param name="xSampling">X sampling.</param>
/// <param name="ySampling">Y sampling.</param>
public ExrChannelInfo(string channelName, ExrPixelType pixelType, byte linear, int xSampling, int ySampling)
{
this.ChannelName = channelName;
this.PixelType = pixelType;
this.Linear = linear;
this.XSampling = xSampling;
this.YSampling = ySampling;
}
/// <summary>
/// Gets the channel name.
/// </summary>
public string ChannelName { get; }
/// <summary>
/// Gets the type of the pixel data.
/// </summary>
public ExrPixelType PixelType { get; }
/// <summary>
/// Gets the linear flag. Hint to lossy compression methods that indicates whether
/// human perception of the quantity represented by this channel
/// is closer to linear or closer to logarithmic.
/// </summary>
public byte Linear { get; }
/// <summary>
/// Gets the x sampling value.
/// </summary>
public int XSampling { get; }
/// <summary>
/// Gets the y sampling value.
/// </summary>
public int YSampling { get; }
}

18
src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs

@ -0,0 +1,18 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the OpenExr format.
/// </summary>
public sealed class ExrConfigurationModule : IImageFormatConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder());
configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, ExrDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector());
}
}

82
src/ImageSharp/Formats/Exr/ExrConstants.cs

@ -0,0 +1,82 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Defines constants relating to OpenExr images.
/// </summary>
internal static class ExrConstants
{
/// <summary>
/// The list of mimetypes that equate to a OpenExr image.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/x-exr" };
/// <summary>
/// The list of file extensions that equate to a OpenExr image.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "exr" };
/// <summary>
/// The magick bytes identifying an OpenExr image.
/// </summary>
public static readonly int MagickBytes = 20000630;
/// <summary>
/// EXR attribute names.
/// </summary>
internal static class AttributeNames
{
public const string Channels = "channels";
public const string Compression = "compression";
public const string DataWindow = "dataWindow";
public const string DisplayWindow = "displayWindow";
public const string LineOrder = "lineOrder";
public const string PixelAspectRatio = "pixelAspectRatio";
public const string ScreenWindowCenter = "screenWindowCenter";
public const string ScreenWindowWidth = "screenWindowWidth";
public const string Tiles = "tiles";
public const string ChunkCount = "chunkCount";
}
/// <summary>
/// EXR attribute types.
/// </summary>
internal static class AttibuteTypes
{
public const string ChannelList = "chlist";
public const string Compression = "compression";
public const string Float = "float";
public const string LineOrder = "lineOrder";
public const string TwoFloat = "v2f";
public const string BoxInt = "box2i";
}
internal static class ChannelNames
{
public const string Red = "R";
public const string Green = "G";
public const string Blue = "B";
public const string Alpha = "A";
public const string Luminance = "Y";
}
}

48
src/ImageSharp/Formats/Exr/ExrDecoder.cs

@ -0,0 +1,48 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Image decoder for generating an image out of a OpenExr stream.
/// </summary>
public class ExrDecoder : ImageDecoder
{
private ExrDecoder()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static ExrDecoder Instance { get; } = new();
/// <inheritdoc/>
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
return new ExrDecoderCore(new ExrDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
}
/// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
ExrDecoderCore decoder = new(new ExrDecoderOptions { GeneralOptions = options });
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
}

1023
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

File diff suppressed because it is too large

13
src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs

@ -0,0 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Image decoder options for decoding OpenExr streams.
/// </summary>
public sealed class ExrDecoderOptions : ISpecializedDecoderOptions
{
/// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new();
}

29
src/ImageSharp/Formats/Exr/ExrEncoder.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Exr.Constants;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Image encoder for writing an image to a stream in the OpenExr Format.
/// </summary>
public sealed class ExrEncoder : ImageEncoder
{
/// <summary>
/// Gets or sets the pixel type of the image.
/// </summary>
public ExrPixelType? PixelType { get; set; }
/// <summary>
/// Gets the compression type to use.
/// </summary>
public ExrCompression? Compression { get; init; }
/// <inheritdoc />
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
ExrEncoderCore encoder = new(this, image.Configuration, image.Configuration.MemoryAllocator);
encoder.Encode(image, stream, cancellationToken);
}
}

710
src/ImageSharp/Formats/Exr/ExrEncoderCore.cs

@ -0,0 +1,710 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Exr.Compression;
using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Image encoder for writing an image to a stream in the OpenExr format.
/// </summary>
internal sealed class ExrEncoderCore
{
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[8];
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The global configuration.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The encoder with options.
/// </summary>
private readonly ExrEncoder encoder;
/// <summary>
/// The pixel type of the image.
/// </summary>
private ExrPixelType? pixelType;
/// <summary>
/// Initializes a new instance of the <see cref="ExrEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder with options.</param>
/// <param name="configuration">The configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
public ExrEncoderCore(ExrEncoder encoder, Configuration configuration, MemoryAllocator memoryAllocator)
{
this.configuration = configuration;
this.encoder = encoder;
this.memoryAllocator = memoryAllocator;
this.Compression = encoder.Compression ?? ExrCompression.None;
this.pixelType = encoder.PixelType;
}
/// <summary>
/// Gets or sets the compression implementation to use when encoding the image.
/// </summary>
internal ExrCompression Compression { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="ImageFrame{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;
ImageMetadata metadata = image.Metadata;
ExrMetadata exrMetadata = metadata.GetExrMetadata();
this.pixelType ??= exrMetadata.PixelType;
int width = image.Width;
int height = image.Height;
float aspectRatio = 1.0f;
ExrBox2i dataWindow = new(0, 0, width - 1, height - 1);
ExrBox2i displayWindow = new(0, 0, width - 1, height - 1);
ExrLineOrder lineOrder = ExrLineOrder.IncreasingY;
PointF screenWindowCenter = new(0.0f, 0.0f);
int screenWindowWidth = 1;
List<ExrChannelInfo> channels =
[
new(ExrConstants.ChannelNames.Alpha, this.pixelType.Value, 0, 1, 1),
new(ExrConstants.ChannelNames.Blue, this.pixelType.Value, 0, 1, 1),
new(ExrConstants.ChannelNames.Green, this.pixelType.Value, 0, 1, 1),
new(ExrConstants.ChannelNames.Red, this.pixelType.Value, 0, 1, 1),
];
ExrHeaderAttributes header = new(
channels,
this.Compression,
dataWindow,
displayWindow,
lineOrder,
aspectRatio,
screenWindowWidth,
screenWindowCenter);
// Write magick bytes.
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes);
stream.Write(this.buffer.AsSpan(0, 4));
// Version number.
this.buffer[0] = 2;
// Second, third and fourth bytes store info about the image, set all to default: zero.
this.buffer[1] = 0;
this.buffer[2] = 0;
this.buffer[3] = 0;
stream.Write(this.buffer.AsSpan(0, 4));
// Write EXR header.
this.WriteHeader(stream, header);
// Next is offsets table to each pixel row, which will be written after the pixel data was written.
ulong startOfRowOffsetData = (ulong)stream.Position;
stream.Position += 8 * height;
// Write pixel data.
switch (this.pixelType)
{
case ExrPixelType.Half:
case ExrPixelType.Float:
{
ulong[] rowOffsets = this.EncodeFloatingPointPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken);
stream.Position = (long)startOfRowOffsetData;
this.WriteRowOffsets(stream, height, rowOffsets);
break;
}
case ExrPixelType.UnsignedInt:
{
ulong[] rowOffsets = this.EncodeUnsignedIntPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken);
stream.Position = (long)startOfRowOffsetData;
this.WriteRowOffsets(stream, height, rowOffsets);
break;
}
}
}
/// <summary>
/// Encodes and writes pixel data with float pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="stream">The stream to write to.</param>
/// <param name="pixels">The pixel bufer.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="channels">The imagechannels.</param>
/// <param name="compression">The compression to use.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The array of pixel row offsets.</returns>
private ulong[] EncodeFloatingPointPixelData<TPixel>(
Stream stream,
Buffer2D<TPixel> pixels,
int width,
int height,
List<ExrChannelInfo> channels,
ExrCompression compression,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue)
{
throw new ImageFormatException("Image is too large to encode in EXR format.");
}
using IMemoryOwner<float> rgbBuffer = this.memoryAllocator.Allocate<float>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
Span<float> redBuffer = rgbBuffer.GetSpan()[..width];
Span<float> greenBuffer = rgbBuffer.GetSpan().Slice(width, width);
Span<float> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<float> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width);
ulong[] rowOffsets = new ulong[height];
for (uint y = 0; y < height; y += rowsPerBlock)
{
rowOffsets[y] = (ulong)stream.Position;
// Write row index.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y);
stream.Write(this.buffer.AsSpan(0, 4));
// At this point, it is not yet known how much bytes the compressed data will take up, keep stream position.
long pixelDataSizePos = stream.Position;
stream.Position = pixelDataSizePos + 4;
uint rowsInBlockCount = 0;
for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++)
{
Span<TPixel> pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex);
for (int x = 0; x < width; x++)
{
// OpenEXR stores RGB associated with alpha. Use the native vector domain so floating-point and HDR component
// ranges are preserved instead of being clamped through the scaled [0, 1] representation.
Vector4 vector4 = pixelRowSpan[x].ToAssociatedVector4();
redBuffer[x] = vector4.X;
greenBuffer[x] = vector4.Y;
blueBuffer[x] = vector4.Z;
alphaBuffer[x] = vector4.W;
}
// Write pixel data to row block buffer.
Span<byte> rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow);
switch (this.pixelType)
{
case ExrPixelType.Float:
WriteSingleRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer);
break;
case ExrPixelType.Half:
WriteHalfSingleRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer);
break;
}
rowsInBlockCount++;
}
// Write compressed pixel row data to the stream.
uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount);
long positionAfterPixelData = stream.Position;
// Write pixel row data size.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes);
stream.Position = pixelDataSizePos;
stream.Write(this.buffer.AsSpan(0, 4));
stream.Position = positionAfterPixelData;
cancellationToken.ThrowIfCancellationRequested();
}
return rowOffsets;
}
/// <summary>
/// Encodes and writes pixel data with the unsigned int pixel type to the stream.
/// </summary>
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="stream">The stream to write to.</param>
/// <param name="pixels">The pixel bufer.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="channels">The imagechannels.</param>
/// <param name="compression">The compression to use.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The array of pixel row offsets.</returns>
private ulong[] EncodeUnsignedIntPixelData<TPixel>(
Stream stream,
Buffer2D<TPixel> pixels,
int width,
int height,
List<ExrChannelInfo> channels,
ExrCompression compression,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue)
{
throw new ImageFormatException("Image is too large to encode in EXR format.");
}
using IMemoryOwner<uint> rgbBuffer = this.memoryAllocator.Allocate<uint>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
Span<uint> redBuffer = rgbBuffer.GetSpan()[..width];
Span<uint> greenBuffer = rgbBuffer.GetSpan().Slice(width, width);
Span<uint> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<uint> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width);
Rgba128 rgb = default;
ulong[] rowOffsets = new ulong[height];
for (uint y = 0; y < height; y += rowsPerBlock)
{
rowOffsets[y] = (ulong)stream.Position;
// Write row index.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y);
stream.Write(this.buffer.AsSpan(0, 4));
// At this point, it is not yet known how much bytes the compressed data will take up, keep stream position.
long pixelDataSizePos = stream.Position;
stream.Position = pixelDataSizePos + 4;
uint rowsInBlockCount = 0;
for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++)
{
Span<TPixel> pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex);
for (int x = 0; x < width; x++)
{
// OpenEXR channels use associated alpha; the native vector conversion also preserves the integer channel range.
Vector4 vector4 = pixelRowSpan[x].ToAssociatedVector4();
rgb = Rgba128.FromVector4(vector4);
redBuffer[x] = rgb.R;
greenBuffer[x] = rgb.G;
blueBuffer[x] = rgb.B;
alphaBuffer[x] = rgb.A;
}
// Write row data to row block buffer.
Span<byte> rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow);
WriteUnsignedIntRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer);
rowsInBlockCount++;
}
// Write pixel row data compressed to the stream.
uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount);
long positionAfterPixelData = stream.Position;
// Write pixel row data size.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes);
stream.Position = pixelDataSizePos;
stream.Write(this.buffer.AsSpan(0, 4));
stream.Position = positionAfterPixelData;
cancellationToken.ThrowIfCancellationRequested();
}
return rowOffsets;
}
/// <summary>
/// Writes the image header to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="header">The header.</param>
private void WriteHeader(Stream stream, ExrHeaderAttributes header)
{
this.WriteChannels(stream, header.Channels);
this.WriteCompression(stream, header.Compression);
this.WriteDataWindow(stream, header.DataWindow);
this.WriteDisplayWindow(stream, header.DisplayWindow);
this.WritePixelAspectRatio(stream, header.AspectRatio);
this.WriteLineOrder(stream, header.LineOrder);
this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter);
this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth);
stream.WriteByte(0);
}
/// <summary>
/// Writes a row of pixels with the FLOAT pixel type to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="width">The width of a row in pixels.</param>
/// <param name="alphaBuffer">The alpha channel buffer.</param>
/// <param name="blueBuffer">The blue channel buffer.</param>
/// <param name="greenBuffer">The green channel buffer.</param>
/// <param name="redBuffer">The red channel buffer.</param>
private static void WriteSingleRow(Span<byte> buffer, int width, Span<float> alphaBuffer, Span<float> blueBuffer, Span<float> greenBuffer, Span<float> redBuffer)
{
int offset = 0;
for (int x = 0; x < width; x++)
{
WriteSingleToBuffer(buffer.Slice(offset, 4), alphaBuffer[x]);
offset += 4;
}
for (int x = 0; x < width; x++)
{
WriteSingleToBuffer(buffer.Slice(offset, 4), blueBuffer[x]);
offset += 4;
}
for (int x = 0; x < width; x++)
{
WriteSingleToBuffer(buffer.Slice(offset, 4), greenBuffer[x]);
offset += 4;
}
for (int x = 0; x < width; x++)
{
WriteSingleToBuffer(buffer.Slice(offset, 4), redBuffer[x]);
offset += 4;
}
}
/// <summary>
/// Writes a row of pixels with the HALF pixel type to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="width">The width of a row in pixels.</param>
/// <param name="alphaBuffer">The alpha channel buffer.</param>
/// <param name="blueBuffer">The blue channel buffer.</param>
/// <param name="greenBuffer">The green channel buffer.</param>
/// <param name="redBuffer">The red channel buffer.</param>
private static void WriteHalfSingleRow(Span<byte> buffer, int width, Span<float> alphaBuffer, Span<float> blueBuffer, Span<float> greenBuffer, Span<float> redBuffer)
{
int offset = 0;
for (int x = 0; x < width; x++)
{
WriteHalfSingleToBuffer(buffer.Slice(offset, 2), alphaBuffer[x]);
offset += 2;
}
for (int x = 0; x < width; x++)
{
WriteHalfSingleToBuffer(buffer.Slice(offset, 2), blueBuffer[x]);
offset += 2;
}
for (int x = 0; x < width; x++)
{
WriteHalfSingleToBuffer(buffer.Slice(offset, 2), greenBuffer[x]);
offset += 2;
}
for (int x = 0; x < width; x++)
{
WriteHalfSingleToBuffer(buffer.Slice(offset, 2), redBuffer[x]);
offset += 2;
}
}
/// <summary>
/// Writes a row of pixels with unsigned int pixel data to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="width">The width of the row in pixels.</param>
/// <param name="alphaBuffer">The alpha channel buffer.</param>
/// <param name="blueBuffer">The blue channel buffer.</param>
/// <param name="greenBuffer">The green channel buffer.</param>
/// <param name="redBuffer">The red channel buffer.</param>
private static void WriteUnsignedIntRow(Span<byte> buffer, int width, Span<uint> alphaBuffer, Span<uint> blueBuffer, Span<uint> greenBuffer, Span<uint> redBuffer)
{
int offset = 0;
for (int x = 0; x < width; x++)
{
WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), alphaBuffer[x]);
offset += 4;
}
for (int x = 0; x < width; x++)
{
WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), blueBuffer[x]);
offset += 4;
}
for (int x = 0; x < width; x++)
{
WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), greenBuffer[x]);
offset += 4;
}
for (int x = 0; x < width; x++)
{
WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), redBuffer[x]);
offset += 4;
}
}
/// <summary>
/// Writes the row offsets to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="height">The height in pixels of the image.</param>
/// <param name="rowOffsets">The row offsets.</param>
private void WriteRowOffsets(Stream stream, int height, ulong[] rowOffsets)
{
for (int i = 0; i < height; i++)
{
BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, rowOffsets[i]);
stream.Write(this.buffer);
}
}
/// <summary>
/// Writes the channel infos to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="channels">The channels.</param>
private void WriteChannels(Stream stream, IList<ExrChannelInfo> channels)
{
int attributeSize = 0;
foreach (ExrChannelInfo channelInfo in channels)
{
attributeSize += channelInfo.ChannelName.Length + 1;
attributeSize += 16;
}
// Last zero byte.
attributeSize++;
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Channels, ExrConstants.AttibuteTypes.ChannelList, attributeSize);
foreach (ExrChannelInfo channelInfo in channels)
{
this.WriteChannelInfo(stream, channelInfo);
}
// Last byte should be zero.
stream.WriteByte(0);
}
/// <summary>
/// Writes info about a single channel to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="channelInfo">The channel information.</param>
private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo)
{
WriteString(stream, channelInfo.ChannelName);
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType);
stream.Write(this.buffer.AsSpan(0, 4));
stream.WriteByte(channelInfo.Linear);
// Next 3 bytes are reserved and will set to zero.
stream.WriteByte(0);
stream.WriteByte(0);
stream.WriteByte(0);
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling);
stream.Write(this.buffer.AsSpan(0, 4));
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling);
stream.Write(this.buffer.AsSpan(0, 4));
}
/// <summary>
/// Writes the compression type to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="compression">The compression type.</param>
private void WriteCompression(Stream stream, ExrCompression compression)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1);
stream.WriteByte((byte)compression);
}
/// <summary>
/// Writes the pixel aspect ratio to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="aspectRatio">The aspect ratio.</param>
private void WritePixelAspectRatio(Stream stream, float aspectRatio)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.PixelAspectRatio, ExrConstants.AttibuteTypes.Float, 4);
this.WriteSingle(stream, aspectRatio);
}
/// <summary>
/// Writes the line order to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="lineOrder">The line order.</param>
private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.LineOrder, ExrConstants.AttibuteTypes.LineOrder, 1);
stream.WriteByte((byte)lineOrder);
}
/// <summary>
/// Writes the screen window center to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="screenWindowCenter">The screen window center.</param>
private void WriteScreenWindowCenter(Stream stream, PointF screenWindowCenter)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowCenter, ExrConstants.AttibuteTypes.TwoFloat, 8);
this.WriteSingle(stream, screenWindowCenter.X);
this.WriteSingle(stream, screenWindowCenter.Y);
}
/// <summary>
/// Writes the screen width to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="screenWindowWidth">Width of the screen window.</param>
private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowWidth, ExrConstants.AttibuteTypes.Float, 4);
this.WriteSingle(stream, screenWindowWidth);
}
/// <summary>
/// Writes the data window to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="dataWindow">The data window.</param>
private void WriteDataWindow(Stream stream, ExrBox2i dataWindow)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DataWindow, ExrConstants.AttibuteTypes.BoxInt, 16);
this.WriteBoxInteger(stream, dataWindow);
}
/// <summary>
/// Writes the display window to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="displayWindow">The display window.</param>
private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow)
{
this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DisplayWindow, ExrConstants.AttibuteTypes.BoxInt, 16);
this.WriteBoxInteger(stream, displayWindow);
}
/// <summary>
/// Writes attribute information to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="type">The type of the attribute.</param>
/// <param name="size">The size in bytes of the attribute.</param>
private void WriteAttributeInformation(Stream stream, string name, string type, int size)
{
// Write attribute name.
WriteString(stream, name);
// Write attribute type.
WriteString(stream, type);
// Write attribute size.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size);
stream.Write(this.buffer.AsSpan(0, 4));
}
/// <summary>
/// Writes a string to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="str">The string to write.</param>
private static void WriteString(Stream stream, string str)
{
foreach (char c in str)
{
stream.WriteByte((byte)c);
}
// Write termination byte.
stream.WriteByte(0);
}
/// <summary>
/// Writes box struct with xmin, xmax, ymin and y max to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="box">The box to write.</param>
private void WriteBoxInteger(Stream stream, ExrBox2i box)
{
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin);
stream.Write(this.buffer.AsSpan(0, 4));
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin);
stream.Write(this.buffer.AsSpan(0, 4));
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax);
stream.Write(this.buffer.AsSpan(0, 4));
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax);
stream.Write(this.buffer.AsSpan(0, 4));
}
/// <summary>
/// Writes 32 bit float value to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="value">The float value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private unsafe void WriteSingle(Stream stream, float value)
{
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value);
stream.Write(this.buffer.AsSpan(0, 4));
}
/// <summary>
/// Writes a 32 bit float value to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="value">The float value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private static unsafe void WriteSingleToBuffer(Span<byte> buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value);
/// <summary>
/// Writes a 16 bit float value to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="value">The float value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private static void WriteHalfSingleToBuffer(Span<byte> buffer, float value)
{
ushort valueAsShort = HalfTypeHelper.Pack(value);
BinaryPrimitives.WriteUInt16LittleEndian(buffer, valueAsShort);
}
/// <summary>
/// Writes one unsigned int to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="value">The uint value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private static void WriteUnsignedIntToBuffer(Span<byte> buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
}

34
src/ImageSharp/Formats/Exr/ExrFormat.cs

@ -0,0 +1,34 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the OpenExr format.
/// </summary>
public sealed class ExrFormat : IImageFormat<ExrMetadata>
{
private ExrFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static ExrFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "EXR";
/// <inheritdoc/>
public string DefaultMimeType => "image/x-exr";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => ExrConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => ExrConstants.FileExtensions;
/// <inheritdoc/>
public ExrMetadata CreateDefaultFormatMetadata() => new();
}

108
src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs

@ -0,0 +1,108 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Exr.Constants;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// The header of an EXR image.
/// <see href="https://openexr.com/en/latest/TechnicalIntroduction.html#header"/>
/// </summary>
internal class ExrHeaderAttributes
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrHeaderAttributes" /> class.
/// </summary>
/// <param name="channels">The image channels.</param>
/// <param name="compression">The compression used.</param>
/// <param name="dataWindow">The data window.</param>
/// <param name="displayWindow">The display window.</param>
/// <param name="lineOrder">The line order.</param>
/// <param name="aspectRatio">The aspect ratio.</param>
/// <param name="screenWindowWidth">Width of the screen window.</param>
/// <param name="screenWindowCenter">The screen window center.</param>
/// <param name="tileXSize">Size of the tile in x dimension.</param>
/// <param name="tileYSize">Size of the tile in y dimension.</param>
/// <param name="chunkCount">The chunk count.</param>
public ExrHeaderAttributes(
IList<ExrChannelInfo> channels,
ExrCompression compression,
ExrBox2i dataWindow,
ExrBox2i displayWindow,
ExrLineOrder lineOrder,
float aspectRatio,
float screenWindowWidth,
PointF screenWindowCenter,
uint? tileXSize = null,
uint? tileYSize = null,
int? chunkCount = null)
{
this.Channels = channels;
this.Compression = compression;
this.DataWindow = dataWindow;
this.DisplayWindow = displayWindow;
this.LineOrder = lineOrder;
this.AspectRatio = aspectRatio;
this.ScreenWindowWidth = screenWindowWidth;
this.ScreenWindowCenter = screenWindowCenter;
this.TileXSize = tileXSize;
this.TileYSize = tileYSize;
this.ChunkCount = chunkCount;
}
/// <summary>
/// Gets or sets a description of the image channels stored in the file.
/// </summary>
public IList<ExrChannelInfo> Channels { get; set; }
/// <summary>
/// Gets or sets the compression method applied to the pixel data of all channels in the file.
/// </summary>
public ExrCompression Compression { get; set; }
/// <summary>
/// Gets or sets the image’s data window.
/// </summary>
public ExrBox2i DataWindow { get; set; }
/// <summary>
/// Gets or sets the image’s display window.
/// </summary>
public ExrBox2i DisplayWindow { get; set; }
/// <summary>
/// Gets or sets in what order the scan lines in the file are stored in the file (increasing Y, decreasing Y, or, for tiled images, also random Y).
/// </summary>
public ExrLineOrder LineOrder { get; set; }
/// <summary>
/// Gets or sets the aspect ratio of the image.
/// </summary>
public float AspectRatio { get; set; }
/// <summary>
/// Gets or sets the screen width.
/// </summary>
public float ScreenWindowWidth { get; set; }
/// <summary>
/// Gets or sets the screen window center.
/// </summary>
public PointF ScreenWindowCenter { get; set; }
/// <summary>
/// Gets or sets the number of horizontal tiles.
/// </summary>
public uint? TileXSize { get; set; }
/// <summary>
/// Gets or sets the number of vertical tiles.
/// </summary>
public uint? TileYSize { get; set; }
/// <summary>
/// Gets or sets the chunk count. Indicates the number of chunks in this part. Required if the multipart bit (12) is set.
/// </summary>
public int? ChunkCount { get; set; }
}

34
src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs

@ -0,0 +1,34 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Detects OpenExr file headers.
/// </summary>
public sealed class ExrImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize => 4;
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
{
if (header.Length >= this.HeaderSize)
{
int fileTypeMarker = BinaryPrimitives.ReadInt32LittleEndian(header);
return fileTypeMarker == ExrConstants.MagickBytes;
}
return false;
}
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
format = this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null;
return format != null;
}
}

157
src/ImageSharp/Formats/Exr/ExrMetadata.cs

@ -0,0 +1,157 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Provides OpenExr specific metadata information for the image.
/// </summary>
public class ExrMetadata : IFormatMetadata<ExrMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="ExrMetadata"/> class.
/// </summary>
public ExrMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExrMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private ExrMetadata(ExrMetadata other) => this.PixelType = other.PixelType;
/// <summary>
/// Gets or sets the pixel format.
/// </summary>
public ExrPixelType PixelType { get; set; } = ExrPixelType.Half;
/// <summary>
/// Gets or sets the image data type, either RGB, RGBA or gray.
/// </summary>
public ExrImageDataType ImageDataType { get; set; } = ExrImageDataType.Unknown;
/// <summary>
/// Gets or sets the compression method.
/// </summary>
public ExrCompression Compression { get; set; } = ExrCompression.None;
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
bool hasAlpha = this.ImageDataType is ExrImageDataType.Rgba;
int bitsPerComponent = 32;
int bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3;
if (this.PixelType == ExrPixelType.Half)
{
bitsPerComponent = 16;
bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3;
}
// OpenEXR defines RGBA color channels as premultiplied by alpha, so expose the association stored by the format.
PixelAlphaRepresentation alpha = hasAlpha ? PixelAlphaRepresentation.Associated : PixelAlphaRepresentation.None;
PixelColorType color = PixelColorType.RGB;
int componentsCount = 0;
int[] precision = [];
switch (this.ImageDataType)
{
case ExrImageDataType.Rgb:
color = PixelColorType.RGB;
componentsCount = 3;
precision = new int[componentsCount];
precision[0] = bitsPerComponent;
precision[1] = bitsPerComponent;
precision[2] = bitsPerComponent;
break;
case ExrImageDataType.Rgba:
color = PixelColorType.RGB | PixelColorType.Alpha;
componentsCount = 4;
precision = new int[componentsCount];
precision[0] = bitsPerComponent;
precision[1] = bitsPerComponent;
precision[2] = bitsPerComponent;
precision[3] = bitsPerComponent;
break;
case ExrImageDataType.Gray:
color = PixelColorType.Luminance;
componentsCount = 1;
precision = new int[componentsCount];
precision[0] = bitsPerComponent;
break;
}
PixelComponentInfo info = PixelComponentInfo.Create(componentsCount, bitsPerPixel, precision);
return new PixelTypeInfo(bitsPerPixel)
{
AlphaRepresentation = alpha,
ComponentInfo = info,
ColorType = color
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
{
EncodingType type = this.Compression is ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Pxr24
? EncodingType.Lossy
: EncodingType.Lossless;
return new()
{
EncodingType = type,
PixelTypeInfo = this.GetPixelTypeInfo()
};
}
/// <inheritdoc/>
public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
{
PixelTypeInfo pixelTypeInfo = metadata.PixelTypeInfo;
PixelComponentInfo? info = pixelTypeInfo.ComponentInfo;
PixelColorType colorType = pixelTypeInfo.ColorType;
int bitsPerComponent = info?.GetMaximumComponentPrecision()
?? (pixelTypeInfo.BitsPerPixel <= 16 ? 16 : 32);
int componentCount = info?.ComponentCount ?? 0;
ExrImageDataType imageDataType = colorType switch
{
PixelColorType.Luminance => ExrImageDataType.Gray,
PixelColorType.RGB or PixelColorType.BGR => ExrImageDataType.Rgb,
PixelColorType.RGB | PixelColorType.Alpha
or PixelColorType.BGR | PixelColorType.Alpha
or PixelColorType.Luminance | PixelColorType.Alpha => ExrImageDataType.Rgba,
_ => componentCount switch
{
>= 4 => ExrImageDataType.Rgba,
>= 3 => ExrImageDataType.Rgb,
1 => ExrImageDataType.Gray,
_ => ExrImageDataType.Unknown,
}
};
return new()
{
PixelType = bitsPerComponent <= 16 ? ExrPixelType.Half : ExrPixelType.Float,
ImageDataType = imageDataType,
};
}
/// <inheritdoc/>
ExrMetadata IDeepCloneable<ExrMetadata>.DeepClone() => new(this);
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new ExrMetadata(this);
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}
}

33
src/ImageSharp/Formats/Exr/ExrThrowHelper.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Formats.Exr;
/// <summary>
/// Cold path optimizations for throwing exr format based exceptions.
/// </summary>
internal static class ExrThrowHelper
{
[DoesNotReturn]
public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}");
[DoesNotReturn]
public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage);
[DoesNotReturn]
public static void ThrowNotSupportedVersion() => throw new NotSupportedException("Unsupported EXR version");
[DoesNotReturn]
public static void ThrowNotSupported(string msg) => throw new NotSupportedException(msg);
[DoesNotReturn]
public static void ThrowInvalidImageHeader() => throw new InvalidImageContentException("Invalid EXR image header");
[DoesNotReturn]
public static void ThrowInvalidImageHeader(string msg) => throw new InvalidImageContentException(msg);
[DoesNotReturn]
public static Exception NotSupportedCompressor(string compressionType) => throw new NotSupportedException($"Not supported encoder compression method: {compressionType}");
}

52
src/ImageSharp/Formats/Exr/ExrUtils.cs

@ -0,0 +1,52 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Exr.Constants;
namespace SixLabors.ImageSharp.Formats.Exr;
internal static class ExrUtils
{
/// <summary>
/// Calcualtes the required bytes for a pixel row.
/// </summary>
/// <param name="channels">The image channels array.</param>
/// <param name="width">The width in pixels of a row.</param>
/// <returns>The number of bytes per row.</returns>
public static ulong CalculateBytesPerRow(IList<ExrChannelInfo> channels, uint width)
{
ulong bytesPerRow = 0;
foreach (ExrChannelInfo channelInfo in channels)
{
if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)
|| channelInfo.ChannelName.Equals("R", StringComparison.Ordinal)
|| channelInfo.ChannelName.Equals("G", StringComparison.Ordinal)
|| channelInfo.ChannelName.Equals("B", StringComparison.Ordinal)
|| channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal))
{
if (channelInfo.PixelType == ExrPixelType.Half)
{
bytesPerRow += 2UL * width;
}
else
{
bytesPerRow += 4UL * width;
}
}
}
return bytesPerRow;
}
/// <summary>
/// Determines how many pixel rows there are in a block. This varies depending on the compression used.
/// </summary>
/// <param name="compression">The compression used.</param>
/// <returns>Pixel rows in a block.</returns>
public static uint RowsPerBlock(ExrCompression compression) => compression switch
{
ExrCompression.Zip or ExrCompression.Pxr24 => 16,
ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Piz => 32,
_ => 1,
};
}

4
src/ImageSharp/Formats/Exr/README.md

@ -0,0 +1,4 @@
### Some useful links for documentation about the OpenEXR format:
- [Technical Introduction](https://openexr.readthedocs.io/en/latest/TechnicalIntroduction.html)
- [OpenExr file layout](https://openexr.readthedocs.io/en/latest/OpenEXRFileLayout.html)

171
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -146,10 +146,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
this.ReadComments(stream);
this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
break;
case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension(stream);
this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
break;
case GifConstants.PlainTextLabel:
SkipBlock(stream); // Not supported by any known decoder.
@ -226,10 +226,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
this.ReadComments(stream);
this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream));
break;
case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension(stream);
this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream));
break;
case GifConstants.PlainTextLabel:
SkipBlock(stream); // Not supported by any known decoder.
@ -266,6 +266,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifThrowHelper.ThrowNoHeader();
}
// Ignoring a malformed ancillary extension must not let identify succeed for a file
// that never contained any readable image frame data.
if (previousFrame is null)
{
GifThrowHelper.ThrowNoData();
}
return new ImageInfo(
new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata,
@ -331,51 +338,128 @@ internal sealed class GifDecoderCore : ImageDecoderCore
private void ReadApplicationExtension(BufferedReadStream stream)
{
int appLength = stream.ReadByte();
if (appLength == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
if (appLength != GifConstants.ApplicationBlockSize)
{
this.ThrowOrIgnoreNonStrictSegmentError($"Gif application extension length '{appLength}' is invalid");
SkipBlock(stream, appLength);
return;
}
// If the length is 11 then it's a valid extension and most likely
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
long position = stream.Position;
if (appLength == GifConstants.ApplicationBlockSize)
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
if (bytesRead != GifConstants.ApplicationBlockSize)
{
stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp && !this.skipMetadata)
{
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
}
else
{
// Reset the stream position and continue.
stream.Position = position;
SkipBlock(stream, appLength);
}
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
return;
}
bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp)
{
this.ReadXmpApplicationExtension(stream, position, appLength);
return;
}
int subBlockSize = stream.ReadByte();
int subBlockSize = stream.ReadByte();
if (subBlockSize == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
stream.Skip(1); // Skip the terminator.
return;
}
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
this.ReadNetscapeApplicationExtension(stream);
return;
}
// Could be something else not supported yet.
// Skip the subblock and terminator.
SkipBlock(stream, subBlockSize);
}
/// <summary>
/// Reads the GIF XMP application extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
/// <param name="appLength">The application block length.</param>
private void ReadXmpApplicationExtension(BufferedReadStream stream, long applicationPosition, int appLength)
{
if (this.skipMetadata)
{
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
return;
}
// Could be something else not supported yet.
// Skip the subblock and terminator.
SkipBlock(stream, subBlockSize);
bool completed = false;
this.ExecuteAncillarySegmentAction(
() =>
{
this.ReadXmpApplicationExtensionData(stream, applicationPosition, appLength);
completed = true;
});
if (!completed)
{
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
}
}
/// <summary>
/// Reads the GIF XMP application extension data.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="applicationPosition">The stream position where the application identifier begins.</param>
/// <param name="appLength">The application block length.</param>
private void ReadXmpApplicationExtensionData(BufferedReadStream stream, long applicationPosition, int appLength)
{
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata!.XmpProfile = new XmpProfile(extension.Data);
return;
}
SkipBlock(stream, appLength); // Not supported by any known decoder.
stream.Position = applicationPosition;
SkipBlock(stream, appLength);
}
/// <summary>
/// Reads the GIF NETSCAPE looping application extension.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtension(BufferedReadStream stream) =>
this.ExecuteAncillarySegmentAction(() => this.ReadNetscapeApplicationExtensionData(stream));
/// <summary>
/// Reads the GIF NETSCAPE looping application extension data.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
int terminator = stream.ReadByte();
if (terminator == -1)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
}
/// <summary>
@ -428,7 +512,12 @@ internal sealed class GifDecoderCore : ImageDecoderCore
using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length);
Span<byte> commentsSpan = commentsBuffer.GetSpan();
stream.Read(commentsSpan);
int bytesRead = stream.Read(commentsSpan);
if (bytesRead != length)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
}
string commentPart = GifConstants.Encoding.GetString(commentsSpan);
stringBuilder.Append(commentPart);
}
@ -468,7 +557,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
rawColorTable = this.currentLocalColorTable!.GetSpan()[..length];
rawColorTable = this.currentLocalColorTable.GetSpan()[..length];
}
else if (this.globalColorTable != null)
{
@ -901,7 +990,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
this.backgroundColorIndex = index;
this.gifMetadata.BackgroundColorIndex = index;
ReadOnlyMemory<Color>? globalColorTable = this.gifMetadata.GlobalColorTable;
if (globalColorTable.HasValue && index < globalColorTable.Value.Length)
{
this.gifMetadata.BackgroundColor = globalColorTable.Value.Span[index];
}
}
private unsafe struct ScratchBuffer

115
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -45,14 +45,6 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
/// </summary>
@ -76,7 +68,6 @@ internal sealed class GifEncoderCore
this.skipMetadata = encoder.SkipMetadata;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
this.transparentColorMode = encoder.TransparentColorMode;
}
@ -113,11 +104,21 @@ internal sealed class GifEncoderCore
TransparentColorMode mode = this.transparentColorMode;
// Create a new quantizer options instance augmenting the transparent color mode to match the encoder.
QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new QuantizerOptions()).DeepClone(o => o.TransparentColorMode = mode);
QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new QuantizerOptions()).DeepClone(o =>
{
o.TransparentColorMode = mode;
// Animated GIF delta frames can use one padded color-table index as transparency.
// Express that through MaxColors so custom quantizers receive the same budget.
if (image.Frames.Count > 1 && o.MaxColors == QuantizerConstants.MaxColors)
{
o.MaxColors = QuantizerConstants.MaxColors - 1;
}
});
if (globalQuantizer is null)
{
// Is this a gif with color information. If so use that, otherwise use octree.
// Is this a gif with color information. If so use that, otherwise use the adaptive hexadecatree quantizer.
if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{
int ti = GetTransparentIndex(quantized, frameMetadata);
@ -132,12 +133,12 @@ internal sealed class GifEncoderCore
}
else
{
globalQuantizer = new OctreeQuantizer(options);
globalQuantizer = new HexadecatreeQuantizer(options);
}
}
else
{
globalQuantizer = new OctreeQuantizer(options);
globalQuantizer = new HexadecatreeQuantizer(options);
}
}
@ -145,6 +146,11 @@ internal sealed class GifEncoderCore
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
ImageFrame<TPixel> encodingFrame = image.Frames.RootFrame;
// This color is encoded as the logical-screen background index and is also
// used when de-duplicating frames that restore to the GIF background.
Color backgroundColor = this.encoder.BackgroundColor ?? gifMetadata.BackgroundColor ?? Color.Transparent;
byte backgroundIndex = 0;
if (useGlobalTableForFirstFrame)
{
using IQuantizer<TPixel> firstFrameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options);
@ -158,6 +164,8 @@ internal sealed class GifEncoderCore
}
quantized = firstFrameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
backgroundIndex = firstFrameQuantizer.GetQuantizedColor(backgroundPixel, out _);
}
else
{
@ -184,8 +192,6 @@ internal sealed class GifEncoderCore
frameMetadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
byte backgroundIndex = GetBackgroundIndex(quantized, gifMetadata, this.backgroundColor);
// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteLogicalScreenDescriptor(image.Metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
@ -222,6 +228,7 @@ internal sealed class GifEncoderCore
image,
globalQuantizer,
globalFrameQuantizer,
backgroundColor,
transparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
@ -253,6 +260,7 @@ internal sealed class GifEncoderCore
Image<TPixel> image,
IQuantizer globalQuantizer,
PaletteQuantizer<TPixel> globalFrameQuantizer,
Color backgroundColor,
int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken)
@ -284,6 +292,7 @@ internal sealed class GifEncoderCore
globalFrameQuantizer,
useLocal,
gifMetadata,
backgroundColor,
previousDisposalMode);
previousFrame = currentFrame;
@ -303,7 +312,7 @@ internal sealed class GifEncoderCore
this.WriteGraphicalControlExtension(metadata, stream);
Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer;
Rectangle interest = indices.FullRectangle();
Rectangle interest = indices.Bounds;
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (metadata.ColorTableMode == FrameColorTableMode.Local);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
@ -327,6 +336,7 @@ internal sealed class GifEncoderCore
PaletteQuantizer<TPixel> globalFrameQuantizer,
bool useLocal,
GifFrameMetadata metadata,
Color backgroundColor,
FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -347,11 +357,14 @@ internal sealed class GifEncoderCore
previous.Metadata.GetGifMetadata().DisposalMode;
Color background = !useTransparency && disposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
? backgroundColor
: Color.Transparent;
// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) =
// Pixels matching the previous frame are replaced with the transparent placeholder.
// When the entire frame matches there is no captured difference, but every pixel is
// still a placeholder, so a transparent index is always required for additional frames.
(_, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
this.configuration,
previous,
@ -368,7 +381,7 @@ internal sealed class GifEncoderCore
bounds,
metadata,
useLocal,
difference,
true,
transparencyIndex,
background);
@ -393,7 +406,7 @@ internal sealed class GifEncoderCore
Rectangle bounds,
GifFrameMetadata metadata,
bool useLocal,
bool hasDuplicates,
bool requiresTransparency,
int transparencyIndex,
Color transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
@ -407,9 +420,11 @@ internal sealed class GifEncoderCore
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
ReadOnlyMemory<Color> palette = metadata.LocalColorTable.Value;
if (hasDuplicates && !metadata.HasTransparency)
if (requiresTransparency && !metadata.HasTransparency)
{
// Duplicates were captured but the metadata does not have transparency.
// The frame was de-duplicated against the previous frame, replacing matching
// pixels with the transparent placeholder, but the metadata does not yet carry
// a transparent index. Reserve one so those pixels encode as transparent.
metadata.HasTransparency = true;
if (palette.Length < 256)
@ -470,7 +485,7 @@ internal sealed class GifEncoderCore
metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
if (hasDuplicates)
if (requiresTransparency)
{
metadata.HasTransparency = true;
}
@ -482,11 +497,19 @@ internal sealed class GifEncoderCore
// Individual frames, though using the shared palette, can use a different transparent index
// to represent transparency.
// A difference was captured but the metadata does not have transparency.
if (hasDuplicates && !metadata.HasTransparency)
// The frame was de-duplicated against the previous frame, replacing matching pixels with
// the transparent placeholder. When the whole frame matches there is no captured difference,
// yet every pixel is still a placeholder, so we must always reserve a transparent index here;
// otherwise the placeholder pixels are matched to the nearest (typically darkest) palette color.
if (requiresTransparency && !metadata.HasTransparency)
{
metadata.HasTransparency = true;
transparencyIndex = globalFrameQuantizer.Palette.Length;
// Normally we pad one index past the palette so the (out of range) value is treated as
// transparent by decoders without growing the color table. A full 256-color palette leaves
// no room to pad within the 8-bit index space (index 256 wraps to 0 when written and exceeds
// the maximum GIF bit depth), so reuse the last in-range index for transparency instead.
transparencyIndex = Math.Min(globalFrameQuantizer.Palette.Length, byte.MaxValue);
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
@ -518,7 +541,7 @@ internal sealed class GifEncoderCore
int index = -1;
if (quantized != null)
{
TPixel transparentPixel = TPixel.FromScaledVector4(Vector4.Zero);
TPixel transparentPixel = TPixel.FromUnassociatedScaledVector4(Vector4.Zero);
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
// Transparent pixels are much more likely to be found at the end of a palette.
@ -534,44 +557,6 @@ internal sealed class GifEncoderCore
return index;
}
/// <summary>
/// Returns the index of the background color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The gif metadata</param>
/// <param name="background">The background color to match.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="byte"/> index of the background color.</returns>
private static byte GetBackgroundIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifMetadata metadata, Color? background)
where TPixel : unmanaged, IPixel<TPixel>
{
int match = -1;
if (quantized != null)
{
if (background.HasValue)
{
TPixel backgroundPixel = background.Value.ToPixel<TPixel>();
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
{
if (!backgroundPixel.Equals(palette[i]))
{
continue;
}
match = i;
break;
}
}
else if (metadata.BackgroundColorIndex < quantized.Palette.Length)
{
match = metadata.BackgroundColorIndex;
}
}
return ClampIndex(match);
}
/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>

8
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -26,7 +26,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
{
this.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode;
this.BackgroundColorIndex = other.BackgroundColorIndex;
this.BackgroundColor = other.BackgroundColor;
if (other.GlobalColorTable?.Length > 0)
{
@ -59,10 +59,9 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
public ReadOnlyMemory<Color>? GlobalColorTable { get; set; }
/// <summary>
/// Gets or sets the index at the <see cref="GlobalColorTable"/> for the background color.
/// The background color is the color used for those pixels on the screen that are not covered by an image.
/// Gets or sets the background color used for pixels on the screen that are not covered by an image.
/// </summary>
public byte BackgroundColorIndex { get; set; }
public Color? BackgroundColor { get; set; }
/// <summary>
/// Gets or sets the collection of comments about the graphics, credits, descriptions or any
@ -101,6 +100,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
{
AnimateRootFrame = true,
ColorTableMode = this.ColorTableMode,
BackgroundColor = this.BackgroundColor ?? Color.Transparent,
PixelTypeInfo = this.GetPixelTypeInfo(),
RepeatCount = this.RepeatCount,
};

17
src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

@ -30,7 +30,11 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
/// <returns>The XMP metadata</returns>
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator)
{
byte[] xmpBytes = ReadXmpData(stream, allocator);
byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated);
if (!terminated)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif XMP data");
}
// Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF
int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0
@ -71,7 +75,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
return this.ContentLength;
}
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator)
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated)
{
using ChunkedMemoryStream bytes = new(allocator);
@ -83,8 +87,15 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
while (true)
{
int b = stream.ReadByte();
if (b <= 0)
if (b == 0)
{
terminated = true;
return bytes.ToArray();
}
if (b < 0)
{
terminated = false;
return bytes.ToArray();
}

68
src/ImageSharp/Formats/ImageDecoderCore.cs

@ -33,6 +33,74 @@ internal abstract class ImageDecoderCore
/// </summary>
public Size Dimensions { get; protected internal set; }
/// <summary>
/// Executes a known ancillary segment parsing action using the configured integrity policy.
/// </summary>
/// <param name="action">The action.</param>
protected void ExecuteAncillarySegmentAction(Action action)
{
if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict)
{
action();
return;
}
try
{
action();
}
catch (Exception ex) when (ex
is ImageFormatException
or InvalidIccProfileException
or InvalidImageContentException
or InvalidOperationException
or NotSupportedException)
{
// Intentionally ignored in non-strict segment integrity modes.
}
}
/// <summary>
/// Executes a known image data segment parsing action using the configured integrity policy.
/// </summary>
/// <param name="action">The action.</param>
protected void ExecuteImageDataSegmentAction(Action action)
{
if (this.Options.SegmentIntegrityHandling is not SegmentIntegrityHandling.IgnoreImageData)
{
action();
return;
}
try
{
action();
}
catch (Exception ex) when (ex
is ImageFormatException
or InvalidIccProfileException
or InvalidImageContentException
or InvalidOperationException
or NotSupportedException)
{
// Intentionally ignored when image data integrity handling is set to IgnoreImageData.
}
}
/// <summary>
/// Throws unless the decoder is running in a non-strict segment integrity mode.
/// Use this only from within <see cref="ExecuteAncillarySegmentAction"/> when local control flow
/// must continue after the error.
/// </summary>
/// <param name="message">The exception message.</param>
protected void ThrowOrIgnoreNonStrictSegmentError(string message)
{
if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict)
{
throw new InvalidImageContentException(message);
}
}
/// <summary>
/// Reads the raw image information from the specified stream.
/// </summary>

326
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs

@ -14,7 +14,7 @@ internal partial struct Block8x8F
public void ScaledCopyFrom(ref float areaOrigin, int areaStride) =>
CopyFrom1x1Scale(ref Unsafe.As<float, byte>(ref areaOrigin), ref Unsafe.As<Block8x8F, byte>(ref this), areaStride);
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(InliningOptions.ColdPath)]
public void ScaledCopyTo(ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale)
{
if (horizontalScale == 1 && verticalScale == 1)
@ -29,7 +29,50 @@ internal partial struct Block8x8F
return;
}
// TODO: Optimize: implement all cases with scale-specific, loopless code!
if (horizontalScale == 2 && verticalScale == 1)
{
this.CopyTo2x1Scale(ref areaOrigin, (uint)areaStride);
return;
}
if (horizontalScale == 1 && verticalScale == 2)
{
this.CopyTo1x2Scale(ref areaOrigin, (uint)areaStride);
return;
}
if (horizontalScale == 4 && verticalScale == 1)
{
this.CopyTo4x1Scale(ref areaOrigin, (uint)areaStride);
return;
}
if (horizontalScale == 4 && verticalScale == 2)
{
this.CopyTo4x2Scale(ref areaOrigin, (uint)areaStride);
return;
}
if (horizontalScale == 1 && verticalScale == 4)
{
this.CopyTo1x4Scale(ref areaOrigin, (uint)areaStride);
return;
}
if (horizontalScale == 2 && verticalScale == 4)
{
this.CopyTo2x4Scale(ref areaOrigin, (uint)areaStride);
return;
}
if (horizontalScale == 4 && verticalScale == 4)
{
this.CopyTo4x4Scale(ref areaOrigin, (uint)areaStride);
return;
}
// The common 1x, 2x, and 4x integral scales are specialized above.
// Uncommon legal factor-3 scales use the generic fallback.
this.CopyArbitraryScale(ref areaOrigin, (uint)areaStride, (uint)horizontalScale, (uint)verticalScale);
}
@ -85,6 +128,285 @@ internal partial struct Block8x8F
}
}
/// <summary>
/// Copies the full 8x8 block into the destination buffer while doubling only the horizontal axis.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyTo2x1Scale(ref float areaOrigin, uint areaStride)
{
ref Vector4 sourceBase = ref this.V0L;
WidenRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 4u, 4u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 5u, 5u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 6u, 6u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 7u, 7u, areaStride);
}
/// <summary>
/// Copies the full 8x8 block into the destination buffer while doubling only the vertical axis.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyTo1x2Scale(ref float areaOrigin, uint areaStride)
{
ref Vector4 sourceBase = ref this.V0L;
CopyRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 4u, 8u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 4u, 9u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 5u, 10u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 5u, 11u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 6u, 12u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 6u, 13u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 7u, 14u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 7u, 15u, areaStride);
}
/// <summary>
/// Copies the full 8x8 block into the destination buffer while quadrupling only the horizontal axis.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyTo4x1Scale(ref float areaOrigin, uint areaStride)
{
ref Vector4 sourceBase = ref this.V0L;
ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 4u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 5u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 6u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 7u, areaStride);
}
/// <summary>
/// Copies the full 8x8 block into the destination buffer while quadrupling horizontally and doubling vertically.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyTo4x2Scale(ref float areaOrigin, uint areaStride)
{
ref Vector4 sourceBase = ref this.V0L;
ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 8u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 9u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 10u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 11u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 12u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 13u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 14u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 15u, areaStride);
}
/// <summary>
/// Copies the full 8x8 block into the destination buffer while quadrupling only the vertical axis.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyTo1x4Scale(ref float areaOrigin, uint areaStride)
{
ref Vector4 sourceBase = ref this.V0L;
CopyRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride);
CopyRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride);
}
/// <summary>
/// Copies the full 8x8 block into the destination buffer while doubling horizontally and quadrupling vertically.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyTo2x4Scale(ref float areaOrigin, uint areaStride)
{
ref Vector4 sourceBase = ref this.V0L;
WidenRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride);
WidenRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride);
}
/// <summary>
/// Copies the full 8x8 block into the destination buffer while quadrupling both axes.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private void CopyTo4x4Scale(ref float areaOrigin, uint areaStride)
{
ref Vector4 sourceBase = ref this.V0L;
ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride);
ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride);
}
/// <summary>
/// Copies one eight-sample row from the full block to the destination row.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private static void CopyRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride)
{
ref Vector4 source = ref Unsafe.Add(ref sourceBase, sourceRow * 2u);
ref Vector4 dest = ref Unsafe.As<float, Vector4>(ref Unsafe.Add(ref areaOrigin, destRow * areaStride));
dest = source;
Unsafe.Add(ref dest, 1u) = Unsafe.Add(ref source, 1u);
}
/// <summary>
/// Expands one eight-sample row to sixteen samples by duplicating each source value horizontally.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private static void WidenRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride)
{
ref Vector4 sourceLeft = ref Unsafe.Add(ref sourceBase, sourceRow * 2u);
ref Vector4 sourceRight = ref Unsafe.Add(ref sourceLeft, 1u);
ref Vector4 dest = ref Unsafe.As<float, Vector4>(ref Unsafe.Add(ref areaOrigin, destRow * areaStride));
Vector4 xyLeft = new(sourceLeft.X);
xyLeft.Z = sourceLeft.Y;
xyLeft.W = sourceLeft.Y;
Vector4 zwLeft = new(sourceLeft.Z);
zwLeft.Z = sourceLeft.W;
zwLeft.W = sourceLeft.W;
Vector4 xyRight = new(sourceRight.X);
xyRight.Z = sourceRight.Y;
xyRight.W = sourceRight.Y;
Vector4 zwRight = new(sourceRight.Z);
zwRight.Z = sourceRight.W;
zwRight.W = sourceRight.W;
dest = xyLeft;
Unsafe.Add(ref dest, 1u) = zwLeft;
Unsafe.Add(ref dest, 2u) = xyRight;
Unsafe.Add(ref dest, 3u) = zwRight;
}
/// <summary>
/// Expands one eight-sample row to thirty-two samples by duplicating each source value four times horizontally.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private static void ExpandRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride)
{
ref Vector4 sourceLeft = ref Unsafe.Add(ref sourceBase, sourceRow * 2u);
ref Vector4 sourceRight = ref Unsafe.Add(ref sourceLeft, 1u);
ref Vector4 dest = ref Unsafe.As<float, Vector4>(ref Unsafe.Add(ref areaOrigin, destRow * areaStride));
dest = new Vector4(sourceLeft.X);
Unsafe.Add(ref dest, 1u) = new Vector4(sourceLeft.Y);
Unsafe.Add(ref dest, 2u) = new Vector4(sourceLeft.Z);
Unsafe.Add(ref dest, 3u) = new Vector4(sourceLeft.W);
Unsafe.Add(ref dest, 4u) = new Vector4(sourceRight.X);
Unsafe.Add(ref dest, 5u) = new Vector4(sourceRight.Y);
Unsafe.Add(ref dest, 6u) = new Vector4(sourceRight.Z);
Unsafe.Add(ref dest, 7u) = new Vector4(sourceRight.W);
}
[MethodImpl(InliningOptions.ColdPath)]
private void CopyArbitraryScale(ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale)
{

40
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs

@ -13,31 +13,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
internal partial struct Block8x8F
{
/// <summary>
/// <see cref="Vector128{Single}"/> version of <see cref="NormalizeColorsInPlace(float)"/> and <see cref="RoundInPlace()"/>.
/// <see cref="Vector128{Single}"/> version of <see cref="NormalizeColorsInPlace(float)"/>.
/// </summary>
/// <param name="maximum">The maximum value to normalize to.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void NormalizeColorsAndRoundInPlaceVector128(float maximum)
public void NormalizeColorsInPlaceVector128(float maximum)
{
Vector128<float> max = Vector128.Create(maximum);
Vector128<float> off = Vector128.Ceiling(max * .5F);
this.V0L = NormalizeAndRoundVector128(this.V0L.AsVector128(), off, max).AsVector4();
this.V0R = NormalizeAndRoundVector128(this.V0R.AsVector128(), off, max).AsVector4();
this.V1L = NormalizeAndRoundVector128(this.V1L.AsVector128(), off, max).AsVector4();
this.V1R = NormalizeAndRoundVector128(this.V1R.AsVector128(), off, max).AsVector4();
this.V2L = NormalizeAndRoundVector128(this.V2L.AsVector128(), off, max).AsVector4();
this.V2R = NormalizeAndRoundVector128(this.V2R.AsVector128(), off, max).AsVector4();
this.V3L = NormalizeAndRoundVector128(this.V3L.AsVector128(), off, max).AsVector4();
this.V3R = NormalizeAndRoundVector128(this.V3R.AsVector128(), off, max).AsVector4();
this.V4L = NormalizeAndRoundVector128(this.V4L.AsVector128(), off, max).AsVector4();
this.V4R = NormalizeAndRoundVector128(this.V4R.AsVector128(), off, max).AsVector4();
this.V5L = NormalizeAndRoundVector128(this.V5L.AsVector128(), off, max).AsVector4();
this.V5R = NormalizeAndRoundVector128(this.V5R.AsVector128(), off, max).AsVector4();
this.V6L = NormalizeAndRoundVector128(this.V6L.AsVector128(), off, max).AsVector4();
this.V6R = NormalizeAndRoundVector128(this.V6R.AsVector128(), off, max).AsVector4();
this.V7L = NormalizeAndRoundVector128(this.V7L.AsVector128(), off, max).AsVector4();
this.V7R = NormalizeAndRoundVector128(this.V7R.AsVector128(), off, max).AsVector4();
this.V0L = NormalizeVector128(this.V0L.AsVector128(), off, max).AsVector4();
this.V0R = NormalizeVector128(this.V0R.AsVector128(), off, max).AsVector4();
this.V1L = NormalizeVector128(this.V1L.AsVector128(), off, max).AsVector4();
this.V1R = NormalizeVector128(this.V1R.AsVector128(), off, max).AsVector4();
this.V2L = NormalizeVector128(this.V2L.AsVector128(), off, max).AsVector4();
this.V2R = NormalizeVector128(this.V2R.AsVector128(), off, max).AsVector4();
this.V3L = NormalizeVector128(this.V3L.AsVector128(), off, max).AsVector4();
this.V3R = NormalizeVector128(this.V3R.AsVector128(), off, max).AsVector4();
this.V4L = NormalizeVector128(this.V4L.AsVector128(), off, max).AsVector4();
this.V4R = NormalizeVector128(this.V4R.AsVector128(), off, max).AsVector4();
this.V5L = NormalizeVector128(this.V5L.AsVector128(), off, max).AsVector4();
this.V5R = NormalizeVector128(this.V5R.AsVector128(), off, max).AsVector4();
this.V6L = NormalizeVector128(this.V6L.AsVector128(), off, max).AsVector4();
this.V6R = NormalizeVector128(this.V6R.AsVector128(), off, max).AsVector4();
this.V7L = NormalizeVector128(this.V7L.AsVector128(), off, max).AsVector4();
this.V7R = NormalizeVector128(this.V7R.AsVector128(), off, max).AsVector4();
}
/// <summary>
@ -71,8 +71,8 @@ internal partial struct Block8x8F
}
[MethodImpl(InliningOptions.ShortMethod)]
private static Vector128<float> NormalizeAndRoundVector128(Vector128<float> value, Vector128<float> off, Vector128<float> max)
=> Vector128_.RoundToNearestInteger(Vector128_.Clamp(value + off, Vector128<float>.Zero, max));
private static Vector128<float> NormalizeVector128(Vector128<float> value, Vector128<float> off, Vector128<float> max)
=> Vector128_.Clamp(value + off, Vector128<float>.Zero, max);
private static void MultiplyIntoInt16Vector128(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest)
{

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save