diff --git a/.gitattributes b/.gitattributes
index f7bd4d061..031420e53 100644
--- a/.gitattributes
+++ b/.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
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..5f9f69435
--- /dev/null
+++ b/.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.
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index ff3d5c7b0..634b904ae 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.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
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index 07ce0408f..00f489b40 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.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
diff --git a/.gitignore b/.gitignore
index fadf36964..a8d2917be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -227,3 +227,5 @@ artifacts/
#lfs
hooks/**
lfs/**
+
+.dotnet
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..814e3ce26
--- /dev/null
+++ b/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.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..5f08a449a
--- /dev/null
+++ b/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.
diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 000000000..b621d2652
--- /dev/null
+++ b/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.
diff --git a/ImageSharp.sln b/ImageSharp.sln
index 7ccd92c07..7e392eaa9 100644
--- a/ImageSharp.sln
+++ b/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}
diff --git a/README.md b/README.md
index dc3073479..4d4375dbf 100644
--- a/README.md
+++ b/README.md
@@ -10,16 +10,14 @@ SixLabors.ImageSharp
[](https://github.com/SixLabors/ImageSharp/actions)
[](https://codecov.io/gh/SixLabors/ImageSharp)
[](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
-[](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)
-### **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.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..8b2e1df86
--- /dev/null
+++ b/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.
diff --git a/shared-infrastructure b/shared-infrastructure
index a1d3ac204..74b7f32b8 160000
--- a/shared-infrastructure
+++ b/shared-infrastructure
@@ -1 +1 @@
-Subproject commit a1d3ac20494631e3cc13132897573796b0e4ee6d
+Subproject commit 74b7f32b8e41fdf8fe2f3eda54fd5a82ebbedfbc
diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs
index a451e111d..e2c4258e2 100644
--- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs
+++ b/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);
+ ///
+ /// Accepts a to implement a double-dispatch pattern in order to
+ /// apply pixel-specific operations on non-generic instances
+ ///
+ /// The source image frame.
+ /// The image visitor.
+ public static void AcceptVisitor(this ImageFrame source, IImageFrameVisitor visitor)
+ => source.Accept(visitor);
+
///
/// Gets the representation of the pixels as a containing the backing pixel data of the image
/// stored in row major order, as a list of contiguous blocks in the source image's pixel format.
diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs
index fef49bffd..2d893e406 100644
--- a/src/ImageSharp/Advanced/AotCompilerTools.cs
+++ b/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
///
/// 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();
Seed();
+ Seed();
Seed();
+ Seed();
Seed();
Seed();
Seed();
+ Seed();
Seed();
Seed();
Seed();
@@ -95,16 +101,23 @@ internal static class AotCompilerTools
Seed();
Seed();
Seed();
+ Seed();
Seed();
Seed();
+ Seed();
Seed();
Seed();
Seed();
Seed();
Seed();
+ Seed();
Seed();
+ Seed();
Seed();
+ Seed();
Seed();
+ Seed();
+ Seed();
Seed();
Seed();
Seed();
@@ -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.");
}
+ ///
+ /// Seeds the modern .NET AOT compiler with bulk pixel operations for every built-in pixel format.
+ ///
+ ///
+ /// This method is used for AOT code generation only. Do not call it at runtime.
+ ///
+ [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();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ AotCompilePixelOperations();
+ }
+ 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.");
+ }
+
///
/// Seeds the compiler using the given pixel format.
///
@@ -127,6 +201,7 @@ internal static class AotCompilerTools
{
// This is we actually call all the individual methods you need to seed.
AotCompileImage();
+ AotCompilePixelOperations();
AotCompileImageProcessingContextFactory();
AotCompileImageEncoderInternals();
AotCompileImageDecoderInternals();
@@ -147,6 +222,68 @@ internal static class AotCompilerTools
// TODO: Do the discovery work to figure out what works and what doesn't.
}
+ ///
+ /// Seeds the selected methods required by Mono WASM AOT.
+ ///
+ /// The pixel format.
+ [Preserve]
+ private static void AotCompilePixelOperations()
+ where TPixel : unmanaged, IPixel
+ {
+ // 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 operations = PixelOperations.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);
+ }
+
///
/// This method pre-seeds the for a given pixel format in the AoT compiler.
///
@@ -158,10 +295,13 @@ internal static class AotCompilerTools
Image img = default;
img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
@@ -172,16 +312,23 @@ internal static class AotCompilerTools
img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
+ img.CloneAs(default);
+ img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
img.CloneAs(default);
@@ -208,7 +355,10 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel
{
default(BmpEncoderCore).Encode(default, default, default);
+ default(CurEncoderCore).Encode(default, default, default);
+ default(ExrEncoderCore).Encode(default, default, default);
default(GifEncoderCore).Encode(default, default, default);
+ default(IcoEncoderCore).Encode(default, default, default);
default(JpegEncoderCore).Encode(default, default, default);
default(PbmEncoderCore).Encode(default, default, default);
default(PngEncoderCore).Encode(default, default, default);
@@ -227,7 +377,10 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel
{
default(BmpDecoderCore).Decode(default, default, default);
+ default(CurDecoderCore).Decode(default, default, default);
+ default(ExrDecoderCore).Decode(default, default, default);
default(GifDecoderCore).Decode(default, default, default);
+ default(IcoDecoderCore).Decode(default, default, default);
default(JpegDecoderCore).Decode(default, default, default);
default(PbmDecoderCore).Decode(default, default, default);
default(PngDecoderCore).Decode(default, default, default);
@@ -247,10 +400,14 @@ internal static class AotCompilerTools
{
AotCompileImageEncoder();
AotCompileImageEncoder();
+ AotCompileImageEncoder();
+ AotCompileImageEncoder();
AotCompileImageEncoder();
+ AotCompileImageEncoder();
AotCompileImageEncoder();
AotCompileImageEncoder();
AotCompileImageEncoder();
+ AotCompileImageEncoder();
AotCompileImageEncoder();
AotCompileImageEncoder();
}
@@ -265,10 +422,14 @@ internal static class AotCompilerTools
{
AotCompileImageDecoder();
AotCompileImageDecoder();
+ AotCompileImageDecoder();
+ AotCompileImageDecoder();
AotCompileImageDecoder();
+ AotCompileImageDecoder();
AotCompileImageDecoder();
AotCompileImageDecoder();
AotCompileImageDecoder();
+ AotCompileImageDecoder();
AotCompileImageDecoder();
AotCompileImageDecoder();
}
@@ -336,6 +497,7 @@ internal static class AotCompilerTools
AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
+ AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
@@ -368,11 +530,13 @@ internal static class AotCompilerTools
AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
+ AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
+ AotCompileImageProcessor();
AotCompileImageProcessor();
AotCompileImageProcessor();
@@ -479,7 +643,7 @@ internal static class AotCompilerTools
private static void AotCompileQuantizers()
where TPixel : unmanaged, IPixel
{
- AotCompileQuantizer();
+ AotCompileQuantizer();
AotCompileQuantizer();
AotCompileQuantizer();
AotCompileQuantizer();
@@ -523,10 +687,8 @@ internal static class AotCompilerTools
private static void AotCompilePixelMaps()
where TPixel : unmanaged, IPixel
{
- default(EuclideanPixelMap).GetClosestColor(default, out _);
default(EuclideanPixelMap).GetClosestColor(default, out _);
default(EuclideanPixelMap).GetClosestColor(default, out _);
- default(EuclideanPixelMap).GetClosestColor(default, out _);
}
///
@@ -551,8 +713,8 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel
where TDither : struct, IDither
{
- OctreeQuantizer octree = default;
- default(TDither).ApplyQuantizationDither, TPixel>(ref octree, default, default, default);
+ HexadecatreeQuantizer hexadecatree = default;
+ default(TDither).ApplyQuantizationDither, TPixel>(ref hexadecatree, default, default, default);
PaletteQuantizer palette = default;
default(TDither).ApplyQuantizationDither, TPixel>(ref palette, default, default, default);
diff --git a/src/ImageSharp/Advanced/IImageFrameVisitor.cs b/src/ImageSharp/Advanced/IImageFrameVisitor.cs
new file mode 100644
index 000000000..f2522110d
--- /dev/null
+++ b/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;
+
+///
+/// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations
+/// on non-generic instances.
+///
+public interface IImageFrameVisitor
+{
+ ///
+ /// Provides a pixel-specific implementation for a given operation.
+ ///
+ /// The image frame.
+ /// The pixel type.
+ public void Visit(ImageFrame frame)
+ where TPixel : unmanaged, IPixel;
+}
diff --git a/src/ImageSharp/Advanced/IImageVisitor.cs b/src/ImageSharp/Advanced/IImageVisitor.cs
index 5e8a4e451..aee690967 100644
--- a/src/ImageSharp/Advanced/IImageVisitor.cs
+++ b/src/ImageSharp/Advanced/IImageVisitor.cs
@@ -16,7 +16,7 @@ public interface IImageVisitor
///
/// The image.
/// The pixel type.
- void Visit(Image image)
+ public void Visit(Image image)
where TPixel : unmanaged, IPixel;
}
@@ -33,6 +33,6 @@ public interface IImageVisitorAsync
/// The token to monitor for cancellation requests.
/// The pixel type.
/// A representing the asynchronous operation.
- Task VisitAsync(Image image, CancellationToken cancellationToken)
+ public Task VisitAsync(Image image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel;
}
diff --git a/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs b/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
index 8b46fc5c3..dfaec640b 100644
--- a/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
+++ b/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
@@ -15,12 +15,12 @@ public interface IRowOperation
///
/// The bounds of the operation.
/// The required buffer length.
- int GetRequiredBufferLength(Rectangle bounds);
+ public int GetRequiredBufferLength(Rectangle bounds);
///
/// Invokes the method passing the row and a buffer.
///
/// The row y coordinate.
/// The contiguous region of memory.
- void Invoke(int y, Span span);
+ public void Invoke(int y, Span span);
}
diff --git a/src/ImageSharp/Advanced/ParallelExecutionSettings.cs b/src/ImageSharp/Advanced/ParallelExecutionSettings.cs
index fd9692f9a..cf86d094d 100644
--- a/src/ImageSharp/Advanced/ParallelExecutionSettings.cs
+++ b/src/ImageSharp/Advanced/ParallelExecutionSettings.cs
@@ -18,7 +18,10 @@ public readonly struct ParallelExecutionSettings
///
/// Initializes a new instance of the struct.
///
- /// The value used for initializing when using TPL.
+ ///
+ /// The value used for initializing when using TPL.
+ /// If set to -1, there is no limit on the number of concurrently running operations.
+ ///
/// The value for .
/// The .
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
///
/// Initializes a new instance of the struct.
///
- /// The value used for initializing when using TPL.
+ ///
+ /// The value used for initializing when using TPL.
+ /// If set to -1, there is no limit on the number of concurrently running operations.
+ ///
/// The .
public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator)
: this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator)
@@ -58,6 +64,7 @@ public readonly struct ParallelExecutionSettings
///
/// Gets the value used for initializing when using TPL.
+ /// A value of -1 means there is no limit on the number of concurrently running operations.
///
public int MaxDegreeOfParallelism { get; }
@@ -86,12 +93,10 @@ public readonly struct ParallelExecutionSettings
}
///
- /// Get the default for a
+ /// Get the default for a
///
/// The .
/// The .
public static ParallelExecutionSettings FromConfiguration(Configuration configuration)
- {
- return new ParallelExecutionSettings(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator);
- }
+ => new(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator);
}
diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.cs b/src/ImageSharp/Advanced/ParallelRowIterator.cs
index b878f9ec0..f404326bc 100644
--- a/src/ImageSharp/Advanced/ParallelRowIterator.cs
+++ b/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 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 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 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 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);
+ ///
+ /// Creates the for the current iteration.
+ ///
+ /// The execution settings.
+ /// The number of row partitions to execute.
+ /// The instance.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static ParallelOptions CreateParallelOptions(in ParallelExecutionSettings parallelSettings, int numOfSteps)
+ => new() { MaxDegreeOfParallelism = parallelSettings.MaxDegreeOfParallelism == -1 ? -1 : numOfSteps };
+
+ ///
+ /// Calculates the number of row partitions to execute for the given region.
+ ///
+ /// The width of the region.
+ /// The height of the region.
+ /// The execution settings.
+ /// The number of row partitions to execute.
+ [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(
diff --git a/src/ImageSharp/Color/Color.WernerPalette.cs b/src/ImageSharp/Color/Color.WernerPalette.cs
index 583c71379..f6b625625 100644
--- a/src/ImageSharp/Color/Color.WernerPalette.cs
+++ b/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
];
}
diff --git a/src/ImageSharp/Color/Color.cs b/src/ImageSharp/Color/Color.cs
index dd248d488..85e300233 100644
--- a/src/ImageSharp/Color/Color.cs
+++ b/src/ImageSharp/Color/Color.cs
@@ -21,29 +21,44 @@ public readonly partial struct Color : IEquatable
{
private readonly Vector4 data;
private readonly IPixel? boxedHighPrecisionPixel;
+ private readonly bool isAssociated;
+ private readonly bool dataIsAssociated;
///
/// Initializes a new instance of the struct.
///
/// The containing the color information.
+ /// The alpha representation exposed by the color.
+ /// The alpha representation of .
[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;
}
///
/// Initializes a new instance of the struct.
///
/// The pixel containing color information.
+ /// The alpha representation of .
[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;
}
+ ///
+ /// Gets the alpha representation used by this color's scaled vector.
+ ///
+ public PixelAlphaRepresentation AlphaRepresentation
+ => this.isAssociated ? PixelAlphaRepresentation.Associated : PixelAlphaRepresentation.Unassociated;
+
///
/// Checks whether two structures are equal.
///
@@ -80,21 +95,46 @@ public readonly partial struct Color : IEquatable
{
// 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);
}
///
/// Creates a from a generic scaled .
///
- /// The vector to load the pixel from.
+ /// The unassociated vector to load the color from.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Color FromScaledVector(Vector4 source)
+ => new(source, PixelAlphaRepresentation.Unassociated, PixelAlphaRepresentation.Unassociated);
+
+ ///
+ /// Creates a from a generic scaled with the specified alpha representation.
+ ///
+ /// The vector to load the color from.
+ /// The alpha representation of .
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Color FromScaledVector(Vector4 source) => new(source);
+ public static Color FromScaledVector(Vector4 source, PixelAlphaRepresentation alphaRepresentation)
+ => new(source, alphaRepresentation, alphaRepresentation);
///
/// Bulk converts a span of generic scaled to a span of .
@@ -103,11 +143,23 @@ public readonly partial struct Color : IEquatable
/// The destination color span.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromScaledVector(ReadOnlySpan source, Span destination)
+ => FromScaledVector(source, destination, PixelAlphaRepresentation.Unassociated);
+
+ ///
+ /// Bulk converts a span of generic scaled values with the specified alpha representation
+ /// to a span of values.
+ ///
+ /// The source vector span.
+ /// The destination color span.
+ /// The alpha representation of the source vectors.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void FromScaledVector(ReadOnlySpan source, Span 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
// 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
/// Alters the alpha channel of the color, returning a new instance.
///
/// The new value of alpha [0..1].
- /// The color having it's alpha channel altered.
+ /// The color having its alpha channel altered.
[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);
}
///
@@ -296,9 +366,7 @@ public readonly partial struct Color : IEquatable
[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();
uint hexOrder = format switch
{
@@ -327,29 +395,76 @@ public readonly partial struct Color : IEquatable
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);
}
///
- /// Expands the color into a generic ("scaled") representation
- /// with values scaled and clamped between 0 and 1.
+ /// Expands the color into a generic ("scaled") representation,
+ /// preserving the , with values scaled and clamped between
+ /// 0 and 1.
/// The vector components are typically expanded in least to greatest significance order.
///
/// The .
[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;
+ }
+
+ ///
+ /// Expands the color into a generic ("scaled") using the specified alpha representation,
+ /// with values scaled and clamped between 0 and 1.
+ ///
+ ///
+ /// The alpha representation to apply. returns color components
+ /// multiplied by alpha; other representations return color components independent of alpha.
+ ///
+ /// The .
+ [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;
}
///
@@ -377,10 +492,11 @@ public readonly partial struct Color : IEquatable
{
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;
}
///
@@ -392,10 +508,10 @@ public readonly partial struct Color : IEquatable
{
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);
}
///
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
index fd99fb446..7f08a7a9b 100644
--- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
+++ b/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 source)
{
- if (Vector.IsHardwareAccelerated && Vector.IsSupported && Vector.Count >= source.Length * 4)
- {
- // SIMD loop
- int i = 0;
- int simdBatchSize = Vector.Count / 4; // Number of Vector4 elements per SIMD batch
- for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
- {
- // Load the vector from source span
- Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i]));
-
- v = Vector.Max(v, Vector.Zero);
-
- // Write the vector to the destination span
- Unsafe.WriteUnaligned(ref Unsafe.As(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 values = MemoryMarshal.Cast(source);
+ TensorPrimitives_.Max(values, 0F, values);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -708,39 +682,9 @@ internal static class ColorProfileConverterExtensionsIcc
private static void LabToLab(Span source, Span destination, [ConstantExpected] float scale)
{
- if (Vector.IsHardwareAccelerated && Vector.IsSupported)
- {
- Vector vScale = new(scale);
- int i = 0;
-
- // SIMD loop
- int simdBatchSize = Vector.Count / 4; // Number of Vector4 elements per SIMD batch
- for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
- {
- // Load the vector from source span
- Vector v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source[i]));
-
- // Scale the vector
- v *= vScale;
-
- // Write the scaled vector to the destination span
- Unsafe.WriteUnaligned(ref Unsafe.As(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(source), scale, MemoryMarshal.Cast(destination));
}
private class ConversionParams
diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
index 253239cb7..60a7e50b9 100644
--- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
@@ -5,14 +5,19 @@ namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
internal partial class LutABCalculator
{
+ ///
+ /// Identifies the transform direction for the configured LUT calculator.
+ ///
private enum CalculationType
{
- AtoB = 1 << 3,
- BtoA = 1 << 4,
+ ///
+ /// Converts from device space to PCS using ICC mAB stage order.
+ ///
+ AtoB,
- SingleCurve = 1,
- CurveMatrix = 2,
- CurveClut = 3,
- Full = 4,
+ ///
+ /// Converts from PCS to device space using ICC mBA stage order.
+ ///
+ BtoA,
}
}
diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
index 172d80639..10ac6e596 100644
--- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
@@ -17,67 +17,106 @@ internal partial class LutABCalculator : IVector4Calculator
private MatrixCalculator matrixCalculator;
private ClutCalculator clutCalculator;
+ ///
+ /// Initializes a new instance of the class for an ICC mAB transform.
+ ///
+ /// The parsed A-to-B LUT entry.
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;
}
+ ///
+ /// Initializes a new instance of the class for an ICC mBA transform.
+ ///
+ /// The parsed B-to-A LUT entry.
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;
}
+ ///
+ /// Calculates the transformed value by applying the configured ICC LUT stages in specification order.
+ ///
+ /// The input value.
+ /// The transformed value.
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");
}
}
+ ///
+ /// Creates calculators for the processing stages present in the LUT entry.
+ ///
+ ///
+ /// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
+ ///
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)
{
diff --git a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs b/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
index 20df08e37..5875b74f1 100644
--- a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
+++ b/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)
diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs
index 91fa42624..a3959f12e 100644
--- a/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs
+++ b/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs
@@ -62,6 +62,7 @@ public sealed class GammaWorkingSpace : RgbWorkingSpace
///
public override int GetHashCode() => HashCode.Combine(
+ typeof(GammaWorkingSpace),
this.WhitePoint,
this.ChromaticityCoordinates,
this.Gamma);
diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs
index 97069b856..b278dd317 100644
--- a/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs
+++ b/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
///
public override int GetHashCode()
- => HashCode.Combine(this.WhitePoint, this.ChromaticityCoordinates);
+ => HashCode.Combine(this.GetType(), this.WhitePoint, this.ChromaticityCoordinates);
}
diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs
index 1c30d857f..0b88aa5b0 100644
--- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs
+++ b/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).
///
[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));
///
@@ -97,7 +97,7 @@ internal static class ColorNumerics
/// Scales a value from a 16 bit to an
/// 8 bit equivalent.
///
- /// The 8 bit component value.
+ /// The 16 bit component value.
/// The
[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);
+ ///
+ /// Scales a value from a 32 bit to an
+ /// 8 bit equivalent.
+ ///
+ /// The 32 bit component value.
+ /// The value.
+ [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);
+
///
/// Scales a value from an 8 bit to
/// an 16 bit equivalent.
@@ -142,6 +185,26 @@ internal static class ColorNumerics
public static ushort From8BitTo16Bit(byte component)
=> (ushort)(component * 257);
+ ///
+ /// Scales a value from an 16 bit to
+ /// an 16 bit equivalent.
+ ///
+ /// The 16 bit component value.
+ /// The 32 bit
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint From16BitTo32Bit(ushort component)
+ => (uint)(component * 65537);
+
+ ///
+ /// Scales a value from an 8 bit to
+ /// an 32 bit equivalent.
+ ///
+ /// The 8 bit component value.
+ /// The 32 bit
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint From8BitTo32Bit(byte component)
+ => (uint)(component * 16843009);
+
///
/// Returns how many bits are required to store the specified number of colors.
/// Performs a Log2() on the value.
diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs
index efe68977b..8980d2b53 100644
--- a/src/ImageSharp/Common/Helpers/Numerics.cs
+++ b/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
/// The maximum inclusive value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span span, byte min, byte max)
- {
- Span 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);
///
/// Clamps the span values to the inclusive range of min and max.
@@ -353,22 +339,7 @@ internal static class Numerics
/// The maximum inclusive value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span span, uint min, uint max)
- {
- Span 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);
///
/// Clamps the span values to the inclusive range of min and max.
@@ -378,22 +349,7 @@ internal static class Numerics
/// The maximum inclusive value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span span, int min, int max)
- {
- Span 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);
///
/// Clamps the span values to the inclusive range of min and max.
@@ -403,22 +359,7 @@ internal static class Numerics
/// The maximum inclusive value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span span, float min, float max)
- {
- Span 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);
///
/// Clamps the span values to the inclusive range of min and max.
@@ -428,92 +369,12 @@ internal static class Numerics
/// The maximum inclusive value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span span, double min, double max)
- {
- Span 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(Span span, T min, T max)
- where T : unmanaged
- {
- if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count)
- {
- int remainder = ModuloP2(span.Length, Vector.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(Span span, T min, T max)
- where T : unmanaged
- {
- ref T sRef = ref MemoryMarshal.GetReference(span);
- Vector vmin = new(min);
- Vector vmax = new(max);
-
- nint n = (nint)(uint)span.Length / Vector.Count;
- nint m = Modulo4(n);
- nint u = n - m;
-
- ref Vector vs0 = ref Unsafe.As>(ref MemoryMarshal.GetReference(span));
- ref Vector vs1 = ref Unsafe.Add(ref vs0, 1);
- ref Vector vs2 = ref Unsafe.Add(ref vs0, 2);
- ref Vector vs3 = ref Unsafe.Add(ref vs0, 3);
- ref Vector 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);
///
/// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact.
///
- /// The to premultiply
+ /// The to premultiply.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Premultiply(ref Vector4 source)
{
@@ -524,50 +385,98 @@ internal static class Numerics
}
///
- /// Bulk variant of
+ /// Clamps associated color components to the alpha component while preserving alpha.
///
- /// The span of vectors
+ /// The associated vector to clamp.
+ [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);
+ }
+
+ ///
+ /// Premultiplies the X, Y, and Z components of each vector by its W component while preserving W.
+ ///
+ /// The vectors to premultiply.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Premultiply(Span vectors)
{
- if (Avx.IsSupported && vectors.Length >= 2)
+ if (Vector512.IsHardwareAccelerated)
+ {
+ int vectorsPerVector = Vector512.Count / Vector128.Count;
+ ref Vector512 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors));
+ ref Vector512 vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
+ Vector128 alphaMask128 = Vector128.Create(0, 0, 0, -1).AsSingle();
+ Vector256 alphaMask256 = Vector256.Create(alphaMask128, alphaMask128);
+ Vector512 alphaMask = Vector512.Create(alphaMask256, alphaMask256);
+
+ while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
+ {
+ Vector512 source = vectorsBase;
+ Vector512 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
+ int vectorsPerVector = Vector256.Count / Vector128.Count;
ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors));
- ref Vector256 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
+ ref Vector256 vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
+ Vector128 alphaMask128 = Vector128.Create(0, 0, 0, -1).AsSingle();
+ Vector256 alphaMask = Vector256.Create(alphaMask128, alphaMask128);
- while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
+ while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Vector256 source = vectorsBase;
- Vector256 alpha = Avx.Permute(source, ShuffleAlphaControl);
- vectorsBase = Avx.Blend(Avx.Multiply(source, alpha), source, BlendAlphaControl);
+ Vector256 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 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors));
+ ref Vector128 vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length);
+ Vector128 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 source = vectorsBase;
+ Vector128 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));
}
}
///
/// Reverses the result of premultiplying a vector via .
+ /// When alpha is zero, the RGB components remain unchanged because no unassociated value can be recovered.
///
- /// The to premultiply
+ /// The to unpremultiply.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnPremultiply(ref Vector4 source)
{
@@ -575,74 +484,147 @@ internal static class Numerics
UnPremultiply(ref source, alpha);
}
+ ///
+ /// 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.
+ ///
+ /// The vector to unpremultiply.
+ /// The source alpha replicated to every component.
[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);
}
///
- /// Bulk variant of
+ /// 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.
///
- /// The span of vectors
+ /// The vectors to unpremultiply.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnPremultiply(Span vectors)
{
- if (Avx.IsSupported && vectors.Length >= 2)
+ if (Vector512.IsHardwareAccelerated)
{
- // Divide by 2 as 4 elements per Vector4 and 8 per Vector256
- ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors));
- ref Vector256 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
- Vector256 epsilon = Vector256.Create(Constants.Epsilon);
+ int vectorsPerVector = Vector512.Count / Vector128.Count;
+ ref Vector512 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors));
+ ref Vector512 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 source = vectorsBase;
- Vector256 alpha = Avx.Permute(source, ShuffleAlphaControl);
+ Vector512 source = vectorsBase;
+ Vector512 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.Count / Vector128.Count;
+ ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors));
+ ref Vector256 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 source = vectorsBase;
+ Vector256 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 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors));
+ ref Vector128 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 source = vectorsBase;
+ Vector128 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));
}
}
+ ///
+ /// 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.
+ ///
+ /// The associated vector.
+ /// The source alpha replicated to every lane.
+ /// The unassociated vector.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 UnPremultiply(Vector128 source, Vector128 alpha)
+ {
+ // Zero alpha has no mathematical inverse, so select the source lanes to preserve stored additive or hidden RGB data.
+ Vector128 zeroMask = Vector128.Equals(alpha, Vector128.Zero);
+ Vector128 result = Vector128.ConditionalSelect(zeroMask, source, source / alpha);
+
+ // Division would replace W with one, so restore the original alpha lane exactly.
+ Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
+ return Vector128.ConditionalSelect(alphaMask, alpha, result);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The associated vectors.
+ /// Each source alpha replicated across its four lanes.
+ /// The unassociated vectors.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256 UnPremultiply(Vector256 source, Vector256 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 zeroMask = Avx.CompareEqual(alpha, Vector256.Zero);
-
- // Divide source by alpha if alpha is nonzero, otherwise set all components to match the source value
Vector256 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);
}
+ ///
+ /// 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.
+ ///
+ /// The associated vectors.
+ /// Each source alpha replicated across its four lanes.
+ /// The unassociated vectors.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 UnPremultiply(Vector512 source, Vector512 alpha)
+ {
+ // Zero alpha has no mathematical inverse, so select the source lanes to preserve stored additive or hidden RGB data.
+ Vector512 zeroMask = Vector512.Equals(alpha, Vector512.Zero);
+ Vector512 result = Vector512.ConditionalSelect(zeroMask, source, source / alpha);
+
+ // Division would replace W with one, so restore all four original alpha lanes exactly.
+ Vector512 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);
+ }
+
///
/// Permutes the given vector return a new instance with all the values set to .
///
@@ -690,7 +672,7 @@ internal static class Numerics
///
/// The span of vectors
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static unsafe void CubePowOnXYZ(Span vectors)
+ public static void CubePowOnXYZ(Span 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
/// The sum of the values in .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Normalize(Span 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 sum256 = Vector256.Create(sum);
-
- while (Unsafe.IsAddressLessThan(ref startRef, ref endRef))
- {
- Unsafe.As>(ref startRef) /= sum256;
- startRef = ref Unsafe.Add(ref startRef, (nuint)8);
- }
-
- if ((span.Length & 7) >= 4)
- {
- Unsafe.As>(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);
}
diff --git a/src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs b/src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs
index c856267db..b1d545ed8 100644
--- a/src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs
+++ b/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;
///
-/// 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.
///
internal interface IComponentShuffle
{
///
- /// Shuffles then slices 8-bit integers in
- /// using a byte control and store the results in .
- /// If successful, this method will reduce the length of length
- /// by the shuffle amount.
+ /// Reorders one packed pixel.
///
- /// The source span of bytes.
- /// The destination span of bytes.
- void ShuffleReduce(ref ReadOnlySpan source, ref Span destination);
+ /// The source components, with the first component in the least-significant byte.
+ /// The reordered packed components.
+ public static abstract uint Invoke(uint source);
///
- /// Shuffle 8-bit integers in
- /// using the control and store the results in .
+ /// Reorders the packed pixels in a 128-bit vector.
///
- /// The source span of bytes.
- /// The destination span of bytes.
- ///
- /// Implementation can assume that source.Length is less or equal than destination.Length.
- /// Loops should iterate using source.Length.
- ///
- void Shuffle(ReadOnlySpan source, Span destination);
+ /// The source pixels.
+ /// The reordered pixels.
+ public static abstract Vector128 Invoke(Vector128 source);
}
diff --git a/src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs b/src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs
index 0f282c7f9..2c80d8f57 100644
--- a/src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs
+++ b/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;
-///
+///
+/// Defines a stateless operation that reorders a three-component pixel after adding opaque alpha.
+///
internal interface IPad3Shuffle4 : IComponentShuffle
{
}
-internal readonly struct DefaultPad3Shuffle4([ConstantExpected] byte control) : IPad3Shuffle4
+///
+/// Preserves XYZ order and appends opaque W.
+///
+internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4
{
- public byte Control { get; } = control;
-
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span 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;
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 source) => source;
+}
+
+///
+/// Reorders padded XYZW components to WXYZ.
+///
+internal readonly struct WXYZPad3Shuffle4 : IPad3Shuffle4
+{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span 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 temp = stackalloc byte[4];
- ref byte t = ref MemoryMarshal.GetReference(temp);
- ref uint tu = ref Unsafe.As(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(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);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 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
+///
+/// Reorders padded XYZW components to WZYX.
+///
+internal readonly struct WZYXPad3Shuffle4 : IPad3Shuffle4
{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span 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);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 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));
+}
+
+///
+/// Reorders padded XYZW components to ZYXW.
+///
+internal readonly struct ZYXWPad3Shuffle4 : IPad3Shuffle4
+{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span 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(ref dBase) = Unsafe.As(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);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 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));
}
diff --git a/src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs b/src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs
index 3c0973ad6..7de8af4e4 100644
--- a/src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs
+++ b/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;
-///
+///
+/// Identifies a stateless three-component shuffle operator.
+///
internal interface IShuffle3 : IComponentShuffle
{
}
-internal readonly struct DefaultShuffle3([ConstantExpected] byte control) : IShuffle3
+///
+/// Reorders XYZ components to ZYX.
+///
+internal readonly struct ZYXShuffle3 : IShuffle3
{
- public byte Control { get; } = control;
-
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span 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 source, Span 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);
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 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));
}
diff --git a/src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs b/src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs
index d5c6df2c8..713de342f 100644
--- a/src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs
+++ b/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;
-///
+///
+/// Defines a stateless operation over one packed four-component pixel.
+///
internal interface IShuffle4 : IComponentShuffle
{
-}
-
-internal readonly struct DefaultShuffle4([ConstantExpected] byte control) : IShuffle4
-{
- public byte Control { get; } = control;
+ ///
+ /// Reorders the packed pixels in a 256-bit vector.
+ ///
+ /// The source pixels.
+ /// The reordered pixels.
+ public static abstract Vector256 Invoke(Vector256 source);
- [MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span 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
+ ///
+ /// Reorders the packed pixels in a 512-bit vector.
+ ///
+ /// The source pixels.
+ /// The reordered pixels.
+ public static abstract Vector512 Invoke(Vector512 source);
- [MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span destination)
+ ///
+ /// Expands one 128-bit lane mask into absolute indices for a 512-bit shuffle.
+ ///
+ /// The indices, from zero through fifteen, for one 128-bit lane.
+ /// The corresponding absolute indices for all four 128-bit lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 ExpandLaneMask(Vector128 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 lane1 = laneMask + Vector128.Create((byte)16);
+ Vector128 lane2 = laneMask + Vector128.Create((byte)32);
+ Vector128 lane3 = laneMask + Vector128.Create((byte)48);
+
+ return Vector512.Create(Vector256.Create(laneMask, lane1), Vector256.Create(lane2, lane3));
}
}
+///
+/// Reorders XYZW components to WXYZ.
+///
internal readonly struct WXYZShuffle4 : IShuffle4
{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination)
- => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle2103);
+ public static uint Invoke(uint source)
- [MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span destination)
+ // source = [W Z Y X]
+ // ROTL(8, source) = [Z Y X W]
+ => BitOperations.RotateLeft(source, 8);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 source)
+ => Vector128_.ShuffleNative(source, CreateLaneMask());
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 Invoke(Vector256 source)
{
- ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source));
- ref uint dBase = ref Unsafe.As(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 mask = CreateLaneMask();
+ return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 Invoke(Vector512 source)
+
+ // Expand the four-pixel lane permutation across all four 128-bit lanes.
+ => Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
+
+ ///
+ /// Creates the indices that rotate each XYZW pixel to WXYZ within one 128-bit lane.
+ ///
+ /// The pixel-local byte shuffle indices.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 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);
}
+///
+/// Reorders XYZW components to WZYX.
+///
internal readonly struct WZYXShuffle4 : IShuffle4
{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination)
- => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle0123);
+ public static uint Invoke(uint source)
- [MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span destination)
+ // source = [W Z Y X]
+ // REVERSE(source) = [X Y Z W]
+ => BinaryPrimitives.ReverseEndianness(source);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 source)
+ => Vector128_.ShuffleNative(source, CreateLaneMask());
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 Invoke(Vector256 source)
{
- ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source));
- ref uint dBase = ref Unsafe.As(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 mask = CreateLaneMask();
+ return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 Invoke(Vector512 source)
+
+ // Expand the four-pixel lane permutation across all four 128-bit lanes.
+ => Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
+
+ ///
+ /// Creates the indices that reverse each XYZW pixel to WZYX within one 128-bit lane.
+ ///
+ /// The pixel-local byte shuffle indices.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 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);
}
+///
+/// Reorders XYZW components to YZWX.
+///
internal readonly struct YZWXShuffle4 : IShuffle4
{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination)
- => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle0321);
+ public static uint Invoke(uint source)
- [MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span destination)
+ // source = [W Z Y X]
+ // ROTR(8, source) = [X W Z Y]
+ => BitOperations.RotateRight(source, 8);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 source)
+ => Vector128_.ShuffleNative(source, CreateLaneMask());
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 Invoke(Vector256 source)
{
- ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source));
- ref uint dBase = ref Unsafe.As(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 mask = CreateLaneMask();
+ return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 Invoke(Vector512 source)
+
+ // Expand the four-pixel lane permutation across all four 128-bit lanes.
+ => Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
+
+ ///
+ /// Creates the indices that rotate each XYZW pixel to YZWX within one 128-bit lane.
+ ///
+ /// The pixel-local byte shuffle indices.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 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);
}
+///
+/// Reorders XYZW components to ZYXW.
+///
internal readonly struct ZYXWShuffle4 : IShuffle4
{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination)
- => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3012);
+ public static uint Invoke(uint source)
- [MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span 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);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 source)
+ => Vector128_.ShuffleNative(source, CreateLaneMask());
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 Invoke(Vector256 source)
{
- ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source));
- ref uint dBase = ref Unsafe.As(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 mask = CreateLaneMask();
+ return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 Invoke(Vector512 source)
+
+ // Expand the four-pixel lane permutation across all four 128-bit lanes.
+ => Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
+
+ ///
+ /// Creates the indices that exchange X and Z in each XYZW pixel within one 128-bit lane.
+ ///
+ /// The pixel-local byte shuffle indices.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 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);
}
+///
+/// Reorders XYZW components to XWZY.
+///
internal readonly struct XWZYShuffle4 : IShuffle4
{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span destination)
- => HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle1230);
+ public static uint Invoke(uint source)
- [MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span 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);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 source)
+ => Vector128_.ShuffleNative(source, CreateLaneMask());
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 Invoke(Vector256 source)
{
- ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source));
- ref uint dBase = ref Unsafe.As(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 mask = CreateLaneMask();
+ return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 Invoke(Vector512 source)
+
+ // Expand the four-pixel lane permutation across all four 128-bit lanes.
+ => Vector512_.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
+
+ ///
+ /// Creates the indices that exchange Y and W in each XYZW pixel within one 128-bit lane.
+ ///
+ /// The pixel-local byte shuffle indices.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 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);
}
diff --git a/src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs b/src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs
index 3e7e44066..8f32a5a6c 100644
--- a/src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs
+++ b/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;
-///
+///
+/// Defines a stateless operation that reorders four packed components before retaining three.
+///
internal interface IShuffle4Slice3 : IComponentShuffle
{
}
-internal readonly struct DefaultShuffle4Slice3([ConstantExpected] byte control) : IShuffle4Slice3
+///
+/// Preserves XYZ order and discards W.
+///
+internal readonly struct XYZWShuffle4Slice3 : IShuffle4Slice3
{
- public byte Control { get; } = control;
-
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span 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;
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 source) => source;
+}
+
+///
+/// Reorders XYZW components to YZW before discarding X.
+///
+internal readonly struct YZWXShuffle4Slice3 : IShuffle4Slice3
+{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span 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);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 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
+///
+/// Reorders XYZW components to WZY before discarding X.
+///
+internal readonly struct WZYXShuffle4Slice3 : IShuffle4Slice3
{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ShuffleReduce(ref ReadOnlySpan source, ref Span 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);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 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));
+}
+
+///
+/// Reorders XYZW components to ZYX before discarding W.
+///
+internal readonly struct ZYXWShuffle4Slice3 : IShuffle4Slice3
+{
+ ///
[MethodImpl(InliningOptions.ShortMethod)]
- public void Shuffle(ReadOnlySpan source, Span destination)
- {
- ref uint sBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source));
- ref Byte3 dBase = ref Unsafe.As(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(ref Unsafe.Add(ref sBase, 0));
- Unsafe.Add(ref dBase, 1) = Unsafe.As(ref Unsafe.Add(ref sBase, 1));
- Unsafe.Add(ref dBase, 2) = Unsafe.As(ref Unsafe.Add(ref sBase, 2));
- Unsafe.Add(ref dBase, 3) = Unsafe.As(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(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);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Invoke(Vector128 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));
}
+///
+/// Represents one tightly packed three-byte value for scalar four-to-three component writes.
+///
[StructLayout(LayoutKind.Explicit, Size = 3)]
internal readonly struct Byte3
{
diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.Convert.cs b/src/ImageSharp/Common/Helpers/SimdUtils.Convert.cs
index 5318ad049..3a66be520 100644
--- a/src/ImageSharp/Common/Helpers/SimdUtils.Convert.cs
+++ b/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);
+ }
+ }
+
+ ///
+ /// Converts byte-magnitude floating-point values to bytes using saturating round-to-nearest with midpoint values away from zero.
+ ///
+ /// The source byte magnitudes.
+ /// The destination bytes.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ internal static void FloatToByteSaturate(ReadOnlySpan source, Span 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 source, Span destination)
+ private static void ConvertFloatToByteRemainder(ReadOnlySpan source, Span 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);
}
diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
index ff5ea5de3..503a64b90 100644
--- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
+++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
@@ -601,51 +601,6 @@ internal static partial class SimdUtils
}
}
- ///
- /// Performs a multiplication and an addition of the .
- /// TODO: Fix. The arguments are in a different order to the FMA intrinsic.
- ///
- /// ret = (vm0 * vm1) + va
- /// The vector to add to the intermediate result.
- /// The first vector to multiply.
- /// The second vector to multiply.
- /// The .
- [MethodImpl(InliningOptions.AlwaysInline)]
- public static Vector256 MultiplyAdd(
- Vector256 va,
- Vector256 vm0,
- Vector256 vm1)
- {
- if (Fma.IsSupported)
- {
- return Fma.MultiplyAdd(vm1, vm0, va);
- }
-
- return va + (vm0 * vm1);
- }
-
- ///
- /// Performs a multiplication and a negated addition of the .
- ///
- /// ret = c - (a * b)
- /// The first vector to multiply.
- /// The second vector to multiply.
- /// The vector to add negated to the intermediate result.
- /// The .
- [MethodImpl(InliningOptions.ShortMethod)]
- public static Vector256 MultiplyAddNegated(
- Vector256 a,
- Vector256 b,
- Vector256 c)
- {
- if (Fma.IsSupported)
- {
- return Fma.MultiplyAddNegated(a, b, c);
- }
-
- return Avx.Subtract(c, Avx.Multiply(a, b));
- }
-
///
/// Blend packed 8-bit integers from and using .
/// The high bit of each corresponding 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
///
- internal static unsafe void ByteToNormalizedFloat(
+ internal static void ByteToNormalizedFloat(
ReadOnlySpan source,
Span 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.Count);
@@ -764,6 +723,8 @@ internal static partial class SimdUtils
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination));
+ Vector512 high = Vector512.Create(reciprocalHigh);
+ Vector512 low = Vector512.Create(reciprocalLow);
for (nuint i = 0; i < n; i++)
{
@@ -773,11 +734,16 @@ internal static partial class SimdUtils
Vector512 i2 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector512.Count * 2)));
Vector512 i3 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector512.Count * 3)));
- // Declare multiplier on each line. Codegen is better.
- Vector512 f0 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i0);
- Vector512 f1 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i1);
- Vector512 f2 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i2);
- Vector512 f3 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i3);
+ Vector512 f0 = Avx512F.ConvertToVector512Single(i0);
+ Vector512 f1 = Avx512F.ConvertToVector512Single(i1);
+ Vector512 f2 = Avx512F.ConvertToVector512Single(i2);
+ Vector512 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 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 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination));
+ Vector256 high = Vector256.Create(reciprocalHigh);
+ Vector256 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(ref Unsafe.Add(ref sourceBase, si));
Vector256 i3 = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(Unsafe.Add(ref refULong, 3)).AsByte());
- // Declare multiplier on each line. Codegen is better.
- Vector256 f0 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i0);
- Vector256 f1 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i1);
- Vector256 f2 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i2);
- Vector256 f3 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i3);
+ Vector256 f0 = Avx.ConvertToVector256Single(i0);
+ Vector256 f1 = Avx.ConvertToVector256Single(i1);
+ Vector256 f2 = Avx.ConvertToVector256Single(i2);
+ Vector256 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 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 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination));
- Vector128 scale = Vector128.Create(1 / (float)byte.MaxValue);
+ Vector128 high = Vector128.Create(reciprocalHigh);
+ Vector128 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 f0 = scale * Vector128.ConvertToSingle(i0);
- Vector128 f1 = scale * Vector128.ConvertToSingle(i1);
- Vector128 f2 = scale * Vector128.ConvertToSingle(i2);
- Vector128