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 [![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions) [![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp) [![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) -[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors) -### **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 f3 = scale * Vector128.ConvertToSingle(i3); + Vector128 f0 = Vector128.ConvertToSingle(i0); + Vector128 f1 = Vector128.ConvertToSingle(i1); + Vector128 f2 = Vector128.ConvertToSingle(i2); + Vector128 f3 = Vector128.ConvertToSingle(i3); + + f0 = Vector128_.FusedMultiplyAdd(f0, high, f0 * low); + f1 = Vector128_.FusedMultiplyAdd(f1, high, f1 * low); + f2 = Vector128_.FusedMultiplyAdd(f2, high, f2 * low); + f3 = Vector128_.FusedMultiplyAdd(f3, high, f3 * low); ref Vector128 d = ref Unsafe.Add(ref destinationBase, i * 4); @@ -879,6 +857,19 @@ internal static partial class SimdUtils internal static void NormalizedFloatToByteSaturateReduce( ref ReadOnlySpan source, ref Span destination) + => FloatToByteSaturateReduce(ref source, ref destination, byte.MaxValue); + + /// + /// Converts as many scaled floating-point values as possible to bytes and retains the unconverted remainder. + /// + /// The source buffer. + /// The destination buffer. + /// The factor applied before conversion. + [MethodImpl(InliningOptions.ShortMethod)] + internal static void FloatToByteSaturateReduce( + ref ReadOnlySpan source, + ref Span destination, + float scaleFactor) { DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!"); @@ -903,9 +894,10 @@ internal static partial class SimdUtils if (adjustedCount > 0) { - NormalizedFloatToByteSaturate( + FloatToByteSaturate( source[..adjustedCount], - destination[..adjustedCount]); + destination[..adjustedCount], + scaleFactor); source = source[adjustedCount..]; destination = destination[adjustedCount..]; @@ -925,6 +917,18 @@ internal static partial class SimdUtils internal static void NormalizedFloatToByteSaturate( ReadOnlySpan source, Span destination) + => FloatToByteSaturate(source, destination, byte.MaxValue); + + /// + /// Converts scaled floating-point values to bytes using saturating round-to-nearest with midpoint values away from zero. + /// + /// The source buffer. + /// The destination buffer. + /// The factor applied before conversion. + internal static void FloatToByteSaturate( + ReadOnlySpan source, + Span destination, + float scaleFactor) { if (Vector512.IsHardwareAccelerated && Avx512BW.IsSupported) { @@ -935,7 +939,7 @@ internal static partial class SimdUtils ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - Vector512 scale = Vector512.Create((float)byte.MaxValue); + Vector512 scale = Vector512.Create(scaleFactor); Vector512 mask = PermuteMaskDeinterleave16x32(); for (nuint i = 0; i < n; i++) @@ -947,10 +951,10 @@ internal static partial class SimdUtils Vector512 f2 = scale * Unsafe.Add(ref s, 2); Vector512 f3 = scale * Unsafe.Add(ref s, 3); - Vector512 w0 = Vector512_.ConvertToInt32RoundToEven(f0); - Vector512 w1 = Vector512_.ConvertToInt32RoundToEven(f1); - Vector512 w2 = Vector512_.ConvertToInt32RoundToEven(f2); - Vector512 w3 = Vector512_.ConvertToInt32RoundToEven(f3); + Vector512 w0 = Vector512_.ConvertToInt32RoundAwayFromZero(f0); + Vector512 w1 = Vector512_.ConvertToInt32RoundAwayFromZero(f1); + Vector512 w2 = Vector512_.ConvertToInt32RoundAwayFromZero(f2); + Vector512 w3 = Vector512_.ConvertToInt32RoundAwayFromZero(f3); Vector512 u0 = Avx512BW.PackSignedSaturate(w0, w1); Vector512 u1 = Avx512BW.PackSignedSaturate(w2, w3); @@ -969,7 +973,7 @@ internal static partial class SimdUtils ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - Vector256 scale = Vector256.Create((float)byte.MaxValue); + Vector256 scale = Vector256.Create(scaleFactor); Vector256 mask = PermuteMaskDeinterleave8x32(); for (nuint i = 0; i < n; i++) @@ -981,10 +985,10 @@ internal static partial class SimdUtils Vector256 f2 = scale * Unsafe.Add(ref s, 2); Vector256 f3 = scale * Unsafe.Add(ref s, 3); - Vector256 w0 = Vector256_.ConvertToInt32RoundToEven(f0); - Vector256 w1 = Vector256_.ConvertToInt32RoundToEven(f1); - Vector256 w2 = Vector256_.ConvertToInt32RoundToEven(f2); - Vector256 w3 = Vector256_.ConvertToInt32RoundToEven(f3); + Vector256 w0 = Vector256_.ConvertToInt32RoundAwayFromZero(f0); + Vector256 w1 = Vector256_.ConvertToInt32RoundAwayFromZero(f1); + Vector256 w2 = Vector256_.ConvertToInt32RoundAwayFromZero(f2); + Vector256 w3 = Vector256_.ConvertToInt32RoundAwayFromZero(f3); Vector256 u0 = Avx2.PackSignedSaturate(w0, w1); Vector256 u1 = Avx2.PackSignedSaturate(w2, w3); @@ -1004,7 +1008,7 @@ internal static partial class SimdUtils ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - Vector128 scale = Vector128.Create((float)byte.MaxValue); + Vector128 scale = Vector128.Create(scaleFactor); Vector128 min = Vector128.Zero; Vector128 max = Vector128.Create((int)byte.MaxValue); @@ -1017,10 +1021,10 @@ internal static partial class SimdUtils Vector128 f2 = scale * Unsafe.Add(ref s, 2); Vector128 f3 = scale * Unsafe.Add(ref s, 3); - Vector128 w0 = Vector128_.ConvertToInt32RoundToEven(f0); - Vector128 w1 = Vector128_.ConvertToInt32RoundToEven(f1); - Vector128 w2 = Vector128_.ConvertToInt32RoundToEven(f2); - Vector128 w3 = Vector128_.ConvertToInt32RoundToEven(f3); + Vector128 w0 = Vector128_.ConvertToInt32RoundAwayFromZero(f0); + Vector128 w1 = Vector128_.ConvertToInt32RoundAwayFromZero(f1); + Vector128 w2 = Vector128_.ConvertToInt32RoundAwayFromZero(f2); + Vector128 w3 = Vector128_.ConvertToInt32RoundAwayFromZero(f3); w0 = Vector128_.Clamp(w0, min, max); w1 = Vector128_.Clamp(w1, min, max); @@ -1172,8 +1176,10 @@ internal static partial class SimdUtils Vector256 rgb, rg, bx; Vector256 r, g, b; + // Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load, + // so we need 3 extra pixels of addressable slack beyond the vectorized chunk. const int bytesPerRgbStride = 24; - nuint count = (uint)source.Length / 8; + nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0; for (nuint i = 0; i < count; i++) { rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte(); @@ -1193,10 +1199,10 @@ internal static partial class SimdUtils } int sliceCount = (int)(count * 8); - redChannel = redChannel.Slice(sliceCount); - greenChannel = greenChannel.Slice(sliceCount); - blueChannel = blueChannel.Slice(sliceCount); - source = source.Slice(sliceCount); + redChannel = redChannel[sliceCount..]; + greenChannel = greenChannel[sliceCount..]; + blueChannel = blueChannel[sliceCount..]; + source = source[sliceCount..]; } } } diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs b/src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs index dbeb54a80..0eeb4832f 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs @@ -3,8 +3,11 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp; @@ -41,22 +44,89 @@ internal static partial class SimdUtils /// The type of shuffle struct. /// The source span of bytes. /// The destination span of bytes. - /// The type of shuffle to perform. [MethodImpl(InliningOptions.ShortMethod)] public static void Shuffle4( ReadOnlySpan source, - Span destination, - TShuffle shuffle) + Span destination) where TShuffle : struct, IShuffle4 { VerifyShuffle4SpanInput(source, destination); - shuffle.ShuffleReduce(ref source, ref destination); + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + int length = source.Length; + int i = 0; - // Deal with the remainder: - if (source.Length > 0) + // The same offset flows through descending widths. This keeps a single traversal while + // allowing a row that is not a multiple of the widest register to retain a vectorized tail. + if (Vector512.IsHardwareAccelerated) + { + int fourVectorsFromEnd = length - (Vector512.Count * 4); + + for (; i <= fourVectorsFromEnd; i += Vector512.Count * 4) + { + // Four independent vectors amortize loop control and expose enough work for the CPU + // to overlap loads, byte shuffles, and stores without changing pixel ordering. + TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i); + TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector512.Count)); + TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector512.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector512.Count * 2))); + TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector512.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector512.Count * 3))); + } + + int oneVectorFromEnd = length - Vector512.Count; + + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int fourVectorsFromEnd = length - (Vector256.Count * 4); + + for (; i <= fourVectorsFromEnd; i += Vector256.Count * 4) + { + TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i); + TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector256.Count)); + TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector256.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector256.Count * 2))); + TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector256.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector256.Count * 3))); + } + + int oneVectorFromEnd = length - Vector256.Count; + + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) { - shuffle.Shuffle(source, destination); + int fourVectorsFromEnd = length - (Vector128.Count * 4); + + for (; i <= fourVectorsFromEnd; i += Vector128.Count * 4) + { + TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i); + TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector128.Count)); + TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector128.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector128.Count * 2))); + TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector128.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector128.Count * 3))); + } + + int oneVectorFromEnd = length - Vector128.Count; + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i); + } + } + + // The vector cascade leaves fewer than four pixels. A full uint load keeps each pixel + // in a register while the closed operator resolves to its rotate, reverse, or mask sequence. + for (; i < length; i += 4) + { + uint packed = Unsafe.As(ref Unsafe.Add(ref sourceBase, (nuint)i)); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)i)) = TShuffle.Invoke(packed); } } @@ -67,23 +137,114 @@ internal static partial class SimdUtils /// The type of shuffle struct. /// The source span of bytes. /// The destination span of bytes. - /// The type of shuffle to perform. [MethodImpl(InliningOptions.ShortMethod)] public static void Shuffle3( ReadOnlySpan source, - Span destination, - TShuffle shuffle) + Span destination) where TShuffle : struct, IShuffle3 { - // Source length should be smaller than destination length, and divisible by 3. VerifyShuffle3SpanInput(source, destination); - shuffle.ShuffleReduce(ref source, ref destination); + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + int length = source.Length; + int i = 0; - // Deal with the remainder: - if (source.Length > 0) + if (Vector128.IsHardwareAccelerated) + { + // Each group contains sixteen XYZ pixels in three registers. For a register beginning + // [X0,Y0,Z0,X1,Y1,Z1,...], the indices [0,1,2,0x80,3,4,5,0x80,...] + // produce four [X,Y,Z,0] pixels. Index 0x80 selects zero on the native byte-shuffle + // instructions and on the portable helper, creating a temporary W lane for TShuffle. + Vector128 padMask = Vector128.Create((byte)0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80); + + // After TShuffle places the retained components in bytes 0..2 of each four-byte pixel, + // [0,1,2,4,5,6,8,9,10,12,13,14] packs four triplets into twelve bytes. Rotating that + // mask by twelve positions moves the packed bytes four positions right. Alternating the + // two alignments lets AlignRight stitch four twelve-byte results into three full registers. + Vector128 sliceMask = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80); + Vector128 sliceEndMask = Vector128_.AlignRight(sliceMask, sliceMask, 12); + ref Vector128 sourceVectors = ref Unsafe.As>(ref sourceBase); + ref Vector128 destinationVectors = ref Unsafe.As>(ref destinationBase); + nuint sourceVectorCount = (uint)length / (uint)Vector128.Count; + nuint vectorIndex = 0; + + for (; vectorIndex + 2 < sourceVectorCount; vectorIndex += 3) + { + // Realign the three source registers into four registers holding four complete + // triplets apiece. All source registers are captured before any destination store. + ref Vector128 source0 = ref Unsafe.Add(ref sourceVectors, vectorIndex); + Vector128 v0 = source0; + Vector128 v1 = Unsafe.Add(ref source0, 1); + Vector128 v2 = Unsafe.Add(ref source0, 2); + Vector128 v3 = Vector128_.ShiftRightBytesInVector(v2, 4); + + v2 = Vector128_.AlignRight(v2, v1, 8); + v1 = Vector128_.AlignRight(v1, v0, 12); + + v0 = TShuffle.Invoke(Vector128_.ShuffleNative(v0, padMask)); + v1 = TShuffle.Invoke(Vector128_.ShuffleNative(v1, padMask)); + v2 = TShuffle.Invoke(Vector128_.ShuffleNative(v2, padMask)); + v3 = TShuffle.Invoke(Vector128_.ShuffleNative(v3, padMask)); + + v0 = Vector128_.ShuffleNative(v0, sliceEndMask); + v1 = Vector128_.ShuffleNative(v1, sliceMask); + v2 = Vector128_.ShuffleNative(v2, sliceEndMask); + v3 = Vector128_.ShuffleNative(v3, sliceMask); + + Vector128 destination0 = Vector128_.AlignRight(v1, v0, 4); + Vector128 destination2 = Vector128_.AlignRight(v3, v2, 12); + v1 = Vector128_.ShiftLeftBytesInVector(v1, 4); + v2 = Vector128_.ShiftRightBytesInVector(v2, 4); + Vector128 destination1 = Vector128_.AlignRight(v2, v1, 8); + + ref Vector128 destination0Ref = ref Unsafe.Add(ref destinationVectors, vectorIndex); + destination0Ref = destination0; + Unsafe.Add(ref destination0Ref, 1) = destination1; + Unsafe.Add(ref destination0Ref, 2) = destination2; + } + + i = (int)(vectorIndex * (uint)Vector128.Count); + int oneTailVectorFromEnd = length - Vector128.Count; + + for (; i <= oneTailVectorFromEnd; i += 12) + { + // A single readable register contains four complete triplets plus four bytes from + // the following pixels. The pad mask ignores those extra bytes before the operator + // runs, and the slice mask packs the four results into the low twelve bytes. + Vector128 result = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + result = Vector128_.ShuffleNative(result, padMask); + result = TShuffle.Invoke(result); + result = Vector128_.ShuffleNative(result, sliceMask); + + // Store exactly twelve bytes so an in-place shuffle does not overwrite the next + // source triplet captured by the following iteration. + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = result.GetLower(); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)(i + 8))) = result.AsUInt32().GetElement(2); + } + } + + int widenedReadEnd = length - 3; + + for (; i < widenedReadEnd; i += 3) { - shuffle.Shuffle(source, destination); + // The fourth byte belongs to the following pixel, but the operator only contributes the + // low three result bytes. This unaligned read replaces three dependent byte loads safely. + uint packed = Unsafe.As(ref Unsafe.Add(ref sourceBase, (nuint)i)); + uint shuffled = TShuffle.Invoke(packed); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)i)) = Unsafe.As(ref shuffled); + } + + if (i < length) + { + // The final triplet has no fourth readable byte, so construct only this terminal pixel. + uint packed = + Unsafe.Add(ref sourceBase, (nuint)i) | + ((uint)Unsafe.Add(ref sourceBase, (nuint)(i + 1)) << 8) | + ((uint)Unsafe.Add(ref sourceBase, (nuint)(i + 2)) << 16); + + uint shuffled = TShuffle.Invoke(packed); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)i)) = Unsafe.As(ref shuffled); } } @@ -94,22 +255,81 @@ internal static partial class SimdUtils /// The type of shuffle struct. /// The source span of bytes. /// The destination span of bytes. - /// The type of shuffle to perform. [MethodImpl(InliningOptions.ShortMethod)] public static void Pad3Shuffle4( ReadOnlySpan source, - Span destination, - TShuffle shuffle) + Span destination) where TShuffle : struct, IPad3Shuffle4 { VerifyPad3Shuffle4SpanInput(source, destination); - shuffle.ShuffleReduce(ref source, ref destination); + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + int sourceLength = source.Length; + int sourceOffset = 0; + int destinationOffset = 0; - // Deal with the remainder: - if (source.Length > 0) + if (Vector128.IsHardwareAccelerated) + { + // For source bytes [X0,Y0,Z0,X1,Y1,Z1,...], the indices + // [0,1,2,0x80,3,4,5,0x80,...] form four [X,Y,Z,0] pixels. The native + // and portable shuffle paths both interpret 0x80 as a zero-producing index. + Vector128 padMask = Vector128.Create((byte)0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80); + + // The broadcast ulong has 0xFF in bytes 3 and 7; repeating it across 128 bits + // fills W at byte positions 3, 7, 11, and 15 without modifying X, Y, or Z. + Vector128 opaqueAlpha = Vector128.Create(0xFF000000FF000000UL).AsByte(); + ref Vector128 sourceVectors = ref Unsafe.As>(ref sourceBase); + ref Vector128 destinationVectors = ref Unsafe.As>(ref destinationBase); + nuint sourceVectorCount = (uint)sourceLength / (uint)Vector128.Count; + nuint sourceVectorIndex = 0; + nuint destinationVectorIndex = 0; + + for (; sourceVectorIndex + 2 < sourceVectorCount; + sourceVectorIndex += 3, destinationVectorIndex += 4) + { + // Three source registers contain sixteen packed triplets. Aligning at 12, 8, and + // 4-byte boundaries produces four registers whose low twelve bytes each hold four pixels. + ref Vector128 source0 = ref Unsafe.Add(ref sourceVectors, sourceVectorIndex); + Vector128 v0 = source0; + Vector128 v1 = Unsafe.Add(ref source0, 1); + Vector128 v2 = Unsafe.Add(ref source0, 2); + Vector128 v3 = Vector128_.ShiftRightBytesInVector(v2, 4); + + v2 = Vector128_.AlignRight(v2, v1, 8); + v1 = Vector128_.AlignRight(v1, v0, 12); + + ref Vector128 destination0 = ref Unsafe.Add(ref destinationVectors, destinationVectorIndex); + destination0 = TShuffle.Invoke(Vector128_.ShuffleNative(v0, padMask) | opaqueAlpha); + Unsafe.Add(ref destination0, 1) = TShuffle.Invoke(Vector128_.ShuffleNative(v1, padMask) | opaqueAlpha); + Unsafe.Add(ref destination0, 2) = TShuffle.Invoke(Vector128_.ShuffleNative(v2, padMask) | opaqueAlpha); + Unsafe.Add(ref destination0, 3) = TShuffle.Invoke(Vector128_.ShuffleNative(v3, padMask) | opaqueAlpha); + } + + sourceOffset = (int)(sourceVectorIndex * (uint)Vector128.Count); + destinationOffset = (int)(destinationVectorIndex * (uint)Vector128.Count); + } + + int widenedReadEnd = sourceLength - 3; + + for (; sourceOffset < widenedReadEnd; sourceOffset += 3, destinationOffset += 4) + { + // The widened load intentionally includes the next pixel's first byte. Replacing that + // high byte with opaque alpha yields the complete XYZW value with one unaligned read. + uint packed = Unsafe.As(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset)) | 0xFF000000; + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = TShuffle.Invoke(packed); + } + + if (sourceOffset < sourceLength) { - shuffle.Shuffle(source, destination); + // The final triplet cannot use the widened load because no following byte is in range. + uint packed = + Unsafe.Add(ref sourceBase, (nuint)sourceOffset) | + ((uint)Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 1)) << 8) | + ((uint)Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 2)) << 16) | + 0xFF000000; + + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = TShuffle.Invoke(packed); } } @@ -120,22 +340,103 @@ internal static partial class SimdUtils /// The type of shuffle struct. /// The source span of bytes. /// The destination span of bytes. - /// The type of shuffle to perform. [MethodImpl(InliningOptions.ShortMethod)] public static void Shuffle4Slice3( ReadOnlySpan source, - Span destination, - TShuffle shuffle) + Span destination) where TShuffle : struct, IShuffle4Slice3 { VerifyShuffle4Slice3SpanInput(source, destination); - shuffle.ShuffleReduce(ref source, ref destination); + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + int sourceLength = source.Length; + int sourceOffset = 0; + int destinationOffset = 0; - // Deal with the remainder: - if (source.Length > 0) + if (Vector128.IsHardwareAccelerated) + { + // Each operator first places the retained components in bytes 0..2 of every four-byte + // pixel. The indices [0,1,2,4,5,6,8,9,10,12,13,14] delete each fourth byte and + // pack four triplets into the low twelve bytes. Indices 0x80 zero the unused bytes. + Vector128 sliceMask = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80); + + // Rotating the mask by twelve moves its packed result four bytes right. Alternating + // shifted and unshifted results gives AlignRight the overlap needed to concatenate + // four twelve-byte groups into three complete destination registers. + Vector128 sliceEndMask = Vector128_.AlignRight(sliceMask, sliceMask, 12); + ref Vector128 sourceVectors = ref Unsafe.As>(ref sourceBase); + ref Vector128 destinationVectors = ref Unsafe.As>(ref destinationBase); + nuint sourceVectorCount = (uint)sourceLength / (uint)Vector128.Count; + nuint sourceVectorIndex = 0; + nuint destinationVectorIndex = 0; + + for (; sourceVectorIndex + 3 < sourceVectorCount; sourceVectorIndex += 4, destinationVectorIndex += 3) + { + // Load and transform all sixteen source pixels before writing the shorter output group. + // This preserves forward progress when source and destination begin at the same address. + ref Vector128 source0 = ref Unsafe.Add(ref sourceVectors, sourceVectorIndex); + Vector128 v0 = TShuffle.Invoke(source0); + Vector128 v1 = TShuffle.Invoke(Unsafe.Add(ref source0, 1)); + Vector128 v2 = TShuffle.Invoke(Unsafe.Add(ref source0, 2)); + Vector128 v3 = TShuffle.Invoke(Unsafe.Add(ref source0, 3)); + + v0 = Vector128_.ShuffleNative(v0, sliceEndMask); + v1 = Vector128_.ShuffleNative(v1, sliceMask); + v2 = Vector128_.ShuffleNative(v2, sliceEndMask); + v3 = Vector128_.ShuffleNative(v3, sliceMask); + + Vector128 destination0 = Vector128_.AlignRight(v1, v0, 4); + Vector128 destination2 = Vector128_.AlignRight(v3, v2, 12); + v1 = Vector128_.ShiftLeftBytesInVector(v1, 4); + v2 = Vector128_.ShiftRightBytesInVector(v2, 4); + Vector128 destination1 = Vector128_.AlignRight(v2, v1, 8); + + ref Vector128 destination0Ref = ref Unsafe.Add(ref destinationVectors, destinationVectorIndex); + destination0Ref = destination0; + Unsafe.Add(ref destination0Ref, 1) = destination1; + Unsafe.Add(ref destination0Ref, 2) = destination2; + } + + sourceOffset = (int)(sourceVectorIndex * (uint)Vector128.Count); + destinationOffset = (int)(destinationVectorIndex * (uint)Vector128.Count); + int oneTailVectorFromEnd = sourceLength - Vector128.Count; + + for (; sourceOffset <= oneTailVectorFromEnd; sourceOffset += 16, destinationOffset += 12) + { + // The operator arranges the three retained components at the front of each pixel. + // One fixed shuffle then compacts four pixels into the low twelve vector bytes. + Vector128 result = TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)sourceOffset)); + + result = Vector128_.ShuffleNative(result, sliceMask); + + // The split store writes the exact 12-byte result and remains safe for in-place shrinking. + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = result.GetLower(); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 8))) = result.AsUInt32().GetElement(2); + } + } + + int fourPixelsFromEnd = sourceLength - 16; + + for (; sourceOffset <= fourPixelsFromEnd; sourceOffset += 16, destinationOffset += 12) + { + // Transform four complete pixels before the first three-byte store. Keeping the source + // values in registers avoids reloads after an in-place shrinking destination advances. + uint packed0 = TShuffle.Invoke(Unsafe.As(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset))); + uint packed1 = TShuffle.Invoke(Unsafe.As(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 4)))); + uint packed2 = TShuffle.Invoke(Unsafe.As(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 8)))); + uint packed3 = TShuffle.Invoke(Unsafe.As(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 12)))); + + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = Unsafe.As(ref packed0); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 3))) = Unsafe.As(ref packed1); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 6))) = Unsafe.As(ref packed2); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 9))) = Unsafe.As(ref packed3); + } + + for (; sourceOffset < sourceLength; sourceOffset += 4, destinationOffset += 3) { - shuffle.Shuffle(source, destination); + uint packed = TShuffle.Invoke(Unsafe.As(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset))); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = Unsafe.As(ref packed); } } @@ -150,10 +451,15 @@ internal static partial class SimdUtils for (nuint i = 0; i < (uint)source.Length; i += 4) { - Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i); - Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i); - Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i); - Unsafe.Add(ref dBase, i + 3) = Unsafe.Add(ref sBase, p3 + i); + // Stage the scalar tail in a local Vector4 so p0..p3 index source + // values that were captured before any overlapping destination writes. + Vector4 v = Unsafe.As(ref Unsafe.Add(ref sBase, i)); + ref float pBase = ref Unsafe.As(ref v); + + Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0); + Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1); + Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2); + Unsafe.Add(ref dBase, i + 3u) = Unsafe.Add(ref pBase, p3); } } diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs new file mode 100644 index 000000000..19f46c06f --- /dev/null +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal static partial class TensorPrimitives_ +{ + /// + /// Computes the element-wise sum of the values in and . + /// + /// The element type. + /// The first addends. + /// The second addends. + /// The destination for the sums. + /// and do not have the same length. + /// is shorter than the input spans. + /// + /// An input and overlap without beginning at the same memory location. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ReadOnlySpan x, ReadOnlySpan y, Span destination) + where T : IAdditionOperators, IAdditiveIdentity + => InvokeSpanSpanIntoSpan>(x, y, destination); + + /// + /// Computes the element-wise sum of the values in and the scalar . + /// + /// The element type. + /// The first addends. + /// The scalar second addend. + /// The destination for the sums. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ReadOnlySpan x, T y, Span destination) + where T : IAdditionOperators, IAdditiveIdentity + => InvokeSpanScalarIntoSpan>(x, y, destination); + + /// + /// Adds corresponding values. + /// + /// The element type. + private readonly struct AddOperator : IBinaryOperator + where T : IAdditionOperators, IAdditiveIdentity + { + /// + /// Gets a value indicating whether this operation supports vector execution. + /// + public static bool Vectorizable => true; + + /// + /// Adds scalar values. + /// + /// The first addend. + /// The second addend. + /// The sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Invoke(T x, T y) => x + y; + + /// + /// Adds 128-bit vectors. + /// + /// The first addends. + /// The second addends. + /// The sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Invoke(Vector128 x, Vector128 y) => x + y; + + /// + /// Adds 256-bit vectors. + /// + /// The first addends. + /// The second addends. + /// The sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Invoke(Vector256 x, Vector256 y) => x + y; + + /// + /// Adds 512-bit vectors. + /// + /// The first addends. + /// The second addends. + /// The sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Invoke(Vector512 x, Vector512 y) => x + y; + } +} diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs new file mode 100644 index 000000000..b974163dc --- /dev/null +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs @@ -0,0 +1,322 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal static partial class TensorPrimitives_ +{ + /// + /// Computes the element-wise result of clamping to the inclusive range specified + /// by and . + /// + /// The element type. + /// The values to clamp. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The destination for the clamped values. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Clamp(ReadOnlySpan x, T min, T max, Span destination) + where T : INumber + => InvokeSpanScalarScalarIntoSpan>(x, min, max, destination); + + /// + /// Clamps single-precision values with the normalized runtime semantics. + /// + /// The values to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ClampSingle( + Vector128 value, + Vector128 min, + Vector128 max) + { + // Unlike the native x86 min/max instructions, the normalized runtime operations propagate a NaN in the + // first operand and select negative zero when equal values have different signs. + Vector128 maximum = Vector128.ConditionalSelect( + Vector128.LessThan(min, value) + | ~Vector128.Equals(value, value) + | (Vector128.Equals(value, min) & (min.AsInt32() >> 31).AsSingle()), + value, + min); + + return Vector128.ConditionalSelect( + Vector128.LessThan(maximum, max) + | ~Vector128.Equals(maximum, maximum) + | (Vector128.Equals(maximum, max) & (maximum.AsInt32() >> 31).AsSingle()), + maximum, + max); + } + + /// + /// Clamps single-precision values with the normalized runtime semantics. + /// + /// The values to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ClampSingle( + Vector256 value, + Vector256 min, + Vector256 max) + { + Vector256 maximum = Vector256.ConditionalSelect( + Vector256.LessThan(min, value) + | ~Vector256.Equals(value, value) + | (Vector256.Equals(value, min) & (min.AsInt32() >> 31).AsSingle()), + value, + min); + + return Vector256.ConditionalSelect( + Vector256.LessThan(maximum, max) + | ~Vector256.Equals(maximum, maximum) + | (Vector256.Equals(maximum, max) & (maximum.AsInt32() >> 31).AsSingle()), + maximum, + max); + } + + /// + /// Clamps single-precision values with the normalized runtime semantics. + /// + /// The values to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ClampSingle( + Vector512 value, + Vector512 min, + Vector512 max) + { + Vector512 maximum = Vector512.ConditionalSelect( + Vector512.LessThan(min, value) + | ~Vector512.Equals(value, value) + | (Vector512.Equals(value, min) & (min.AsInt32() >> 31).AsSingle()), + value, + min); + + return Vector512.ConditionalSelect( + Vector512.LessThan(maximum, max) + | ~Vector512.Equals(maximum, maximum) + | (Vector512.Equals(maximum, max) & (maximum.AsInt32() >> 31).AsSingle()), + maximum, + max); + } + + /// + /// Clamps double-precision values with the normalized runtime semantics. + /// + /// The values to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ClampDouble( + Vector128 value, + Vector128 min, + Vector128 max) + { + Vector128 maximum = Vector128.ConditionalSelect( + Vector128.LessThan(min, value) + | ~Vector128.Equals(value, value) + | (Vector128.Equals(value, min) & (min.AsInt64() >> 63).AsDouble()), + value, + min); + + return Vector128.ConditionalSelect( + Vector128.LessThan(maximum, max) + | ~Vector128.Equals(maximum, maximum) + | (Vector128.Equals(maximum, max) & (maximum.AsInt64() >> 63).AsDouble()), + maximum, + max); + } + + /// + /// Clamps double-precision values with the normalized runtime semantics. + /// + /// The values to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ClampDouble( + Vector256 value, + Vector256 min, + Vector256 max) + { + Vector256 maximum = Vector256.ConditionalSelect( + Vector256.LessThan(min, value) + | ~Vector256.Equals(value, value) + | (Vector256.Equals(value, min) & (min.AsInt64() >> 63).AsDouble()), + value, + min); + + return Vector256.ConditionalSelect( + Vector256.LessThan(maximum, max) + | ~Vector256.Equals(maximum, maximum) + | (Vector256.Equals(maximum, max) & (maximum.AsInt64() >> 63).AsDouble()), + maximum, + max); + } + + /// + /// Clamps double-precision values with the normalized runtime semantics. + /// + /// The values to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ClampDouble( + Vector512 value, + Vector512 min, + Vector512 max) + { + Vector512 maximum = Vector512.ConditionalSelect( + Vector512.LessThan(min, value) + | ~Vector512.Equals(value, value) + | (Vector512.Equals(value, min) & (min.AsInt64() >> 63).AsDouble()), + value, + min); + + return Vector512.ConditionalSelect( + Vector512.LessThan(maximum, max) + | ~Vector512.Equals(maximum, maximum) + | (Vector512.Equals(maximum, max) & (maximum.AsInt64() >> 63).AsDouble()), + maximum, + max); + } + + /// + /// Clamps values using the complete runtime tensor contract, including signed-zero correction. + /// + /// The element type. + private readonly struct ClampOperator : ITernaryOperator + where T : INumber + { + /// + /// Gets a value indicating whether this operation supports vector execution. + /// + public static bool Vectorizable => true; + + /// + /// Clamps a scalar value. + /// + /// The value. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The clamped value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Invoke(T x, T min, T max) + => Vector128.IsSupported ? T.Min(T.Max(x, min), max) : T.Clamp(x, min, max); + + /// + /// Clamps a 128-bit vector. + /// + /// The values. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Invoke(Vector128 x, Vector128 min, Vector128 max) + { + if (typeof(T) == typeof(float)) + { + Vector128 result = ClampSingle( + Unsafe.As, Vector128>(ref x), + Unsafe.As, Vector128>(ref min), + Unsafe.As, Vector128>(ref max)); + + return Unsafe.As, Vector128>(ref result); + } + + if (typeof(T) == typeof(double)) + { + Vector128 result = ClampDouble( + Unsafe.As, Vector128>(ref x), + Unsafe.As, Vector128>(ref min), + Unsafe.As, Vector128>(ref max)); + + return Unsafe.As, Vector128>(ref result); + } + + return Vector128_.Clamp(x, min, max); + } + + /// + /// Clamps a 256-bit vector. + /// + /// The values. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Invoke(Vector256 x, Vector256 min, Vector256 max) + { + if (typeof(T) == typeof(float)) + { + Vector256 result = ClampSingle( + Unsafe.As, Vector256>(ref x), + Unsafe.As, Vector256>(ref min), + Unsafe.As, Vector256>(ref max)); + + return Unsafe.As, Vector256>(ref result); + } + + if (typeof(T) == typeof(double)) + { + Vector256 result = ClampDouble( + Unsafe.As, Vector256>(ref x), + Unsafe.As, Vector256>(ref min), + Unsafe.As, Vector256>(ref max)); + + return Unsafe.As, Vector256>(ref result); + } + + return Vector256_.Clamp(x, min, max); + } + + /// + /// Clamps a 512-bit vector. + /// + /// The values. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Invoke(Vector512 x, Vector512 min, Vector512 max) + { + if (typeof(T) == typeof(float)) + { + Vector512 result = ClampSingle( + Unsafe.As, Vector512>(ref x), + Unsafe.As, Vector512>(ref min), + Unsafe.As, Vector512>(ref max)); + + return Unsafe.As, Vector512>(ref result); + } + + if (typeof(T) == typeof(double)) + { + Vector512 result = ClampDouble( + Unsafe.As, Vector512>(ref x), + Unsafe.As, Vector512>(ref min), + Unsafe.As, Vector512>(ref max)); + + return Unsafe.As, Vector512>(ref result); + } + + return Vector512_.Clamp(x, min, max); + } + } +} diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs new file mode 100644 index 000000000..f4a5bd01a --- /dev/null +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal static partial class TensorPrimitives_ +{ + /// + /// Computes the element-wise result of dividing the values in by . + /// + /// The element type. + /// The dividend values. + /// The divisor. + /// The destination for the quotient values. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Divide(ReadOnlySpan x, T y, Span destination) + where T : IDivisionOperators + => InvokeSpanScalarIntoSpanForDivision>(x, y, destination); + + /// + /// Determines whether has the same vector division support as . + /// + /// The element type. + /// when is a 32-bit signed native integer type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInt32Like() + => typeof(T) == typeof(int) || (IntPtr.Size == 4 && typeof(T) == typeof(nint)); + + /// + /// Divides values by a scalar. + /// + /// The element type. + private readonly struct DivideOperator : IBinaryOperator + where T : IDivisionOperators + { + /// + /// Gets a value indicating whether this operation supports vector execution. + /// + public static bool Vectorizable => typeof(T) == typeof(float) + || typeof(T) == typeof(double) + || (Vector256.IsHardwareAccelerated && IsInt32Like()); + + /// + /// Divides scalar values. + /// + /// The dividend. + /// The divisor. + /// The quotient. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Invoke(T x, T y) => x / y; + + /// + /// Divides 128-bit vectors. + /// + /// The dividends. + /// The divisors. + /// The quotients. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Invoke(Vector128 x, Vector128 y) => x / y; + + /// + /// Divides 256-bit vectors. + /// + /// The dividends. + /// The divisors. + /// The quotients. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Invoke(Vector256 x, Vector256 y) => x / y; + + /// + /// Divides 512-bit vectors. + /// + /// The dividends. + /// The divisors. + /// The quotients. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Invoke(Vector512 x, Vector512 y) => x / y; + } +} diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs new file mode 100644 index 000000000..e80ca5411 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs @@ -0,0 +1,900 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +/// +/// Provides compatibility implementations for tensor operations that are not available on every target framework. +/// +/// +/// The API shape follows System.Numerics.Tensors.TensorPrimitives so call sites can move to the runtime +/// implementation when ImageSharp no longer supports target frameworks that predate it. +/// +internal static partial class TensorPrimitives_ +{ + /// + /// Defines an element-wise binary operation. + /// + /// The element type. + private interface IBinaryOperator + { + /// + /// Gets a value indicating whether the operation supports vector execution. + /// + public static abstract bool Vectorizable { get; } + + /// + /// Applies the operation to scalar values. + /// + /// The first value. + /// The second value. + /// The operation result. + public static abstract T Invoke(T x, T y); + + /// + /// Applies the operation to 128-bit vectors. + /// + /// The first vector. + /// The second vector. + /// The operation result. + public static abstract Vector128 Invoke(Vector128 x, Vector128 y); + + /// + /// Applies the operation to 256-bit vectors. + /// + /// The first vector. + /// The second vector. + /// The operation result. + public static abstract Vector256 Invoke(Vector256 x, Vector256 y); + + /// + /// Applies the operation to 512-bit vectors. + /// + /// The first vector. + /// The second vector. + /// The operation result. + public static abstract Vector512 Invoke(Vector512 x, Vector512 y); + } + + /// + /// Defines an element-wise ternary operation. + /// + /// The element type. + private interface ITernaryOperator + { + /// + /// Gets a value indicating whether the operation supports vector execution. + /// + public static abstract bool Vectorizable { get; } + + /// + /// Applies the operation to scalar values. + /// + /// The first value. + /// The second value. + /// The third value. + /// The operation result. + public static abstract T Invoke(T x, T y, T z); + + /// + /// Applies the operation to 128-bit vectors. + /// + /// The first vector. + /// The second vector. + /// The third vector. + /// The operation result. + public static abstract Vector128 Invoke(Vector128 x, Vector128 y, Vector128 z); + + /// + /// Applies the operation to 256-bit vectors. + /// + /// The first vector. + /// The second vector. + /// The third vector. + /// The operation result. + public static abstract Vector256 Invoke(Vector256 x, Vector256 y, Vector256 z); + + /// + /// Applies the operation to 512-bit vectors. + /// + /// The first vector. + /// The second vector. + /// The third vector. + /// The operation result. + public static abstract Vector512 Invoke(Vector512 x, Vector512 y, Vector512 z); + } + + /// + /// Validates that an input and destination are either disjoint or begin at the same memory location. + /// + /// The element type. + /// The input values. + /// The destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ValidateInputOutputSpanNonOverlapping(ReadOnlySpan input, Span destination) + { + // Runtime TensorPrimitives permits exact same-start overlap for in-place operation. A shifted overlap is + // rejected because forward SIMD stores could overwrite input elements before a later load consumes them. + if (!Unsafe.AreSame(ref MemoryMarshal.GetReference(input), ref MemoryMarshal.GetReference(destination)) + && input.Overlaps(destination)) + { + ThrowInputAndDestinationSpanMustNotOverlap(); + } + } + + /// + /// Throws when input spans do not have the same length. + /// + [DoesNotReturn] + private static void ThrowSpansMustHaveSameLength() + => throw new ArgumentException("Input span arguments must all have the same length."); + + /// + /// Throws when the destination cannot hold every result. + /// + [DoesNotReturn] + private static void ThrowDestinationTooShort() + => throw new ArgumentException("Destination is too short.", "destination"); + + /// + /// Throws when an input and destination overlap without beginning at the same memory location. + /// + [DoesNotReturn] + private static void ThrowInputAndDestinationSpanMustNotOverlap() + => throw new ArgumentException( + "The destination span may only overlap with an input span if the two spans start at the same memory location.", + "destination"); + + /// + /// Performs an element-wise binary operation between two spans. + /// + /// The element type. + /// The operation to apply. + /// The first input values. + /// The second input values. + /// The destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeSpanSpanIntoSpan( + ReadOnlySpan x, + ReadOnlySpan y, + Span destination) + where TOperator : struct, IBinaryOperator + { + if (x.Length != y.Length) + { + ThrowSpansMustHaveSameLength(); + } + + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + ValidateInputOutputSpanNonOverlapping(y, destination); + + ref T xRef = ref MemoryMarshal.GetReference(x); + ref T yRef = ref MemoryMarshal.GetReference(y); + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + nuint length = (uint)x.Length; + + // Runtime main selects the widest supported pipeline once one complete vector is available. + // Each pipeline preloads its final inputs when a tail overlaps so same-start in-place operation remains correct. + if (TOperator.Vectorizable + && Vector512.IsHardwareAccelerated + && Vector512.IsSupported + && length >= (uint)Vector512.Count) + { + InvokeVectorized512(ref xRef, ref yRef, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256.IsSupported && length >= (uint)Vector256.Count) + { + InvokeVectorized256(ref xRef, ref yRef, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128.IsSupported && length >= (uint)Vector128.Count) + { + InvokeVectorized128(ref xRef, ref yRef, ref destinationRef, length); + return; + } + + for (nuint i = 0; i < length; i++) + { + Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), Unsafe.Add(ref yRef, i)); + } + } + + /// + /// Performs an element-wise binary operation between a span and a scalar. + /// + /// The element type. + /// The operation to apply. + /// The input values. + /// The scalar input. + /// The destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeSpanScalarIntoSpan( + ReadOnlySpan x, + T y, + Span destination) + where TOperator : struct, IBinaryOperator + { + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + + ref T xRef = ref MemoryMarshal.GetReference(x); + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + nuint length = (uint)x.Length; + + // Runtime main selects the widest supported pipeline once one complete vector is available. + if (TOperator.Vectorizable + && Vector512.IsHardwareAccelerated + && Vector512.IsSupported + && length >= (uint)Vector512.Count) + { + InvokeVectorized512(ref xRef, y, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256.IsSupported && length >= (uint)Vector256.Count) + { + InvokeVectorized256(ref xRef, y, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128.IsSupported && length >= (uint)Vector128.Count) + { + InvokeVectorized128(ref xRef, y, ref destinationRef, length); + return; + } + + for (nuint i = 0; i < length; i++) + { + Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), y); + } + } + + /// + /// Performs element-wise division using the runtime tensor width-selection order. + /// + /// The element type. + /// The division operation to apply. + /// The input values. + /// The scalar divisor. + /// The destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeSpanScalarIntoSpanForDivision( + ReadOnlySpan x, + T y, + Span destination) + where TOperator : struct, IBinaryOperator + { + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + + ref T xRef = ref MemoryMarshal.GetReference(x); + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + nuint length = (uint)x.Length; + + // Runtime main selects the widest supported pipeline once one complete vector is available. + if (TOperator.Vectorizable + && Vector512.IsHardwareAccelerated + && Vector512.IsSupported + && length >= (uint)Vector512.Count) + { + InvokeVectorized512(ref xRef, y, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256.IsSupported && length >= (uint)Vector256.Count) + { + InvokeVectorized256(ref xRef, y, ref destinationRef, length); + return; + } + + // Four values fill one 128-bit float vector. Processing exactly one packed prefix before the scalar + // remainder avoids the overlapping second vector that regresses the common seven-element normalization. + if (TOperator.Vectorizable + && Vector128.IsHardwareAccelerated + && Vector128.IsSupported + && length >= (uint)Vector128.Count) + { + nuint vectorCount = (uint)Vector128.Count; + Vector128 yVector = Vector128.Create(y); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef), yVector).StoreUnsafe(ref destinationRef); + + for (nuint i = vectorCount; i < length; i++) + { + Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), y); + } + + return; + } + + for (nuint i = 0; i < length; i++) + { + Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), y); + } + } + + /// + /// Performs an element-wise ternary operation between a span and two scalars. + /// + /// The element type. + /// The operation to apply. + /// The input values. + /// The first scalar input. + /// The second scalar input. + /// The destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeSpanScalarScalarIntoSpan( + ReadOnlySpan x, + T y, + T z, + Span destination) + where TOperator : struct, ITernaryOperator + { + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + + ref T xRef = ref MemoryMarshal.GetReference(x); + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + nuint length = (uint)x.Length; + + // This dispatch mirrors the runtime pipeline: large inputs use the widest available registers while + // short inputs fall through to a width that fits, keeping the operator contract identical at every length. + if (TOperator.Vectorizable && Vector512.IsHardwareAccelerated && Vector512.IsSupported && length >= (uint)Vector512.Count) + { + InvokeVectorized512(ref xRef, y, z, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256.IsSupported && length >= (uint)Vector256.Count) + { + InvokeVectorized256(ref xRef, y, z, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128.IsSupported && length >= (uint)Vector128.Count) + { + InvokeVectorized128(ref xRef, y, z, ref destinationRef, length); + return; + } + + for (nuint i = 0; i < length; i++) + { + Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i), y, z); + } + } + + /// + /// Applies a binary operation between two spans with 128-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first element of the first input. + /// The first element of the second input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized128( + ref T xRef, + ref T yRef, + ref T destinationRef, + nuint length) + where TOperator : struct, IBinaryOperator + { + nuint vectorCount = (uint)Vector128.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + + // When a tail exists, both final inputs are loaded before any stores. This permits either source to also + // be the destination when the tail starts inside the range written by the preceding full vector. + Vector128 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector128.LoadUnsafe(ref xRef, length - vectorCount), + Vector128.LoadUnsafe(ref yRef, length - vectorCount)); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7)), Vector128.LoadUnsafe(ref yRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index), Vector128.LoadUnsafe(ref yRef, index)).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a binary operation between two spans with 256-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first element of the first input. + /// The first element of the second input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized256( + ref T xRef, + ref T yRef, + ref T destinationRef, + nuint length) + where TOperator : struct, IBinaryOperator + { + nuint vectorCount = (uint)Vector256.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector256 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector256.LoadUnsafe(ref xRef, length - vectorCount), + Vector256.LoadUnsafe(ref yRef, length - vectorCount)); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7)), Vector256.LoadUnsafe(ref yRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index), Vector256.LoadUnsafe(ref yRef, index)).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a binary operation between two spans with 512-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first element of the first input. + /// The first element of the second input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized512( + ref T xRef, + ref T yRef, + ref T destinationRef, + nuint length) + where TOperator : struct, IBinaryOperator + { + nuint vectorCount = (uint)Vector512.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector512 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector512.LoadUnsafe(ref xRef, length - vectorCount), + Vector512.LoadUnsafe(ref yRef, length - vectorCount)); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7)), Vector512.LoadUnsafe(ref yRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index), Vector512.LoadUnsafe(ref yRef, index)).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a binary operation between a span and a scalar with 128-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The scalar input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized128( + ref T xRef, + T y, + ref T destinationRef, + nuint length) + where TOperator : struct, IBinaryOperator + { + nuint vectorCount = (uint)Vector128.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector128 yVector = Vector128.Create(y); + + // When a tail exists, preloading its final vector is required for in-place operation because it must + // observe the original values before an earlier overlapping store writes them. + Vector128 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector128.LoadUnsafe(ref xRef, length - vectorCount), + yVector); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index), yVector).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a binary operation with 256-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The scalar input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized256( + ref T xRef, + T y, + ref T destinationRef, + nuint length) + where TOperator : struct, IBinaryOperator + { + nuint vectorCount = (uint)Vector256.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector256 yVector = Vector256.Create(y); + Vector256 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector256.LoadUnsafe(ref xRef, length - vectorCount), + yVector); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index), yVector).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a binary operation with 512-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The scalar input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized512( + ref T xRef, + T y, + ref T destinationRef, + nuint length) + where TOperator : struct, IBinaryOperator + { + nuint vectorCount = (uint)Vector512.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector512 yVector = Vector512.Create(y); + Vector512 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector512.LoadUnsafe(ref xRef, length - vectorCount), + yVector); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index), yVector).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a ternary operation with 128-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The first scalar input. + /// The second scalar input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized128( + ref T xRef, + T y, + T z, + ref T destinationRef, + nuint length) + where TOperator : struct, ITernaryOperator + { + nuint vectorCount = (uint)Vector128.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector128 yVector = Vector128.Create(y); + Vector128 zVector = Vector128.Create(z); + Vector128 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector128.LoadUnsafe(ref xRef, length - vectorCount), + yVector, + zVector); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index), yVector, zVector).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a ternary operation with 256-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The first scalar input. + /// The second scalar input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized256( + ref T xRef, + T y, + T z, + ref T destinationRef, + nuint length) + where TOperator : struct, ITernaryOperator + { + nuint vectorCount = (uint)Vector256.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector256 yVector = Vector256.Create(y); + Vector256 zVector = Vector256.Create(z); + Vector256 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector256.LoadUnsafe(ref xRef, length - vectorCount), + yVector, + zVector); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index), yVector, zVector).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a ternary operation with 512-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The first scalar input. + /// The second scalar input. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeVectorized512( + ref T xRef, + T y, + T z, + ref T destinationRef, + nuint length) + where TOperator : struct, ITernaryOperator + { + nuint vectorCount = (uint)Vector512.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector512 yVector = Vector512.Create(y); + Vector512 zVector = Vector512.Create(z); + Vector512 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke( + Vector512.LoadUnsafe(ref xRef, length - vectorCount), + yVector, + zVector); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7)), yVector, zVector).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index), yVector, zVector).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } +} diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs new file mode 100644 index 000000000..fc621a122 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs @@ -0,0 +1,249 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal static partial class TensorPrimitives_ +{ + /// + /// Computes the element-wise maximum of the values in and . + /// + /// The element type. + /// The values to compare. + /// The value to compare with each element. + /// The destination for the maximum values. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Max(ReadOnlySpan x, T y, Span destination) + where T : INumber + => InvokeSpanScalarIntoSpan>(x, y, destination); + + /// + /// Selects maximum single-precision values with the normalized runtime semantics. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 MaxSingle(Vector128 x, Vector128 y) + { + // The .NET 8 operation already handles ordered unequal values. Correct its second-operand result for a + // first-operand NaN, then use bitwise AND for equal values so positive zero wins regardless of operand order. + Vector128 result = Vector128.Max(x, y); + result = Vector128.ConditionalSelect(~Vector128.Equals(x, x), x, result); + + return Vector128.ConditionalSelect( + Vector128.Equals(x, y), + x & y, + result); + } + + /// + /// Selects maximum single-precision values with the normalized runtime semantics. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 MaxSingle(Vector256 x, Vector256 y) + { + Vector256 result = Vector256.Max(x, y); + result = Vector256.ConditionalSelect(~Vector256.Equals(x, x), x, result); + + return Vector256.ConditionalSelect( + Vector256.Equals(x, y), + x & y, + result); + } + + /// + /// Selects maximum single-precision values with the normalized runtime semantics. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 MaxSingle(Vector512 x, Vector512 y) + { + Vector512 result = Vector512.Max(x, y); + result = Vector512.ConditionalSelect(~Vector512.Equals(x, x), x, result); + + return Vector512.ConditionalSelect( + Vector512.Equals(x, y), + x & y, + result); + } + + /// + /// Selects maximum double-precision values with the normalized runtime semantics. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 MaxDouble(Vector128 x, Vector128 y) + { + Vector128 result = Vector128.Max(x, y); + result = Vector128.ConditionalSelect(~Vector128.Equals(x, x), x, result); + + return Vector128.ConditionalSelect( + Vector128.Equals(x, y), + x & y, + result); + } + + /// + /// Selects maximum double-precision values with the normalized runtime semantics. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 MaxDouble(Vector256 x, Vector256 y) + { + Vector256 result = Vector256.Max(x, y); + result = Vector256.ConditionalSelect(~Vector256.Equals(x, x), x, result); + + return Vector256.ConditionalSelect( + Vector256.Equals(x, y), + x & y, + result); + } + + /// + /// Selects maximum double-precision values with the normalized runtime semantics. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 MaxDouble(Vector512 x, Vector512 y) + { + Vector512 result = Vector512.Max(x, y); + result = Vector512.ConditionalSelect(~Vector512.Equals(x, x), x, result); + + return Vector512.ConditionalSelect( + Vector512.Equals(x, y), + x & y, + result); + } + + /// + /// Selects the maximum corresponding values. + /// + /// The element type. + private readonly struct MaxOperator : IBinaryOperator + where T : INumber + { + /// + /// Gets a value indicating whether this operation supports vector execution. + /// + public static bool Vectorizable => true; + + /// + /// Selects the maximum scalar value. + /// + /// The first value. + /// The second value. + /// The maximum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Invoke(T x, T y) => T.Max(x, y); + + /// + /// Selects the maximum values from 128-bit vectors. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Invoke(Vector128 x, Vector128 y) + { + if (typeof(T) == typeof(float)) + { + Vector128 result = MaxSingle( + Unsafe.As, Vector128>(ref x), + Unsafe.As, Vector128>(ref y)); + + return Unsafe.As, Vector128>(ref result); + } + + if (typeof(T) == typeof(double)) + { + Vector128 result = MaxDouble( + Unsafe.As, Vector128>(ref x), + Unsafe.As, Vector128>(ref y)); + + return Unsafe.As, Vector128>(ref result); + } + + return Vector128.Max(x, y); + } + + /// + /// Selects the maximum values from 256-bit vectors. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Invoke(Vector256 x, Vector256 y) + { + if (typeof(T) == typeof(float)) + { + Vector256 result = MaxSingle( + Unsafe.As, Vector256>(ref x), + Unsafe.As, Vector256>(ref y)); + + return Unsafe.As, Vector256>(ref result); + } + + if (typeof(T) == typeof(double)) + { + Vector256 result = MaxDouble( + Unsafe.As, Vector256>(ref x), + Unsafe.As, Vector256>(ref y)); + + return Unsafe.As, Vector256>(ref result); + } + + return Vector256.Max(x, y); + } + + /// + /// Selects the maximum values from 512-bit vectors. + /// + /// The first values. + /// The second values. + /// The maximum values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Invoke(Vector512 x, Vector512 y) + { + if (typeof(T) == typeof(float)) + { + Vector512 result = MaxSingle( + Unsafe.As, Vector512>(ref x), + Unsafe.As, Vector512>(ref y)); + + return Unsafe.As, Vector512>(ref result); + } + + if (typeof(T) == typeof(double)) + { + Vector512 result = MaxDouble( + Unsafe.As, Vector512>(ref x), + Unsafe.As, Vector512>(ref y)); + + return Unsafe.As, Vector512>(ref result); + } + + return Vector512.Max(x, y); + } + } +} diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs new file mode 100644 index 000000000..367a7ab0b --- /dev/null +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal static partial class TensorPrimitives_ +{ + /// + /// Computes the element-wise product of the values in and . + /// + /// The element type. + /// The multiplicands. + /// The multiplier. + /// The destination for the products. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Multiply(ReadOnlySpan x, T y, Span destination) + where T : IMultiplyOperators, IMultiplicativeIdentity + => InvokeSpanScalarIntoSpan>(x, y, destination); + + /// + /// Multiplies corresponding values. + /// + /// The element type. + private readonly struct MultiplyOperator : IBinaryOperator + where T : IMultiplyOperators, IMultiplicativeIdentity + { + /// + /// Gets a value indicating whether this operation supports vector execution. + /// + public static bool Vectorizable => true; + + /// + /// Multiplies scalar values. + /// + /// The multiplicand. + /// The multiplier. + /// The product. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Invoke(T x, T y) => x * y; + + /// + /// Multiplies 128-bit vectors. + /// + /// The multiplicands. + /// The multipliers. + /// The products. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Invoke(Vector128 x, Vector128 y) => x * y; + + /// + /// Multiplies 256-bit vectors. + /// + /// The multiplicands. + /// The multipliers. + /// The products. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Invoke(Vector256 x, Vector256 y) => x * y; + + /// + /// Multiplies 512-bit vectors. + /// + /// The multiplicands. + /// The multipliers. + /// The products. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Invoke(Vector512 x, Vector512 y) => x * y; + } +} diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs new file mode 100644 index 000000000..05f93ac66 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs @@ -0,0 +1,332 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal static partial class TensorPrimitives_ +{ + /// + /// Defines an element-wise unary operation. + /// + /// The element type. + private interface IUnaryOperator + { + /// + /// Gets a value indicating whether the operation supports vector execution. + /// + public static abstract bool Vectorizable { get; } + + /// + /// Applies the operation to a scalar value. + /// + /// The input value. + /// The operation result. + public static abstract T Invoke(T x); + + /// + /// Applies the operation to a 128-bit vector. + /// + /// The input vector. + /// The operation result. + public static abstract Vector128 Invoke(Vector128 x); + + /// + /// Applies the operation to a 256-bit vector. + /// + /// The input vector. + /// The operation result. + public static abstract Vector256 Invoke(Vector256 x); + + /// + /// Applies the operation to a 512-bit vector. + /// + /// The input vector. + /// The operation result. + public static abstract Vector512 Invoke(Vector512 x); + } + + /// + /// Computes the element-wise negation of the values in . + /// + /// The element type. + /// The values to negate. + /// The destination for the negated values. + /// is shorter than . + /// + /// and overlap without beginning at the same memory location. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Negate(ReadOnlySpan x, Span destination) + where T : IUnaryNegationOperators + => InvokeSpanIntoSpan>(x, destination); + + /// + /// Performs an element-wise unary operation over a span. + /// + /// The element type. + /// The operation to apply. + /// The input values. + /// The destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeSpanIntoSpan(ReadOnlySpan x, Span destination) + where TOperator : struct, IUnaryOperator + { + if (x.Length > destination.Length) + { + ThrowDestinationTooShort(); + } + + ValidateInputOutputSpanNonOverlapping(x, destination); + + ref T xRef = ref MemoryMarshal.GetReference(x); + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + nuint length = (uint)x.Length; + + // Runtime main selects the widest supported pipeline once one complete vector is available. + if (TOperator.Vectorizable + && Vector512.IsHardwareAccelerated + && Vector512.IsSupported + && length >= (uint)Vector512.Count) + { + InvokeUnaryVectorized512(ref xRef, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256.IsSupported && length >= (uint)Vector256.Count) + { + InvokeUnaryVectorized256(ref xRef, ref destinationRef, length); + return; + } + + if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128.IsSupported && length >= (uint)Vector128.Count) + { + InvokeUnaryVectorized128(ref xRef, ref destinationRef, length); + return; + } + + for (nuint i = 0; i < length; i++) + { + Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i)); + } + } + + /// + /// Applies a unary operation with 128-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeUnaryVectorized128(ref T xRef, ref T destinationRef, nuint length) + where TOperator : struct, IUnaryOperator + { + nuint vectorCount = (uint)Vector128.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + + // The final vector overlaps the preceding store when the length is not a vector multiple. Loading it + // before any stores preserves same-start in-place operation because it captures the original tail. + Vector128 end = default; + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, length - vectorCount)); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a unary operation with 256-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeUnaryVectorized256(ref T xRef, ref T destinationRef, nuint length) + where TOperator : struct, IUnaryOperator + { + nuint vectorCount = (uint)Vector256.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector256 end = default; + + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, length - vectorCount)); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Applies a unary operation with 512-bit vectors. + /// + /// The element type. + /// The operation to apply. + /// The first input element. + /// The first destination element. + /// The number of elements to process. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InvokeUnaryVectorized512(ref T xRef, ref T destinationRef, nuint length) + where TOperator : struct, IUnaryOperator + { + nuint vectorCount = (uint)Vector512.Count; + nuint vectorsPerLoop = vectorCount * 8; + nuint index = 0; + Vector512 end = default; + + if ((length % vectorCount) != 0) + { + end = TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, length - vectorCount)); + } + + while ((length - index) >= vectorsPerLoop) + { + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6)); + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7)); + + index += vectorsPerLoop; + } + + while ((length - index) >= vectorCount) + { + TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index); + index += vectorCount; + } + + if (index != length) + { + end.StoreUnsafe(ref destinationRef, length - vectorCount); + } + } + + /// + /// Implements element-wise negation for scalar and SIMD inputs. + /// + /// The element type. + private readonly struct NegateOperator : IUnaryOperator + where T : IUnaryNegationOperators + { + /// + public static bool Vectorizable => true; + + /// + public static T Invoke(T x) => -x; + + /// + public static Vector128 Invoke(Vector128 x) + { + if (typeof(T) == typeof(float)) + { + // IEEE-754 negation toggles the sign bit. Expressing that operation explicitly avoids the + // subtraction-based ARM64 code generated by .NET 8 for generic vector negation, which loses + // the sign when +0F is negated and therefore differs from both scalar and runtime-main behavior. + return x ^ Vector128.Create(-0F).As(); + } + + if (typeof(T) == typeof(double)) + { + // Double-precision values use the same sign-bit representation, with the sign in bit 63. + return x ^ Vector128.Create(-0D).As(); + } + + return -x; + } + + /// + public static Vector256 Invoke(Vector256 x) + { + if (typeof(T) == typeof(float)) + { + // Keep the operation bitwise at every width so ARM64 preserves signed zero exactly. + return x ^ Vector256.Create(-0F).As(); + } + + if (typeof(T) == typeof(double)) + { + return x ^ Vector256.Create(-0D).As(); + } + + return -x; + } + + /// + public static Vector512 Invoke(Vector512 x) + { + if (typeof(T) == typeof(float)) + { + // Vector512 can be hardware accelerated directly or decomposed by the runtime; the explicit + // bit operation provides identical IEEE-754 behavior in either case. + return x ^ Vector512.Create(-0F).As(); + } + + if (typeof(T) == typeof(double)) + { + return x ^ Vector512.Create(-0D).As(); + } + + return -x; + } + } +} diff --git a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs index a5d377eb9..4a53fdda4 100644 --- a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs @@ -307,11 +307,37 @@ internal static class Vector128_ return Vector128.ConvertToInt32(val_2p23_f32 | sign); } + /// + /// Converts all values in to signed 32-bit integers, rounding midpoint values away from zero. + /// + /// The values to convert. + /// The converted integer values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ConvertToInt32RoundAwayFromZero(Vector128 vector) + { + if (Sse2.IsSupported) + { + // The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero. + Vector128 x86Adjustment = Vector128.Create(.5F) | (vector & Vector128.Create(-0F)); + return Sse2.ConvertToVector128Int32WithTruncation(vector + x86Adjustment); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.ConvertToInt32RoundAwayFromZero(vector); + } + + Vector128 sign = vector & Vector128.Create(-0F); + Vector128 fallbackAdjustment = Vector128.Create(.5F) | sign; + return Vector128.ConvertToInt32(vector + fallbackAdjustment); + } + /// /// Rounds all values in to the nearest integer /// following semantics. /// - /// The vector + /// The vector. + /// The vector with each value rounded to the nearest integer. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 RoundToNearestInteger(Vector128 vector) { @@ -338,30 +364,58 @@ internal static class Vector128_ } /// - /// Performs a multiplication and an addition of the . + /// Computes an estimate of ( * ) + . /// - /// ret = (vm0 * vm1) + va - /// The vector to add to the intermediate result. - /// The first vector to multiply. - /// The second vector to multiply. - /// The . + /// The first vector to multiply. + /// The second vector to multiply. + /// The vector to add to the product. + /// An estimate of the multiplication and addition result. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector128 MultiplyAdd( - Vector128 va, - Vector128 vm0, - Vector128 vm1) + public static Vector128 MultiplyAddEstimate(Vector128 left, Vector128 right, Vector128 addend) { if (Fma.IsSupported) { - return Fma.MultiplyAdd(vm1, vm0, va); + return Fma.MultiplyAdd(left, right, addend); } if (AdvSimd.IsSupported) { - return AdvSimd.FusedMultiplyAdd(va, vm0, vm1); + return AdvSimd.FusedMultiplyAdd(addend, left, right); } - return va + (vm0 * vm1); + return (left * right) + addend; + } + + /// + /// Computes ( * ) + , rounded as one ternary operation. + /// + /// The first vector to multiply. + /// The second vector to multiply. + /// The vector to add to the product. + /// The fused multiplication and addition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 FusedMultiplyAdd(Vector128 left, Vector128 right, Vector128 addend) + { + if (Fma.IsSupported) + { + return Fma.MultiplyAdd(left, right, addend); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.FusedMultiplyAdd(addend, left, right); + } + + // WebAssembly SIMD has no exact fused multiply-add, so match the runtime fallback by preserving fused rounding per element. + Vector64 lower = Vector64.Create( + MathF.FusedMultiplyAdd(left.GetElement(0), right.GetElement(0), addend.GetElement(0)), + MathF.FusedMultiplyAdd(left.GetElement(1), right.GetElement(1), addend.GetElement(1))); + + Vector64 upper = Vector64.Create( + MathF.FusedMultiplyAdd(left.GetElement(2), right.GetElement(2), addend.GetElement(2)), + MathF.FusedMultiplyAdd(left.GetElement(3), right.GetElement(3), addend.GetElement(3))); + + return Vector128.Create(lower, upper); } /// @@ -1242,22 +1296,9 @@ internal static class Vector128_ return PackedSimd.SubtractSaturate(left, right); } - // Widen inputs to 16-bit - (Vector128 leftLo, Vector128 leftHi) = Vector128.Widen(left); - (Vector128 rightLo, Vector128 rightHi) = Vector128.Widen(right); - - // Subtract - Vector128 diffLo = leftLo - rightLo; - Vector128 diffHi = leftHi - rightHi; - - // Clamp to signed 8-bit range - Vector128 max = Vector128.Create((ushort)byte.MaxValue); - - diffLo = Clamp(diffLo, Vector128.Zero, max); - diffHi = Clamp(diffHi, Vector128.Zero, max); - - // Narrow back to bytes - return Vector128.Narrow(diffLo, diffHi); + // Subtracting the smaller operand implements the .NET 10 unsigned contract: + // lanes where right exceeds left subtract left from itself and therefore saturate at zero. + return left - Vector128.Min(left, right); } /// diff --git a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs index 14ac13dd8..681f80013 100644 --- a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs @@ -73,11 +73,32 @@ internal static class Vector256_ return Vector256.ConvertToInt32(val_2p23_f32 | sign); } + /// + /// Converts all values in to signed 32-bit integers, rounding midpoint values away from zero. + /// + /// The values to convert. + /// The converted integer values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ConvertToInt32RoundAwayFromZero(Vector256 vector) + { + if (Avx.IsSupported) + { + // The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero. + Vector256 x86Adjustment = Vector256.Create(.5F) | (vector & Vector256.Create(-0F)); + return Avx.ConvertToVector256Int32WithTruncation(vector + x86Adjustment); + } + + Vector256 sign = vector & Vector256.Create(-0F); + Vector256 fallbackAdjustment = Vector256.Create(.5F) | sign; + return Vector256.ConvertToInt32(vector + fallbackAdjustment); + } + /// /// Rounds all values in to the nearest integer /// following semantics. /// - /// The vector + /// The vector. + /// The vector with each value rounded to the nearest integer. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 RoundToNearestInteger(Vector256 vector) { @@ -94,25 +115,68 @@ internal static class Vector256_ } /// - /// Performs a multiplication and an addition of the . + /// Computes an estimate of ( * ) + . /// - /// ret = (vm0 * vm1) + va - /// The vector to add to the intermediate result. + /// The first vector to multiply. + /// The second vector to multiply. + /// The vector to add to the product. + /// An estimate of the multiplication and addition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyAddEstimate(Vector256 left, Vector256 right, Vector256 addend) + { + if (Fma.IsSupported) + { + return Fma.MultiplyAdd(left, right, addend); + } + + Vector128 lower = Vector128_.MultiplyAddEstimate(left.GetLower(), right.GetLower(), addend.GetLower()); + Vector128 upper = Vector128_.MultiplyAddEstimate(left.GetUpper(), right.GetUpper(), addend.GetUpper()); + + return Vector256.Create(lower, upper); + } + + /// + /// Computes ( * ) + , rounded as one ternary operation. + /// + /// The first vector to multiply. + /// The second vector to multiply. + /// The vector to add to the product. + /// The fused multiplication and addition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 FusedMultiplyAdd(Vector256 left, Vector256 right, Vector256 addend) + { + if (Fma.IsSupported) + { + return Fma.MultiplyAdd(left, right, addend); + } + + // Match the runtime fallback by recursively applying the same fused contract to both halves. + Vector128 lower = Vector128_.FusedMultiplyAdd(left.GetLower(), right.GetLower(), addend.GetLower()); + Vector128 upper = Vector128_.FusedMultiplyAdd(left.GetUpper(), right.GetUpper(), addend.GetUpper()); + + return Vector256.Create(lower, upper); + } + + /// + /// Performs a multiplication and a negated addition of the . + /// + /// ret = va - (vm0 * vm1) + /// The vector to add to the negated intermediate result. /// The first vector to multiply. /// The second vector to multiply. /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 MultiplyAdd( + [MethodImpl(InliningOptions.ShortMethod)] + public static Vector256 MultiplyAddNegated( Vector256 va, Vector256 vm0, Vector256 vm1) { if (Fma.IsSupported) { - return Fma.MultiplyAdd(vm0, vm1, va); + return Fma.MultiplyAddNegated(vm0, vm1, va); } - return va + (vm0 * vm1); + return va - (vm0 * vm1); } /// @@ -431,9 +495,9 @@ internal static class Vector256_ return Avx2.SubtractSaturate(left, right); } - return Vector256.Create( - Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()), - Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper())); + // The .NET 10 portable implementation applies the same saturated operation to + // both 128-bit halves, allowing each half to select its native instruction set. + return Vector256.Create(Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()), Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper())); } /// diff --git a/src/ImageSharp/Common/Helpers/Vector512Utilities.cs b/src/ImageSharp/Common/Helpers/Vector512Utilities.cs index 03ee4626c..d2c5b4a87 100644 --- a/src/ImageSharp/Common/Helpers/Vector512Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector512Utilities.cs @@ -59,33 +59,110 @@ internal static class Vector512_ public static Vector512 ConvertToInt32RoundToEven(Vector512 vector) => Avx512F.ConvertToVector512Int32(vector); + /// + /// Converts all values in to signed 32-bit integers, rounding midpoint values away from zero. + /// + /// The values to convert. + /// The converted integer values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ConvertToInt32RoundAwayFromZero(Vector512 vector) + { + // The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero. + Vector512 half = Vector512.Create(.5F) | (vector & Vector512.Create(-0F)); + return Avx512F.ConvertToVector512Int32WithTruncation(vector + half); + } + /// /// Rounds all values in to the nearest integer /// following semantics. /// - /// The vector + /// The vector. + /// The vector with each value rounded to the nearest integer. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector512 RoundToNearestInteger(Vector512 vector) - // imm8 = 0b1000: - // imm8[7:4] = 0b0000 -> preserve 0 fractional bits (round to whole numbers) - // imm8[3:0] = 0b1000 -> _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC (round to nearest even, suppress exceptions) - => Avx512F.RoundScale(vector, 0b0000_1000); + // imm8 = 0b1000: + // imm8[7:4] = 0b0000 -> preserve 0 fractional bits (round to whole numbers) + // imm8[3:0] = 0b1000 -> _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC (round to nearest even, suppress exceptions) + => Avx512F.RoundScale(vector, 0b0000_1000); + + /// + /// Computes an estimate of ( * ) + . + /// + /// The first vector to multiply. + /// The second vector to multiply. + /// The vector to add to the product. + /// An estimate of the multiplication and addition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyAddEstimate(Vector512 left, Vector512 right, Vector512 addend) + { + if (Avx512F.IsSupported) + { + return Avx512F.FusedMultiplyAdd(left, right, addend); + } + + Vector256 lower = Vector256_.MultiplyAddEstimate(left.GetLower(), right.GetLower(), addend.GetLower()); + Vector256 upper = Vector256_.MultiplyAddEstimate(left.GetUpper(), right.GetUpper(), addend.GetUpper()); + + return Vector512.Create(lower, upper); + } + + /// + /// Computes ( * ) + , rounded as one ternary operation. + /// + /// The first vector to multiply. + /// The second vector to multiply. + /// The vector to add to the product. + /// The fused multiplication and addition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 FusedMultiplyAdd(Vector512 left, Vector512 right, Vector512 addend) + { + if (Avx512F.IsSupported) + { + return Avx512F.FusedMultiplyAdd(left, right, addend); + } + + // Match the runtime fallback by recursively applying the same fused contract to both halves. + Vector256 lower = Vector256_.FusedMultiplyAdd(left.GetLower(), right.GetLower(), addend.GetLower()); + Vector256 upper = Vector256_.FusedMultiplyAdd(left.GetUpper(), right.GetUpper(), addend.GetUpper()); + + return Vector512.Create(lower, upper); + } + + /// + /// Subtracts packed unsigned 8-bit integers in from + /// , saturating negative lane results to zero. + /// + /// The vector from which is subtracted. + /// The vector to subtract from . + /// The element-wise saturated differences. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSaturate(Vector512 left, Vector512 right) + { + if (Avx512BW.IsSupported) + { + return Avx512BW.SubtractSaturate(left, right); + } + + // This mirrors the .NET 10 portable implementation: recursively processing both + // 256-bit halves preserves lane order and lets each half select its available ISA. + return Vector512.Create(Vector256_.SubtractSaturate(left.GetLower(), right.GetLower()), Vector256_.SubtractSaturate(left.GetUpper(), right.GetUpper())); + } /// - /// Performs a multiplication and an addition of the . + /// Performs a multiplication and a negated addition of the . /// - /// ret = (vm0 * vm1) + va - /// The vector to add to the intermediate result. + /// ret = va - (vm0 * vm1) + /// The vector to add to the negated intermediate result. /// The first vector to multiply. /// The second vector to multiply. - /// The . + /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector512 MultiplyAdd( + public static Vector512 MultiplyAddNegated( Vector512 va, Vector512 vm0, Vector512 vm1) - => Avx512F.FusedMultiplyAdd(vm0, vm1, va); + => Avx512F.FusedMultiplyAddNegated(vm0, vm1, va); /// /// Restricts a vector between a minimum and a maximum value. diff --git a/src/ImageSharp/Common/Tuples/Octet{T}.cs b/src/ImageSharp/Common/Tuples/Octet{T}.cs deleted file mode 100644 index f73bdd339..000000000 --- a/src/ImageSharp/Common/Tuples/Octet{T}.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Tuples; - -/// -/// Contains 8 element value tuples of various types. -/// -[StructLayout(LayoutKind.Sequential)] -internal struct Octet - where T : unmanaged -{ - public T V0; - public T V1; - public T V2; - public T V3; - public T V4; - public T V5; - public T V6; - public T V7; - - /// - public override readonly string ToString() - { - return $"Octet<{typeof(T)}>({this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7})"; - } -} - -/// -/// Extension methods for the type. -/// -internal static class OctetExtensions -{ - /// - /// Loads the fields in a target of from one of type. - /// - /// The target of instance. - /// The source of instance. - [MethodImpl(InliningOptions.ShortMethod)] - public static void LoadFrom(ref this Octet destination, ref Octet source) - { - destination.V0 = source.V0; - destination.V1 = source.V1; - destination.V2 = source.V2; - destination.V3 = source.V3; - destination.V4 = source.V4; - destination.V5 = source.V5; - destination.V6 = source.V6; - destination.V7 = source.V7; - } - - /// - /// Loads the fields in a target of from one of type. - /// - /// The target of instance. - /// The source of instance. - [MethodImpl(InliningOptions.ShortMethod)] - public static void LoadFrom(ref this Octet destination, ref Octet source) - { - destination.V0 = (byte)source.V0; - destination.V1 = (byte)source.V1; - destination.V2 = (byte)source.V2; - destination.V3 = (byte)source.V3; - destination.V4 = (byte)source.V4; - destination.V5 = (byte)source.V5; - destination.V6 = (byte)source.V6; - destination.V7 = (byte)source.V7; - } -} diff --git a/src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs b/src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs new file mode 100644 index 000000000..b697327ff --- /dev/null +++ b/src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs @@ -0,0 +1,119 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Compression.Zlib; + +/// +/// A read-only stream over a sequence of length-delimited segments. Bytes are +/// pulled from the inner stream up to the current segment's remaining length; +/// when the segment is exhausted the supplied delegate is invoked to advance +/// to the next segment and return its length. The inner stream is not owned +/// and is not disposed. +/// +internal sealed class ChunkedReadStream : Stream +{ + private static readonly Func GetDataNoOp = () => 0; + + private readonly BufferedReadStream innerStream; + private readonly Func getData; + private int currentDataRemaining; + + public ChunkedReadStream(BufferedReadStream innerStream) + : this(innerStream, GetDataNoOp) + { + } + + public ChunkedReadStream(BufferedReadStream innerStream, Func getData) + { + this.innerStream = innerStream; + this.getData = getData; + } + + /// + public override bool CanRead => this.innerStream.CanRead; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => throw new NotSupportedException(); + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + /// + /// Sets the number of bytes available to read from the current segment. + /// Must be called before reading each segment. + /// + public void SetCurrentSegmentLength(int bytes) => this.currentDataRemaining = bytes; + + /// + public override void Flush() => throw new NotSupportedException(); + + /// + public override int ReadByte() + { + if (this.currentDataRemaining is 0) + { + this.currentDataRemaining = this.getData(); + if (this.currentDataRemaining is 0) + { + return -1; + } + } + + int value = this.innerStream.ReadByte(); + if (value is not -1) + { + this.currentDataRemaining--; + } + + return value; + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + // Decrement currentDataRemaining only by bytes actually returned by + // innerStream.Read; a short read otherwise underflows the segment + // counter and triggers getData() before the segment is truly drained. + int totalBytesRead = 0; + while (totalBytesRead < count) + { + if (this.currentDataRemaining is 0) + { + this.currentDataRemaining = this.getData(); + if (this.currentDataRemaining is 0) + { + break; + } + } + + int bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); + int bytesRead = this.innerStream.Read(buffer, offset + totalBytesRead, bytesToRead); + if (bytesRead is 0) + { + break; + } + + this.currentDataRemaining -= bytesRead; + totalBytesRead += bytesRead; + } + + return totalBytesRead; + } + + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +} diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs new file mode 100644 index 000000000..de69717da --- /dev/null +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Compression.Zlib; + +/// +/// Reads chunked input, parses the zlib CMF/FLG header, and exposes a +/// over the remaining DEFLATE payload. The +/// Adler-32 trailer is not validated. +/// +internal sealed class ZlibInflateReader : IDisposable +{ + /// + /// Used to read the Adler-32 and Crc-32 checksums. + /// We don't actually use this for anything so it doesn't + /// have to be threadsafe. + /// + private static readonly byte[] ChecksumBuffer = new byte[4]; + + private readonly ChunkedReadStream segmentStream; + + public ZlibInflateReader(BufferedReadStream innerStream) + => this.segmentStream = new ChunkedReadStream(innerStream); + + public ZlibInflateReader(BufferedReadStream innerStream, Func getData) + => this.segmentStream = new ChunkedReadStream(innerStream, getData); + + /// + /// Gets the compressed stream over the deframed inner stream. + /// + public DeflateStream? CompressedStream { get; private set; } + + /// + /// Sets the length of the next segment of compressed input and, on first + /// call, parses the zlib header. + /// + /// The remaining data length for the current segment. + /// Whether to throw on a malformed zlib header. + /// The . + [MemberNotNullWhen(true, nameof(CompressedStream))] + public bool AllocateNewBytes(int bytes, bool isCriticalChunk) + { + this.segmentStream.SetCurrentSegmentLength(bytes); + if (this.CompressedStream is null) + { + return this.InitializeInflateStream(isCriticalChunk); + } + + return true; + } + + public void Dispose() + { + this.CompressedStream?.Dispose(); + this.segmentStream?.Dispose(); + } + + [MemberNotNullWhen(true, nameof(CompressedStream))] + private bool InitializeInflateStream(bool isCriticalChunk) + { + // Read the zlib header : http://tools.ietf.org/html/rfc1950 + // CMF(Compression Method and flags) + // This byte is divided into a 4 - bit compression method and a + // 4-bit information field depending on the compression method. + // bits 0 to 3 CM Compression method + // bits 4 to 7 CINFO Compression info + // + // 0 1 + // +---+---+ + // |CMF|FLG| + // +---+---+ + int cmf = this.segmentStream.ReadByte(); + int flag = this.segmentStream.ReadByte(); + if (cmf == -1 || flag == -1) + { + return false; + } + + if ((cmf & 0x0F) == 8) + { + // CINFO is the base-2 logarithm of the LZ77 window size, minus eight. + int cinfo = (cmf & 0xF0) >> 4; + + if (cinfo > 7) + { + if (isCriticalChunk) + { + // Values of CINFO above 7 are not allowed in RFC1950. + // CINFO is not defined in this specification for CM not equal to 8. + throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); + } + + return false; + } + } + else if (isCriticalChunk) + { + throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); + } + else + { + return false; + } + + // The preset dictionary. + bool fdict = (flag & 32) != 0; + if (fdict) + { + // We don't need this for inflate so simply skip by the next four bytes. + // https://tools.ietf.org/html/rfc1950#page-6 + if (this.segmentStream.Read(ChecksumBuffer, 0, 4) != 4) + { + return false; + } + } + + this.CompressedStream = new DeflateStream(this.segmentStream, CompressionMode.Decompress, leaveOpen: true); + + return true; + } +} diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs deleted file mode 100644 index 1d743bf3a..000000000 --- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using System.IO.Compression; -using SixLabors.ImageSharp.IO; - -namespace SixLabors.ImageSharp.Compression.Zlib; - -/// -/// Provides methods and properties for deframing streams from PNGs. -/// -internal sealed class ZlibInflateStream : Stream -{ - /// - /// Used to read the Adler-32 and Crc-32 checksums. - /// We don't actually use this for anything so it doesn't - /// have to be threadsafe. - /// - private static readonly byte[] ChecksumBuffer = new byte[4]; - - /// - /// A default delegate to get more data from the inner stream. - /// - private static readonly Func GetDataNoOp = () => 0; - - /// - /// The inner raw memory stream. - /// - private readonly BufferedReadStream innerStream; - - /// - /// A value indicating whether this instance of the given entity has been disposed. - /// - /// if this instance has been disposed; otherwise, . - /// - /// If the entity is disposed, it must not be disposed a second - /// time. The isDisposed field is set the first time the entity - /// is disposed. If the isDisposed field is true, then the Dispose() - /// method will not dispose again. This help not to prolong the entity's - /// life in the Garbage Collector. - /// - private bool isDisposed; - - /// - /// The current data remaining to be read. - /// - private int currentDataRemaining; - - /// - /// Delegate to get more data once we've exhausted the current data remaining. - /// - private readonly Func getData; - - /// - /// Initializes a new instance of the class. - /// - /// The inner raw stream. - public ZlibInflateStream(BufferedReadStream innerStream) - : this(innerStream, GetDataNoOp) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The inner raw stream. - /// A delegate to get more data from the inner stream. - public ZlibInflateStream(BufferedReadStream innerStream, Func getData) - { - this.innerStream = innerStream; - this.getData = getData; - } - - /// - public override bool CanRead => this.innerStream.CanRead; - - /// - public override bool CanSeek => false; - - /// - public override bool CanWrite => throw new NotSupportedException(); - - /// - public override long Length => throw new NotSupportedException(); - - /// - public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - - /// - /// Gets the compressed stream over the deframed inner stream. - /// - public DeflateStream? CompressedStream { get; private set; } - - /// - /// Adds new bytes from a frame found in the original stream. - /// - /// The current remaining data according to the chunk length. - /// Whether the chunk to be inflated is a critical chunk. - /// The . - [MemberNotNullWhen(true, nameof(CompressedStream))] - public bool AllocateNewBytes(int bytes, bool isCriticalChunk) - { - this.currentDataRemaining = bytes; - if (this.CompressedStream is null) - { - return this.InitializeInflateStream(isCriticalChunk); - } - - return true; - } - - /// - public override void Flush() => throw new NotSupportedException(); - - /// - public override int ReadByte() - { - this.currentDataRemaining--; - return this.innerStream.ReadByte(); - } - - /// - public override int Read(byte[] buffer, int offset, int count) - { - if (this.currentDataRemaining is 0) - { - // Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks. - this.currentDataRemaining = this.getData(); - - if (this.currentDataRemaining is 0) - { - return 0; - } - } - - int bytesToRead = Math.Min(count, this.currentDataRemaining); - this.currentDataRemaining -= bytesToRead; - int totalBytesRead = this.innerStream.Read(buffer, offset, bytesToRead); - long innerStreamLength = this.innerStream.Length; - - // Keep reading data until we've reached the end of the stream or filled the buffer. - int bytesRead = 0; - offset += totalBytesRead; - while (this.currentDataRemaining is 0 && totalBytesRead < count) - { - this.currentDataRemaining = this.getData(); - - if (this.currentDataRemaining is 0) - { - return totalBytesRead; - } - - offset += bytesRead; - - if (offset >= innerStreamLength || offset >= count) - { - return totalBytesRead; - } - - bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); - this.currentDataRemaining -= bytesToRead; - bytesRead = this.innerStream.Read(buffer, offset, bytesToRead); - if (bytesRead == 0) - { - return totalBytesRead; - } - - totalBytesRead += bytesRead; - } - - return totalBytesRead; - } - - /// - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - - /// - public override void SetLength(long value) => throw new NotSupportedException(); - - /// - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - if (disposing) - { - // Dispose managed resources. - if (this.CompressedStream != null) - { - this.CompressedStream.Dispose(); - this.CompressedStream = null; - } - } - - base.Dispose(disposing); - - // Call the appropriate methods to clean up - // unmanaged resources here. - // Note disposing is done. - this.isDisposed = true; - } - - [MemberNotNullWhen(true, nameof(CompressedStream))] - private bool InitializeInflateStream(bool isCriticalChunk) - { - // Read the zlib header : http://tools.ietf.org/html/rfc1950 - // CMF(Compression Method and flags) - // This byte is divided into a 4 - bit compression method and a - // 4-bit information field depending on the compression method. - // bits 0 to 3 CM Compression method - // bits 4 to 7 CINFO Compression info - // - // 0 1 - // +---+---+ - // |CMF|FLG| - // +---+---+ - int cmf = this.innerStream.ReadByte(); - int flag = this.innerStream.ReadByte(); - this.currentDataRemaining -= 2; - if (cmf == -1 || flag == -1) - { - return false; - } - - if ((cmf & 0x0F) == 8) - { - // CINFO is the base-2 logarithm of the LZ77 window size, minus eight. - int cinfo = (cmf & 0xF0) >> 4; - - if (cinfo > 7) - { - if (isCriticalChunk) - { - // Values of CINFO above 7 are not allowed in RFC1950. - // CINFO is not defined in this specification for CM not equal to 8. - throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); - } - - return false; - } - } - else if (isCriticalChunk) - { - throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); - } - else - { - return false; - } - - // The preset dictionary. - bool fdict = (flag & 32) != 0; - if (fdict) - { - // We don't need this for inflate so simply skip by the next four bytes. - // https://tools.ietf.org/html/rfc1950#page-6 - if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4) - { - return false; - } - - this.currentDataRemaining -= 4; - } - - // Initialize the deflate BufferedReadStream. - this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true); - - return true; - } -} diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index c2b02dedd..1e5433b9e 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -2,9 +2,12 @@ // Licensed under the Six Labors Split License. using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; @@ -37,6 +40,9 @@ public sealed class Configuration /// /// Initializes a new instance of the class. /// + // Every image operation requires a Configuration. Attaching the dependency to its constructors keeps the compile-only + // seed graph visible to modern .NET trimmers without executing that graph or adding module-initialization work. + [DynamicDependency(nameof(AotCompilerTools.SeedPixelOperations), typeof(AotCompilerTools))] public Configuration() { } @@ -45,6 +51,8 @@ public sealed class Configuration /// Initializes a new instance of the class. /// /// A collection of configuration modules to register. + // The default Configuration is created through this overload, while callers may use either constructor. + [DynamicDependency(nameof(AotCompilerTools.SeedPixelOperations), typeof(AotCompilerTools))] public Configuration(params IImageFormatConfigurationModule[] configurationModules) { if (configurationModules != null) @@ -64,7 +72,9 @@ public sealed class Configuration /// /// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms /// configured with this instance. - /// Initialized with by default. + /// A positive value limits the number of concurrent operations to the set value. + /// If set to -1, there is no limit on the number of concurrently running operations. + /// Defaults to . /// public int MaxDegreeOfParallelism { @@ -212,6 +222,7 @@ public sealed class Configuration /// . /// . /// . + /// . /// . /// /// The default configuration of . @@ -224,6 +235,7 @@ public sealed class Configuration new TgaConfigurationModule(), new TiffConfigurationModule(), new WebpConfigurationModule(), + new ExrConfigurationModule(), new QoiConfigurationModule(), new IcoConfigurationModule(), new CurConfigurationModule()); diff --git a/src/ImageSharp/Formats/Bmp/BmpConstants.cs b/src/ImageSharp/Formats/Bmp/BmpConstants.cs index 1ac79a9e2..913cbdcd1 100644 --- a/src/ImageSharp/Formats/Bmp/BmpConstants.cs +++ b/src/ImageSharp/Formats/Bmp/BmpConstants.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats.Bmp; diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 17c1f545b..aba1243d7 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -131,6 +131,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore try { int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); + ushort bitsPerPixel = this.infoHeader.BitsPerPixel; image = new Image(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata); @@ -138,23 +139,27 @@ internal sealed class BmpDecoderCore : ImageDecoderCore switch (this.infoHeader.Compression) { - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3: + case BmpCompression.RGB when bitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3: this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32: + + case BmpCompression.RGB when bitsPerPixel is 32: this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 24: + + case BmpCompression.RGB when bitsPerPixel is 24: this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 16: + + case BmpCompression.RGB when bitsPerPixel is 16: this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8 && this.processedAlphaMask: + + case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8 && this.processedAlphaMask: this.ReadRgbPaletteWithAlphaMask( stream, pixels, @@ -166,7 +171,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8: + + case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8: this.ReadRgbPalette( stream, pixels, @@ -179,6 +185,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore break; + case BmpCompression.RGB when bitsPerPixel is <= 0 or > 32: + BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}"); + break; + case BmpCompression.RLE24: this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); @@ -340,10 +350,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore pixelRow[x] = this.rleSkippedPixelHandling switch { RleSkippedPixelHandling.FirstColorOfPalette => TPixel.FromBgr24(Unsafe.As(ref colors[colorIdx * 4])), - RleSkippedPixelHandling.Transparent => TPixel.FromScaledVector4(Vector4.Zero), + RleSkippedPixelHandling.Transparent => TPixel.FromUnassociatedScaledVector4(Vector4.Zero), // Default handling for skipped pixels is black (which is what System.Drawing is also doing). - _ => TPixel.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)), + _ => TPixel.FromUnassociatedScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)), }; } else @@ -401,10 +411,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore pixelRow[x] = this.rleSkippedPixelHandling switch { RleSkippedPixelHandling.FirstColorOfPalette => TPixel.FromBgr24(Unsafe.As(ref bufferSpan[idx])), - RleSkippedPixelHandling.Transparent => TPixel.FromScaledVector4(Vector4.Zero), + RleSkippedPixelHandling.Transparent => TPixel.FromUnassociatedScaledVector4(Vector4.Zero), // Default handling for skipped pixels is black (which is what System.Drawing is also doing). - _ => TPixel.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)), + _ => TPixel.FromUnassociatedScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)), }; } else @@ -1262,7 +1272,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore g * invMaxValueGreen, b * invMaxValueBlue, alpha); - pixelRow[x] = TPixel.FromScaledVector4(vector4); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(vector4); } else { @@ -1421,12 +1431,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore this.infoHeader = BmpInfoHeader.ParseV5(buffer); if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) { - // Read color profile. long streamPosition = stream.Position; - byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; - stream.Position = infoHeaderStart + this.infoHeader.ProfileData; - stream.Read(iccProfileData); - this.metadata.IccProfile = new IccProfile(iccProfileData); + this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart)); stream.Position = streamPosition; } } @@ -1460,6 +1466,33 @@ internal sealed class BmpDecoderCore : ImageDecoderCore this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height); } + /// + /// Reads the embedded ICC profile from the BMP V5 info header. + /// + /// The containing image data. + /// The image metadata. + /// The stream position where the info header begins. + private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart) + { + byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; + stream.Position = infoHeaderStart + this.infoHeader.ProfileData; + + if (stream.Read(iccProfileData) != iccProfileData.Length) + { + BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile."); + } + + IccProfile profile = new(iccProfileData); + if (profile.CheckIsValid()) + { + imageMetadata.IccProfile = profile; + } + else + { + throw new InvalidIccProfileException("Invalid BMP ICC profile."); + } + } + /// /// Reads the from the stream. /// @@ -1538,6 +1571,12 @@ internal sealed class BmpDecoderCore : ImageDecoderCore case BmpFileMarkerType.Bitmap: if (this.fileHeader.HasValue) { + if (this.fileHeader.Value.Offset > stream.Length) + { + BmpThrowHelper.ThrowInvalidImageContentException( + $"Pixel data offset {this.fileHeader.Value.Offset} exceeds file size {stream.Length}."); + } + colorMapSizeBytes = this.fileHeader.Value.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize; } else diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index e25556804..210c08464 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -13,7 +13,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder /// /// Initializes a new instance of the class. /// - public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree; + public BmpEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree; /// /// Gets the number of bits per pixel. diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index ccc620d6c..0bf57c561 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -116,7 +116,7 @@ internal sealed class BmpEncoderCore this.bitsPerPixel = encoder.BitsPerPixel; // TODO: Use a palette quantizer if supplied. - this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree; + this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.transparentColorMode = encoder.TransparentColorMode; this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; diff --git a/src/ImageSharp/Formats/Bmp/BmpFormat.cs b/src/ImageSharp/Formats/Bmp/BmpFormat.cs index 5dec4a674..50e4090e1 100644 --- a/src/ImageSharp/Formats/Bmp/BmpFormat.cs +++ b/src/ImageSharp/Formats/Bmp/BmpFormat.cs @@ -30,5 +30,5 @@ public sealed class BmpFormat : IImageFormat public IEnumerable FileExtensions => BmpConstants.FileExtensions; /// - public BmpMetadata CreateDefaultFormatMetadata() => new(); + public BmpMetadata CreateDefaultFormatMetadata() => new BmpMetadata(); } diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs index d4d80fc12..916888af3 100644 --- a/src/ImageSharp/Formats/DecoderOptions.cs +++ b/src/ImageSharp/Formats/DecoderOptions.cs @@ -60,7 +60,7 @@ public sealed class DecoderOptions /// /// Gets the segment error handling strategy to use during decoding. /// - public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreNonCritical; + public SegmentIntegrityHandling SegmentIntegrityHandling { get; init; } = SegmentIntegrityHandling.IgnoreAncillary; /// /// Gets a value that controls how ICC profiles are handled during decode. diff --git a/src/ImageSharp/Formats/EncodingUtilities.cs b/src/ImageSharp/Formats/EncodingUtilities.cs index 399474393..64781f4b8 100644 --- a/src/ImageSharp/Formats/EncodingUtilities.cs +++ b/src/ImageSharp/Formats/EncodingUtilities.cs @@ -65,9 +65,9 @@ internal static class EncodingUtilities for (int y = 0; y < region.Height; y++) { Span span = region.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ReplaceTransparentPixels(vectorsSpan); - PixelOperations.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs new file mode 100644 index 000000000..8aca0da65 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; + +/// +/// Compressor for EXR image data which does not use any compression method. +/// +internal class NoneExrCompressor : ExrBaseCompressor +{ + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed image data to. + /// The memory allocator. + /// Bytes per row block. + /// Bytes per pixel row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + } + + /// + public override uint CompressRowBlock(Span rows, int rowCount) + { + this.Output.Write(rows); + return (uint)rows.Length; + } + + /// + protected override void Dispose(bool disposing) + { + } +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs new file mode 100644 index 000000000..01248cc39 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; + +/// +/// Compressor for EXR image data using the ZIP compression. +/// +internal class ZipExrCompressor : ExrBaseCompressor +{ + private readonly DeflateCompressionLevel compressionLevel; + + private readonly MemoryStream memoryStream; + + private readonly System.Buffers.IMemoryOwner buffer; + + /// + /// Initializes a new instance of the class. + /// + /// The stream to write the compressed data to. + /// The memory allocator. + /// The bytes per block. + /// The bytes per row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + /// The compression level for deflate compression. + public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, DeflateCompressionLevel compressionLevel) + : base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + this.compressionLevel = compressionLevel; + this.buffer = allocator.Allocate((int)bytesPerBlock); + this.memoryStream = new(); + } + + /// + public override uint CompressRowBlock(Span rows, int rowCount) + { + // Re-oder pixel values. + Span reordered = this.buffer.GetSpan()[..(int)(rowCount * this.BytesPerRow)]; + int n = reordered.Length; + int t1 = 0; + int t2 = (n + 1) >> 1; + for (int i = 0; i < n; i++) + { + bool isOdd = (i & 1) == 1; + reordered[isOdd ? t2++ : t1++] = rows[i]; + } + + // Predictor. + Span predicted = reordered; + byte p = predicted[0]; + for (int i = 1; i < predicted.Length; i++) + { + int d = (predicted[i] - p + 128 + 256) & 255; + p = predicted[i]; + predicted[i] = (byte)d; + } + + this.memoryStream.Seek(0, SeekOrigin.Begin); + using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) + { + stream.Write(predicted); + stream.Flush(); + } + + int size = (int)this.memoryStream.Position; + byte[] buffer = this.memoryStream.GetBuffer(); + this.Output.Write(buffer, 0, size); + + // Reset memory stream for next pixel row. + this.memoryStream.Seek(0, SeekOrigin.Begin); + this.memoryStream.SetLength(0); + + return (uint)size; + } + + /// + protected override void Dispose(bool disposing) + { + this.buffer.Dispose(); + this.memoryStream?.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs new file mode 100644 index 000000000..06df7b6a2 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -0,0 +1,205 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; + +/// +/// Implementation of B44 decompressor for EXR image data. +/// +internal class B44ExrCompression : ExrBaseDecompressor +{ + private readonly int channelCount; + + private readonly byte[] scratch = new byte[14]; + + private readonly ushort[] s = new ushort[16]; + + private readonly IMemoryOwner tmpBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per row. + /// The pixel rows per block. + /// The width of a pixel row in pixels. + /// The number of channels of the image. + public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + this.channelCount = channelCount; + this.tmpBuffer = allocator.Allocate((int)(width * rowsPerBlock * channelCount)); + } + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span outputBuffer = MemoryMarshal.Cast(buffer); + Span decompressed = this.tmpBuffer.GetSpan(); + int outputOffset = 0; + int bytesLeft = (int)compressedBytes; + for (int i = 0; i < this.channelCount && bytesLeft > 0; i++) + { + for (int y = 0; y < this.RowsPerBlock; y += 4) + { + Span row0 = decompressed.Slice(outputOffset, this.Width); + outputOffset += this.Width; + Span row1 = decompressed.Slice(outputOffset, this.Width); + outputOffset += this.Width; + Span row2 = decompressed.Slice(outputOffset, this.Width); + outputOffset += this.Width; + Span row3 = decompressed.Slice(outputOffset, this.Width); + outputOffset += this.Width; + + int rowOffset = 0; + for (int x = 0; x < this.Width && bytesLeft > 0; x += 4) + { + int bytesRead = stream.Read(this.scratch, 0, 3); + if (bytesRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + } + + // Check if 3-byte encoded flat field. + if (this.scratch[2] >= 13 << 2) + { + Unpack3(this.scratch, this.s); + bytesLeft -= 3; + } + else + { + bytesRead = stream.Read(this.scratch, 3, 11); + if (bytesRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + } + + Unpack14(this.scratch, this.s); + bytesLeft -= 14; + } + + int n = x + 3 < this.Width ? 4 : this.Width - x; + if (y + 3 < this.RowsPerBlock) + { + this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); + this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]); + this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]); + this.s.AsSpan(12, n).CopyTo(row3[rowOffset..]); + } + else + { + this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); + if (y + 1 < this.RowsPerBlock) + { + this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]); + } + + if (y + 2 < this.RowsPerBlock) + { + this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]); + } + } + + rowOffset += 4; + } + + if (bytesLeft <= 0) + { + break; + } + } + } + + // Rearrange the decompressed data such that the data for each scan line form a contiguous block. + int offsetDecompressed = 0; + int offsetOutput = 0; + int blockSize = (int)(this.Width * this.RowsPerBlock); + for (int y = 0; y < this.RowsPerBlock; y++) + { + for (int i = 0; i < this.channelCount; i++) + { + decompressed.Slice(offsetDecompressed + (i * blockSize), this.Width).CopyTo(outputBuffer[offsetOutput..]); + offsetOutput += this.Width; + } + + offsetDecompressed += this.Width; + } + } + + /// + /// Unpack a 14-byte block into 4 by 4 16-bit pixels. + /// + /// The source byte data to unpack. + /// Destintation buffer. + private static void Unpack14(Span b, Span s) + { + s[0] = (ushort)((b[0] << 8) | b[1]); + + ushort shift = (ushort)(b[2] >> 2); + ushort bias = (ushort)(0x20u << shift); + + s[4] = (ushort)(s[0] + ((((b[2] << 4) | (b[3] >> 4)) & 0x3fu) << shift) - bias); + s[8] = (ushort)(s[4] + ((((b[3] << 2) | (b[4] >> 6)) & 0x3fu) << shift) - bias); + s[12] = (ushort)(s[8] + ((b[4] & 0x3fu) << shift) - bias); + + s[1] = (ushort)(s[0] + ((uint)(b[5] >> 2) << shift) - bias); + s[5] = (ushort)(s[4] + ((((b[5] << 4) | (b[6] >> 4)) & 0x3fu) << shift) - bias); + s[9] = (ushort)(s[8] + ((((b[6] << 2) | (b[7] >> 6)) & 0x3fu) << shift) - bias); + s[13] = (ushort)(s[12] + ((b[7] & 0x3fu) << shift) - bias); + + s[2] = (ushort)(s[1] + ((uint)(b[8] >> 2) << shift) - bias); + s[6] = (ushort)(s[5] + ((((b[8] << 4) | (b[9] >> 4)) & 0x3fu) << shift) - bias); + s[10] = (ushort)(s[9] + ((((b[9] << 2) | (b[10] >> 6)) & 0x3fu) << shift) - bias); + s[14] = (ushort)(s[13] + ((b[10] & 0x3fu) << shift) - bias); + + s[3] = (ushort)(s[2] + ((uint)(b[11] >> 2) << shift) - bias); + s[7] = (ushort)(s[6] + ((((b[11] << 4) | (b[12] >> 4)) & 0x3fu) << shift) - bias); + s[11] = (ushort)(s[10] + ((((b[12] << 2) | (b[13] >> 6)) & 0x3fu) << shift) - bias); + s[15] = (ushort)(s[14] + ((b[13] & 0x3fu) << shift) - bias); + + for (int i = 0; i < 16; ++i) + { + if ((s[i] & 0x8000) != 0) + { + s[i] &= 0x7fff; + } + else + { + s[i] = (ushort)~s[i]; + } + } + } + + /// + /// // Unpack a 3-byte block into 4 by 4 identical 16-bit pixels. + /// + /// The source byte data to unpack. + /// The destination buffer. + private static void Unpack3(Span b, Span s) + { + s[0] = (ushort)((b[0] << 8) | b[1]); + + if ((s[0] & 0x8000) != 0) + { + s[0] &= 0x7fff; + } + else + { + s[0] = (ushort)~s[0]; + } + + for (int i = 1; i < 16; ++i) + { + s[i] = s[0]; + } + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs new file mode 100644 index 000000000..19edb31af --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; + +/// +/// Decompressor for EXR image data which do not use any compression. +/// +internal class NoneExrCompression : ExrBaseDecompressor +{ + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per pixel row. + /// The pixel rows per block. + /// The number of pixels per row. + public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + } + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock)); + if (bytesRead != (int)this.BytesPerBlock) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data from the stream!"); + } + } + + /// + protected override void Dispose(bool disposing) + { + } +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs new file mode 100644 index 000000000..f45b660e7 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; + +/// +/// Implementation of PXR24 decompressor for EXR image data. +/// +internal class Pxr24Compression : ExrBaseDecompressor +{ + private readonly IMemoryOwner tmpBuffer; + + private readonly int channelCount; + + private readonly ExrPixelType pixelType; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per pixel row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + /// The number of channels for a pixel. + /// The pixel type. + public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount, ExrPixelType pixelType) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + this.channelCount = channelCount; + this.pixelType = pixelType; + } + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + Span outputBufferHalf = MemoryMarshal.Cast(buffer); + Span outputBufferFloat = MemoryMarshal.Cast(buffer); + Span outputBufferUint = MemoryMarshal.Cast(buffer); + + uint uncompressedBytes = this.BytesPerBlock; + UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); + + int lastIn = 0; + int outputOffset = 0; + for (int y = 0; y < this.RowsPerBlock; y++) + { + for (int c = 0; c < this.channelCount; c++) + { + switch (this.pixelType) + { + case ExrPixelType.UnsignedInt: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + int offsetT2 = lastIn; + lastIn += this.Width; + int offsetT3 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint t2 = uncompressed[offsetT2]; + uint t3 = uncompressed[offsetT3]; + uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8) | t3; + + pixel += diff; + outputBufferUint[outputOffset] = pixel; + + offsetT0++; + offsetT1++; + offsetT2++; + offsetT3++; + outputOffset++; + } + + break; + } + + case ExrPixelType.Half: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint diff = (t0 << 8) | t1; + + pixel += diff; + outputBufferHalf[outputOffset] = (ushort)pixel; + + offsetT0++; + offsetT1++; + outputOffset++; + } + + break; + } + + case ExrPixelType.Float: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + int offsetT2 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint t2 = uncompressed[offsetT2]; + uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8); + + pixel += diff; + outputBufferFloat[outputOffset] = pixel; + + offsetT0++; + offsetT1++; + offsetT2++; + outputOffset++; + } + + break; + } + } + } + } + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs new file mode 100644 index 000000000..f548a8181 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; + +/// +/// Implementation of RLE decompressor for EXR images. +/// +internal class RunLengthExrCompression : ExrBaseDecompressor +{ + private readonly IMemoryOwner tmpBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + int maxLength = (int)this.BytesPerBlock; + int offset = 0; + while (compressedBytes > 0) + { + byte nextByte = ReadNextByte(stream); + + sbyte input = (sbyte)nextByte; + if (input < 0) + { + int count = -input; + compressedBytes -= (uint)(count + 1); + + if ((maxLength -= count) < 0) + { + return; + } + + for (int i = 0; i < count; i++) + { + uncompressed[offset + i] = ReadNextByte(stream); + } + + offset += count; + } + else + { + int count = input; + byte value = ReadNextByte(stream); + compressedBytes -= 2; + + if ((maxLength -= count + 1) < 0) + { + return; + } + + for (int i = 0; i < count + 1; i++) + { + uncompressed[offset + i] = value; + } + + offset += count + 1; + } + } + + Reconstruct(uncompressed, this.BytesPerBlock); + Interleave(uncompressed, this.BytesPerBlock, buffer); + } + + /// + /// Reads the next byte from the stream. + /// + /// The stream. + /// The next byte. + private static byte ReadNextByte(BufferedReadStream stream) + { + int nextByte = stream.ReadByte(); + if (nextByte == -1) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to decompress RLE encoded EXR image!"); + } + + return (byte)nextByte; + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs new file mode 100644 index 000000000..8bab76f40 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; + +/// +/// Implementation of zhe Zip decompressor for EXR image data. +/// +internal class ZipExrCompression : ExrBaseDecompressor +{ + private readonly IMemoryOwner tmpBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per pixel row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + + uint uncompressedBytes = (uint)buffer.Length; + int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); + + Reconstruct(uncompressed, (uint)totalRead); + Interleave(uncompressed, (uint)totalRead, buffer); + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); +} diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs new file mode 100644 index 000000000..72d5be980 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression; + +/// +/// Base class for EXR compression. +/// +internal abstract class ExrBaseCompression : IDisposable +{ + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per block. + /// The bytes per row. + /// The number of pixel rows per block. + /// The number of pixels of a row. + protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + { + this.Allocator = allocator; + this.BytesPerBlock = bytesPerBlock; + this.BytesPerRow = bytesPerRow; + this.RowsPerBlock = rowsPerBlock; + this.Width = width; + } + + /// + /// Gets the memory allocator. + /// + protected MemoryAllocator Allocator { get; } + + /// + /// Gets the bits per pixel. + /// + public int BitsPerPixel { get; } + + /// + /// Gets the bytes per row. + /// + public uint BytesPerRow { get; } + + /// + /// Gets the uncompressed bytes per block. + /// + public uint BytesPerBlock { get; } + + /// + /// Gets the number of pixel rows per block. + /// + public uint RowsPerBlock { get; } + + /// + /// Gets the image width. + /// + public int Width { get; } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.Dispose(true); + } + + protected abstract void Dispose(bool disposing); +} diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs new file mode 100644 index 000000000..f598955f4 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO.Compression; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression; + +/// +/// The base EXR decompressor class. +/// +internal abstract class ExrBaseDecompressor : ExrBaseCompression +{ + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per row block. + /// The bytes per row. + /// The pixel rows per block. + /// The number of pixels per row. + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + { + } + + /// + /// Decompresses the specified stream. + /// + /// The buffered stream to decompress. + /// The compressed bytes. + /// The buffer to write the decompressed data to. + public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + + /// + /// Decompresses zip compressed data. + /// + /// The buffered stream to decompress. + /// The compressed bytes. + /// The buffer to write the uncompressed data to. + /// The uncompressed bytes. + /// The total bytes read from the stream. + protected static int UndoZipCompression(BufferedReadStream stream, uint compressedBytes, Span uncompressed, uint uncompressedBytes) + { + long pos = stream.Position; + using ZlibInflateReader inflateStream = new( + stream, + () => + { + int left = (int)(compressedBytes - (stream.Position - pos)); + return left > 0 ? left : 0; + }); + inflateStream.AllocateNewBytes((int)compressedBytes, true); + using DeflateStream dataStream = inflateStream.CompressedStream!; + + int totalRead = 0; + while (totalRead < uncompressedBytes) + { + int bytesRead = dataStream.Read(uncompressed, totalRead, (int)uncompressedBytes - totalRead); + if (bytesRead <= 0) + { + break; + } + + totalRead += bytesRead; + } + + if (totalRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed EXR image data!"); + } + + return totalRead; + } + + /// + /// Integrate over all differences to the previous value in order to + /// reconstruct sample values. + /// + /// The buffer with the data. + /// The un compressed bytes. + protected static void Reconstruct(Span buffer, uint unCompressedBytes) + { + int offset = 0; + for (int i = 0; i < unCompressedBytes - 1; i++) + { + byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128)); + buffer[offset + 1] = d; + offset++; + } + } + + /// + /// Interleaves the input data. + /// + /// The source data. + /// The uncompressed bytes. + /// The output to write to. + protected static void Interleave(Span source, uint unCompressedBytes, Span output) + { + int sourceOffset = 0; + int offset0 = 0; + int offset1 = (int)((unCompressedBytes + 1) / 2); + while (sourceOffset < unCompressedBytes) + { + output[sourceOffset++] = source[offset0++]; + output[sourceOffset++] = source[offset1++]; + } + } +} diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs new file mode 100644 index 000000000..0a7d4157f --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression; + +/// +/// Factory class for creating a compressor for EXR image data. +/// +internal static class ExrCompressorFactory +{ + /// + /// Creates the specified exr data compressor. + /// + /// The compression method. + /// The memory allocator. + /// The output stream. + /// The bytes per block. + /// The bytes per row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + /// The deflate compression level. + /// A compressor for EXR image data. + public static ExrBaseCompressor Create( + ExrCompression method, + MemoryAllocator allocator, + Stream output, + uint bytesPerBlock, + uint bytesPerRow, + uint rowsPerBlock, + int width, + DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) => method switch + { + ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel), + ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel), + _ => throw ExrThrowHelper.NotSupportedCompressor(method.ToString()), + }; +} diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs new file mode 100644 index 000000000..353d1af11 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression; + +/// +/// The Factory class for creating a EXR data decompressor. +/// +internal static class ExrDecompressorFactory +{ + /// + /// Creates a decomprssor for a specific EXR compression type. + /// + /// The compression method. + /// The memory allocator. + /// The width in pixels of the image. + /// The bytes per block. + /// The bytes per row. + /// The rows per block. + /// The number of image channels. + /// The pixel type. + /// Decompressor for EXR image data. + public static ExrBaseDecompressor Create( + ExrCompression method, + MemoryAllocator memoryAllocator, + int width, + uint bytesPerBlock, + uint bytesPerRow, + uint rowsPerBlock, + int channelCount, + ExrPixelType pixelType) => method switch + { + ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), + ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount, pixelType), + _ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)), + }; +} diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs b/src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs new file mode 100644 index 000000000..d0964bf33 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants; + +/// +/// Enumeration representing the compression formats defined by the EXR file-format. +/// +public enum ExrCompression +{ + /// + /// Pixel data is not compressed. + /// + None = 0, + + /// + /// Differences between horizontally adjacent pixels are run-length encoded. + /// This method is fast, and works well for images with large flat areas, but for photographic images, + /// the compressed file size is usually between 60 and 75 percent of the uncompressed size. + /// Compression is lossless. + /// + RunLengthEncoded = 1, + + /// + /// Uses the open source zlib library for compression. Unlike ZIP compression, this operates one scan line at a time. + /// Compression is lossless. + /// + Zips = 2, + + /// + /// Differences between horizontally adjacent pixels are compressed using the open source zlib library. + /// Unlike ZIPS compression, this operates in in blocks of 16 scan lines. + /// Compression is lossless. + /// + Zip = 3, + + /// + /// A wavelet transform is applied to the pixel data, and the result is Huffman-encoded. + /// Compression is lossless. + /// + Piz = 4, + + /// + /// After reducing 32-bit floating-point data to 24 bits by rounding, differences between horizontally adjacent pixels are compressed with zlib, + /// similar to ZIP. PXR24 compression preserves image channels of type HALF and UINT exactly, but the relative error of FLOAT data increases to about 3×10-5. + /// Compression is lossy. + /// + Pxr24 = 5, + + /// + /// Channels of type HALF are split into blocks of four by four pixels or 32 bytes. Each block is then packed into 14 bytes, + /// reducing the data to 44 percent of their uncompressed size. + /// Compression is lossy. + /// + B44 = 6, + + /// + /// Like B44, except for blocks of four by four pixels where all pixels have the same value, which are packed into 3 instead of 14 bytes. + /// For images with large uniform areas, B44A produces smaller files than B44 compression. + /// Compression is lossy. + /// + B44A = 7 +} diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs new file mode 100644 index 000000000..9453a7d9c --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants; + +/// +/// This enum represents the type of pixel data in the EXR image. +/// +public enum ExrImageDataType +{ + /// + /// The pixel data is unknown. + /// + Unknown = 0, + + /// + /// The pixel data has 3 channels: red, green and blue. + /// + Rgb = 1, + + /// + /// The pixel data has four channels: red, green, blue and a alpha channel. + /// + Rgba = 2, + + /// + /// There is only one channel with the luminance. + /// + Gray = 3, +} diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs new file mode 100644 index 000000000..9e2cd009f --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants; + +/// +/// Enum for the differnt exr image type. +/// +internal enum ExrImageType +{ + /// + /// The image data is stored in scan lines. + /// + ScanLine = 0, + + /// + /// The image data is stored in tile. + /// This is not yet supported. + /// + Tiled = 1 +} diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs b/src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs new file mode 100644 index 000000000..73e86d6ef --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants; + +/// +/// Enum for the different scan line ordering. +/// +internal enum ExrLineOrder : byte +{ + /// + /// The scan lines are written from top-to-bottom. + /// + IncreasingY = 0, + + /// + /// The scan lines are written from bottom-to-top. + /// + DecreasingY = 1, + + /// + /// The Scan lines are written in no particular oder. + /// + RandomY = 2 +} diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs new file mode 100644 index 000000000..cddc43d59 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Constants; + +/// +/// The different pixel formats for a OpenEXR image. +/// +public enum ExrPixelType +{ + /// + /// unsigned int (32 bit). + /// + UnsignedInt = 0, + + /// + /// half (16 bit floating point). + /// + Half = 1, + + /// + /// float (32 bit floating point). + /// + Float = 2 +} diff --git a/src/ImageSharp/Formats/Exr/ExrAttribute.cs b/src/ImageSharp/Formats/Exr/ExrAttribute.cs new file mode 100644 index 000000000..093c97391 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrAttribute.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Repressents an exr image attribute. +/// +[DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")] +internal class ExrAttribute +{ + public static readonly ExrAttribute EmptyAttribute = new(string.Empty, string.Empty, 0); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the attribute. + /// The type of the attribute. + /// The length in bytes. + public ExrAttribute(string name, string type, int length) + { + this.Name = name; + this.Type = type; + this.Length = length; + } + + /// + /// Gets the name of the attribute. + /// + public string Name { get; } + + /// + /// Gets the type of the attribute. + /// + public string Type { get; } + + /// + /// Gets the length in bytes of the attribute. + /// + public int Length { get; } +} diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs new file mode 100644 index 000000000..9e3fe8ec6 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression; + +internal abstract class ExrBaseCompressor : ExrBaseCompression +{ + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed image to. + /// The memory allocator. + /// Bytes per row block. + /// Bytes per pixel row. + /// The pixel rows per block. + /// The number of pixels per row. + protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) + => this.Output = output; + + /// + /// Gets the output stream to write the compressed image to. + /// + public Stream Output { get; } + + /// + /// Compresses a block of rows of the image. + /// + /// Image rows to compress. + /// The number of rows to compress. + /// Number of bytes of of the compressed data. + public abstract uint CompressRowBlock(Span rows, int rowCount); +} diff --git a/src/ImageSharp/Formats/Exr/ExrBox2i.cs b/src/ImageSharp/Formats/Exr/ExrBox2i.cs new file mode 100644 index 000000000..c9c53d648 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrBox2i.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Integer region definition. +/// +[DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")] +internal readonly struct ExrBox2i +{ + /// + /// Initializes a new instance of the struct. + /// + /// The minimum x value. + /// The minimum y value. + /// The maximum x value. + /// The maximum y value. + public ExrBox2i(int xMin, int yMin, int xMax, int yMax) + { + this.XMin = xMin; + this.YMin = yMin; + this.XMax = xMax; + this.YMax = yMax; + } + + /// + /// Gets the minimum x value. + /// + public int XMin { get; } + + /// + /// Gets the minimum y value. + /// + public int YMin { get; } + + /// + /// Gets the maximum x value. + /// + public int XMax { get; } + + /// + /// Gets the maximum y value. + /// + public int YMax { get; } +} diff --git a/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs b/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs new file mode 100644 index 000000000..f9269f786 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Exr.Constants; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Information about a pixel channel. +/// +[DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")] +[StructLayout(LayoutKind.Sequential, Pack = 1)] +internal readonly struct ExrChannelInfo +{ + /// + /// Initializes a new instance of the struct. + /// + /// Name of the channel. + /// The type of the pixel data. + /// Linear flag, possible values are 0 and 1. + /// X sampling. + /// Y sampling. + public ExrChannelInfo(string channelName, ExrPixelType pixelType, byte linear, int xSampling, int ySampling) + { + this.ChannelName = channelName; + this.PixelType = pixelType; + this.Linear = linear; + this.XSampling = xSampling; + this.YSampling = ySampling; + } + + /// + /// Gets the channel name. + /// + public string ChannelName { get; } + + /// + /// Gets the type of the pixel data. + /// + public ExrPixelType PixelType { get; } + + /// + /// Gets the linear flag. Hint to lossy compression methods that indicates whether + /// human perception of the quantity represented by this channel + /// is closer to linear or closer to logarithmic. + /// + public byte Linear { get; } + + /// + /// Gets the x sampling value. + /// + public int XSampling { get; } + + /// + /// Gets the y sampling value. + /// + public int YSampling { get; } +} diff --git a/src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs b/src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs new file mode 100644 index 000000000..a2f1b8c7a --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Registers the image encoders, decoders and mime type detectors for the OpenExr format. +/// +public sealed class ExrConfigurationModule : IImageFormatConfigurationModule +{ + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder()); + configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, ExrDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector()); + } +} diff --git a/src/ImageSharp/Formats/Exr/ExrConstants.cs b/src/ImageSharp/Formats/Exr/ExrConstants.cs new file mode 100644 index 000000000..214dbd832 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrConstants.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Defines constants relating to OpenExr images. +/// +internal static class ExrConstants +{ + /// + /// The list of mimetypes that equate to a OpenExr image. + /// + public static readonly IEnumerable MimeTypes = new[] { "image/x-exr" }; + + /// + /// The list of file extensions that equate to a OpenExr image. + /// + public static readonly IEnumerable FileExtensions = new[] { "exr" }; + + /// + /// The magick bytes identifying an OpenExr image. + /// + public static readonly int MagickBytes = 20000630; + + /// + /// EXR attribute names. + /// + internal static class AttributeNames + { + public const string Channels = "channels"; + + public const string Compression = "compression"; + + public const string DataWindow = "dataWindow"; + + public const string DisplayWindow = "displayWindow"; + + public const string LineOrder = "lineOrder"; + + public const string PixelAspectRatio = "pixelAspectRatio"; + + public const string ScreenWindowCenter = "screenWindowCenter"; + + public const string ScreenWindowWidth = "screenWindowWidth"; + + public const string Tiles = "tiles"; + + public const string ChunkCount = "chunkCount"; + } + + /// + /// EXR attribute types. + /// + internal static class AttibuteTypes + { + public const string ChannelList = "chlist"; + + public const string Compression = "compression"; + + public const string Float = "float"; + + public const string LineOrder = "lineOrder"; + + public const string TwoFloat = "v2f"; + + public const string BoxInt = "box2i"; + } + + internal static class ChannelNames + { + public const string Red = "R"; + + public const string Green = "G"; + + public const string Blue = "B"; + + public const string Alpha = "A"; + + public const string Luminance = "Y"; + } +} diff --git a/src/ImageSharp/Formats/Exr/ExrDecoder.cs b/src/ImageSharp/Formats/Exr/ExrDecoder.cs new file mode 100644 index 000000000..2e2771728 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrDecoder.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Image decoder for generating an image out of a OpenExr stream. +/// +public class ExrDecoder : ImageDecoder +{ + private ExrDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static ExrDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new ExrDecoderCore(new ExrDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + ExrDecoderCore decoder = new(new ExrDecoderOptions { GeneralOptions = options }); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); +} diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs new file mode 100644 index 000000000..abe3ae439 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -0,0 +1,1023 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System.Buffers; +using System.Buffers.Binary; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; +using SixLabors.ImageSharp.Formats.Exr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Performs the OpenExr decoding operation. +/// +internal sealed class ExrDecoderCore : ImageDecoderCore +{ + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[8]; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The metadata. + /// + private ImageMetadata metadata; + + /// + /// The exr specific metadata. + /// + private ExrMetadata exrMetadata; + + /// + /// Initializes a new instance of the class. + /// + /// The options. + public ExrDecoderCore(ExrDecoderOptions options) + : base(options.GeneralOptions) + { + this.configuration = options.GeneralOptions.Configuration; + this.memoryAllocator = this.configuration.MemoryAllocator; + } + + /// + /// Gets or sets the image width. + /// + private int Width { get; set; } + + /// + /// Gets or sets the image height. + /// + private int Height { get; set; } + + /// + /// Gets or sets the image channel info's. + /// + private IList Channels { get; set; } + + /// + /// Gets or sets the compression method. + /// + private ExrCompression Compression { get; set; } + + /// + /// Gets or sets the image data type, either RGB, RGBA or gray. + /// + private ExrImageDataType ImageDataType { get; set; } + + /// + /// Gets or sets the pixel type. + /// + private ExrPixelType PixelType { get; set; } + + /// + /// Gets or sets the header attributes. + /// + private ExrHeaderAttributes HeaderAttributes { get; set; } + + /// + /// Gets or sets the earliest valid stream position for a scanline chunk. + /// + private long MinimumChunkOffset { get; set; } + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ReadExrHeader(stream); + if (!this.IsSupportedCompression()) + { + ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + } + + Image image = null; + try + { + image = new Image(this.configuration, this.Width, this.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + switch (this.PixelType) + { + case ExrPixelType.Half: + case ExrPixelType.Float: + this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken); + break; + case ExrPixelType.UnsignedInt: + this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken); + break; + default: + ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); + break; + } + + return image; + } + catch + { + image?.Dispose(); + throw; + } + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + ExrHeaderAttributes header = this.ReadExrHeader(stream); + + return new ImageInfo(new Size(header.DataWindow.XMax, header.DataWindow.YMax), this.metadata); + } + + /// + /// Decodes image data with floating point pixel data. + /// + /// The type of the pixels. + /// The stream to read from. + /// The pixel buffer. + /// The cancellation token. + private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + bool hasAlpha = this.HasAlpha(); + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerBlock > int.MaxValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size."); + } + + int width = this.Width; + int height = this.Height; + int channelCount = this.Channels.Count; + + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); + Span redPixelData = rowBuffer.GetSpan()[..width]; + Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); + Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); + Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create( + this.Compression, + this.memoryAllocator, + width, + (uint)bytesPerBlock, + (uint)bytesPerRow, + rowsPerBlock, + channelCount, + this.PixelType); + + int decodedRows = 0; + while (decodedRows < height) + { + ulong rowOffset = this.ReadUnsignedLong(stream); + long nextRowOffsetPosition = stream.Position; + + this.ValidateChunkOffset(rowOffset, stream); + stream.Position = (long)rowOffset; + uint rowStartIndex = this.ReadUnsignedInteger(stream); + + uint compressedBytesCount = this.ReadUnsignedInteger(stream); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + + int offset = 0; + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) + { + ExrChannelInfo channel = this.Channels[channelIdx]; + offset += ReadFloatChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + } + + for (int x = 0; x < width; x++) + { + Vector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1F); + + // OpenEXR channels are associated color values, not values in the destination pixel format's native numeric range. + pixelRow[x] = TPixel.FromAssociatedScaledVector4(pixelValue); + } + + decodedRows++; + } + + stream.Position = nextRowOffsetPosition; + + cancellationToken.ThrowIfCancellationRequested(); + } + } + + /// + /// Decodes image data with unsigned int pixel data. + /// + /// The type of the pixels. + /// The stream to read from. + /// The pixel buffer. + /// The cancellation token. + private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + bool hasAlpha = this.HasAlpha(); + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerBlock > int.MaxValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size."); + } + + int width = this.Width; + int height = this.Height; + int channelCount = this.Channels.Count; + + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); + Span redPixelData = rowBuffer.GetSpan()[..width]; + Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); + Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); + Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create( + this.Compression, + this.memoryAllocator, + width, + (uint)bytesPerBlock, + (uint)bytesPerRow, + rowsPerBlock, + channelCount, + this.PixelType); + + int decodedRows = 0; + while (decodedRows < height) + { + ulong rowOffset = this.ReadUnsignedLong(stream); + long nextRowOffsetPosition = stream.Position; + + this.ValidateChunkOffset(rowOffset, stream); + stream.Position = (long)rowOffset; + uint rowStartIndex = this.ReadUnsignedInteger(stream); + + uint compressedBytesCount = this.ReadUnsignedInteger(stream); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + + int offset = 0; + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) + { + ExrChannelInfo channel = this.Channels[channelIdx]; + offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + } + + for (int x = 0; x < width; x++) + { + Rgba128 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); + + // Rgba128 normalizes the unsigned channel values before the destination applies its own numeric representation. + pixelRow[x] = TPixel.FromAssociatedScaledVector4(pixelValue.ToVector4()); + } + + decodedRows++; + } + + stream.Position = nextRowOffsetPosition; + + cancellationToken.ThrowIfCancellationRequested(); + } + } + + /// + /// Reads float image channel data. + /// + /// The stream to read from. + /// The channel info. + /// The decompressed pixel data. + /// The red channel pixel data. + /// The green channel pixel data. + /// The blue channel pixel data. + /// The alpha channel pixel data. + /// The width of a row in pixels. + /// The bytes read. + private static int ReadFloatChannelData( + BufferedReadStream stream, + ExrChannelInfo channel, + Span decompressedPixelData, + Span redPixelData, + Span greenPixelData, + Span bluePixelData, + Span alphaPixelData, + int width) + { + switch (channel.ChannelName) + { + case ExrConstants.ChannelNames.Red: + return ReadChannelData(channel, decompressedPixelData, redPixelData, width); + + case ExrConstants.ChannelNames.Blue: + return ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + + case ExrConstants.ChannelNames.Green: + return ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + + case ExrConstants.ChannelNames.Alpha: + return ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + + case ExrConstants.ChannelNames.Luminance: + int bytesRead = ReadChannelData(channel, decompressedPixelData, redPixelData, width); + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); + + return bytesRead; + + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += width * channelDataSizeInBytes; + return channelDataSizeInBytes; + } + } + + /// + /// Reads UINT image channel data. + /// + /// The stream to read from. + /// The channel info. + /// The decompressed pixel data. + /// The red channel pixel data. + /// The green channel pixel data. + /// The blue channel pixel data. + /// The alpha channel pixel data. + /// The width of a row in pixels. + /// The bytes read. + private int ReadUnsignedIntChannelData( + BufferedReadStream stream, + ExrChannelInfo channel, + Span decompressedPixelData, + Span redPixelData, + Span greenPixelData, + Span bluePixelData, + Span alphaPixelData, + int width) + { + switch (channel.ChannelName) + { + case ExrConstants.ChannelNames.Red: + return ReadChannelData(channel, decompressedPixelData, redPixelData, width); + + case ExrConstants.ChannelNames.Blue: + return ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + + case ExrConstants.ChannelNames.Green: + return ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + + case ExrConstants.ChannelNames.Alpha: + return ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + + case ExrConstants.ChannelNames.Luminance: + int bytesRead = ReadChannelData(channel, decompressedPixelData, redPixelData, width); + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); + return bytesRead; + + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += this.Width * channelDataSizeInBytes; + return channelDataSizeInBytes; + } + } + + /// + /// Reads the channel data for pixel type HALF or FLOAT. + /// + /// The channel info. + /// The decompressed pixel data. + /// The pixel data as float. + /// The width in pixel of a row. + /// The bytes read. + private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) => channel.PixelType switch + { + ExrPixelType.Half => ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width), + ExrPixelType.Float => ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width), + _ => 0, + }; + + /// + /// Reads the channel data for pixel type UINT. + /// + /// The channel info. + /// The decompressed pixel data. + /// The pixel data as uint. + /// The width in pixels. + /// The bytes read. + private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) => channel.PixelType switch + { + ExrPixelType.UnsignedInt => ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width), + _ => 0, + }; + + /// + /// Reads a pixel row with the pixel data being 16 bit half values. + /// + /// The decompressed pixel data. + /// The channel data as float. + /// The width of a row in pixels. + /// The bytes read. + private static int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + ushort shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); + channelData[x] = HalfTypeHelper.Unpack(shortValue); + offset += 2; + } + + return offset; + } + + /// + /// Reads a pixel row with 32 bit float pixel data. + /// + /// The decompressed pixel data. + /// The pixel data as float. + /// The width in pixels of a row. + /// The bytes read. + private static int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + int intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + channelData[x] = Unsafe.As(ref intValue); + offset += 4; + } + + return offset; + } + + /// + /// Reads a pixel row with the pixel typ UINT. + /// + /// The decompressed pixel bytes. + /// The uint pixel data. + /// The width of a row in pixels. + /// The bytes read. + private static int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + offset += 4; + } + + return offset; + } + + /// + /// Validates that all image channels have the same type and are among the supported pixel types. + /// + /// The pixel type. + private ExrPixelType ValidateChannels() + { + if (this.Channels.Count == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("At least one channel of pixel data is expected!"); + } + + // Find pixel the type of any channel which is R, G, B or A. + ExrPixelType? pixelType = null; + for (int i = 0; i < this.Channels.Count; i++) + { + if (this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Blue, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Green, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Red, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Alpha, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Luminance, StringComparison.Ordinal)) + { + if (!pixelType.HasValue) + { + pixelType = this.Channels[i].PixelType; + } + else + { + if (pixelType != this.Channels[i].PixelType) + { + ExrThrowHelper.ThrowNotSupported("Pixel channel data is expected to be the same for all channels."); + } + } + } + } + + if (!pixelType.HasValue) + { + ExrThrowHelper.ThrowNotSupported("Pixel channel data is unknown! Only R, G, B, A and Y are supported."); + } + + return pixelType.Value; + } + + /// + /// Determines the type image from the channel information. + /// + /// The image data type. + private ExrImageDataType DetermineImageDataType() + { + bool hasRedChannel = false; + bool hasGreenChannel = false; + bool hasBlueChannel = false; + bool hasAlphaChannel = false; + bool hasLuminance = false; + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)) + { + hasAlphaChannel = true; + } + + if (channelInfo.ChannelName.Equals("R", StringComparison.Ordinal)) + { + hasRedChannel = true; + } + + if (channelInfo.ChannelName.Equals("G", StringComparison.Ordinal)) + { + hasGreenChannel = true; + } + + if (channelInfo.ChannelName.Equals("B", StringComparison.Ordinal)) + { + hasBlueChannel = true; + } + + if (channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) + { + hasLuminance = true; + } + } + + if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel) + { + return ExrImageDataType.Rgba; + } + + if (hasRedChannel && hasGreenChannel && hasBlueChannel) + { + return ExrImageDataType.Rgb; + } + + if (hasLuminance && this.Channels.Count == 1) + { + return ExrImageDataType.Gray; + } + + return ExrImageDataType.Unknown; + } + + /// + /// Reads the exr image header. + /// + /// + /// The stream. + /// The image header attributes. + private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) + { + // Skip over the magick bytes, we already know its an EXR image. + stream.Skip(4); + + // Read version number. + byte version = (byte)stream.ReadByte(); + if (version != 2) + { + ExrThrowHelper.ThrowNotSupportedVersion(); + } + + // Next three bytes contain info's about the image. + byte flagsByte0 = (byte)stream.ReadByte(); + if ((flagsByte0 & (1 << 1)) != 0) + { + ExrThrowHelper.ThrowNotSupported("Decoding tiled exr images is not supported yet!"); + } + + // Discard the next two bytes. + int bytesRead = stream.Read(this.buffer, 0, 2); + if (bytesRead != 2) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for exr file!"); + } + + this.HeaderAttributes = this.ParseHeaderAttributes(stream); + + ExrBox2i dataWindow = this.HeaderAttributes.DataWindow; + if (dataWindow.XMax < dataWindow.XMin || dataWindow.YMax < dataWindow.YMin) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow max values must be greater than or equal to min values."); + } + + long width = (long)dataWindow.XMax - dataWindow.XMin + 1; + long height = (long)dataWindow.YMax - dataWindow.YMin + 1; + + // Decoding stages each row as four color planes, so the width must be bounded + // before later width * 4 buffer sizing can overflow. + if (width > int.MaxValue / 4 || height > int.MaxValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow dimensions exceed the maximum allowed size."); + } + + this.Width = (int)width; + this.Height = (int)height; + this.Channels = this.HeaderAttributes.Channels; + this.Compression = this.HeaderAttributes.Compression; + uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); + long chunkCount = (this.Height + rowsPerBlock - 1) / rowsPerBlock; + long offsetTableByteCount = chunkCount * sizeof(ulong); + + // The scanline offset table sits between the header and pixel chunks; proving it + // fits in the stream keeps all later chunk offsets on the pixel-data side. + if (stream.Position > stream.Length || offsetTableByteCount > stream.Length - stream.Position) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset table is outside the bounds of the stream."); + } + + this.MinimumChunkOffset = stream.Position + offsetTableByteCount; + this.PixelType = this.ValidateChannels(); + this.ImageDataType = this.DetermineImageDataType(); + + this.metadata = new ImageMetadata(); + + this.exrMetadata = this.metadata.GetExrMetadata(); + this.exrMetadata.PixelType = this.PixelType; + this.exrMetadata.ImageDataType = this.ImageDataType; + this.exrMetadata.Compression = this.Compression; + + return this.HeaderAttributes; + } + + /// + /// Parses the image header attributes. + /// + /// The stream to read from. + /// The image header attributes. + private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) + { + ExrAttribute attribute = this.ReadAttribute(stream); + + IList channels = null; + ExrBox2i? dataWindow = null; + ExrCompression? compression = null; + ExrBox2i? displayWindow = null; + ExrLineOrder? lineOrder = null; + float? aspectRatio = null; + float? screenWindowCenterX = null; + float? screenWindowCenterY = null; + float? screenWindowWidth = null; + uint? tileXSize = null; + uint? tileYSize = null; + int? chunkCount = null; + while (!attribute.Equals(ExrAttribute.EmptyAttribute)) + { + switch (attribute.Name) + { + case ExrConstants.AttributeNames.Channels: + channels = this.ReadChannelList(stream, attribute.Length); + break; + case ExrConstants.AttributeNames.Compression: + compression = (ExrCompression)stream.ReadByte(); + break; + case ExrConstants.AttributeNames.DataWindow: + dataWindow = this.ReadBoxInteger(stream); + break; + case ExrConstants.AttributeNames.DisplayWindow: + displayWindow = this.ReadBoxInteger(stream); + break; + case ExrConstants.AttributeNames.LineOrder: + lineOrder = (ExrLineOrder)stream.ReadByte(); + break; + case ExrConstants.AttributeNames.PixelAspectRatio: + aspectRatio = this.ReadSingle(stream); + break; + case ExrConstants.AttributeNames.ScreenWindowCenter: + screenWindowCenterX = this.ReadSingle(stream); + screenWindowCenterY = this.ReadSingle(stream); + break; + case ExrConstants.AttributeNames.ScreenWindowWidth: + screenWindowWidth = this.ReadSingle(stream); + break; + case ExrConstants.AttributeNames.Tiles: + tileXSize = this.ReadUnsignedInteger(stream); + tileYSize = this.ReadUnsignedInteger(stream); + break; + case ExrConstants.AttributeNames.ChunkCount: + chunkCount = this.ReadSignedInteger(stream); + break; + default: + // Skip unknown attribute bytes. + stream.Skip(attribute.Length); + break; + } + + attribute = this.ReadAttribute(stream); + } + + if (!displayWindow.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the displayWindow attribute is missing!"); + } + + if (!dataWindow.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the dataWindow attribute is missing!"); + } + + if (channels is null) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the channels attribute is missing!"); + } + + if (!compression.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the compression attribute is missing!"); + } + + if (!lineOrder.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the lineOrder attribute is missing!"); + } + + if (!aspectRatio.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the aspectRatio attribute is missing!"); + } + + if (!screenWindowWidth.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowWidth attribute is missing!"); + } + + if (!screenWindowCenterX.HasValue || !screenWindowCenterY.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowCenter attribute is missing!"); + } + + ExrHeaderAttributes header = new( + channels, + compression.Value, + dataWindow.Value, + displayWindow.Value, + lineOrder.Value, + aspectRatio.Value, + screenWindowWidth.Value, + new PointF(screenWindowCenterX.Value, screenWindowCenterY.Value), + tileXSize, + tileYSize, + chunkCount); + return header; + } + + /// + /// Reads a attrbute from the stream, which consist of a name, a type and a size in bytes. + /// + /// The stream to read from. + /// A attribute. + private ExrAttribute ReadAttribute(BufferedReadStream stream) + { + string attributeName = ReadString(stream); + if (attributeName.Equals(string.Empty, StringComparison.Ordinal)) + { + return ExrAttribute.EmptyAttribute; + } + + string attributeType = ReadString(stream); + int attributeSize = this.ReadSignedInteger(stream); + + return new ExrAttribute(attributeName, attributeType, attributeSize); + } + + /// + /// Reads a box attribute, which is a xMin, xMax and yMin, yMax value. + /// + /// The stream to reaad from. + /// A box struct. + private ExrBox2i ReadBoxInteger(BufferedReadStream stream) + { + int xMin = this.ReadSignedInteger(stream); + int yMin = this.ReadSignedInteger(stream); + int xMax = this.ReadSignedInteger(stream); + int yMax = this.ReadSignedInteger(stream); + + return new ExrBox2i(xMin, yMin, xMax, yMax); + } + + /// + /// Reads the channel list from the stream. + /// + /// The stream to read from. + /// The size in bytes of the channel list attribute. + /// The channel list. + private List ReadChannelList(BufferedReadStream stream, int attributeSize) + { + List channels = []; + while (attributeSize > 1) + { + ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead); + channels.Add(channelInfo); + attributeSize -= bytesRead; + } + + // Last byte should be a null byte. + if (stream.ReadByte() == -1) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data to read the exr channel list!"); + } + + return channels; + } + + /// + /// Reads the channel information from the stream. + /// + /// The stream to read from. + /// The bytes read. + /// Channel info. + private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesRead) + { + string channelName = ReadString(stream); + bytesRead = channelName.Length + 1; + + ExrPixelType pixelType = (ExrPixelType)this.ReadSignedInteger(stream); + bytesRead += 4; + + byte pLinear = (byte)stream.ReadByte(); + + // Next 3 bytes are reserved bytes and not use. + if (stream.Read(this.buffer, 0, 3) != 3) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data to read exr channel info!"); + } + + bytesRead += 4; + + int xSampling = this.ReadSignedInteger(stream); + bytesRead += 4; + + int ySampling = this.ReadSignedInteger(stream); + bytesRead += 4; + + return new ExrChannelInfo(channelName, pixelType, pLinear, xSampling, ySampling); + } + + /// + /// Reads a the string from the stream. + /// + /// The stream to read from. + /// A string. + private static string ReadString(BufferedReadStream stream) + { + StringBuilder str = new(); + int character = stream.ReadByte(); + if (character == 0) + { + // End of file header reached. + return string.Empty; + } + + while (character != 0) + { + if (character == -1) + { + ExrThrowHelper.ThrowInvalidImageHeader(); + } + + str.Append((char)character); + character = stream.ReadByte(); + } + + return str.ToString(); + } + + /// + /// Determines whether the compression is supported. + /// + /// True if the compression is supported; otherwise, false>. + private bool IsSupportedCompression() => this.Compression switch + { + ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 or ExrCompression.Pxr24 => true, + _ => false, + }; + + /// + /// Validates a scanline chunk offset read from the EXR offset table. + /// + /// The chunk offset to validate. + /// The stream containing the image data. + private void ValidateChunkOffset(ulong chunkOffset, BufferedReadStream stream) + { + if (chunkOffset < (ulong)this.MinimumChunkOffset || chunkOffset >= (ulong)stream.Length) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset is outside the bounds of the stream."); + } + } + + /// + /// Determines whether this image has alpha channel. + /// + /// True if this image has a alpha channel; otherwise, false. + private bool HasAlpha() + { + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + /// + /// Reads a unsigned long value from the stream. + /// + /// The stream to read the data from. + /// The unsigned long value. + private ulong ReadUnsignedLong(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 8); + if (bytesRead != 8) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a unsigned long from the stream!"); + } + + return BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); + } + + /// + /// Reads a unsigned integer value from the stream. + /// + /// The stream to read the data from. + /// The integer value. + private uint ReadUnsignedInteger(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a unsigned int from the stream!"); + } + + return BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + } + + /// + /// Reads a signed integer value from the stream. + /// + /// The stream to read the data from. + /// The integer value. + private int ReadSignedInteger(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a signed int from the stream!"); + } + + return BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + } + + /// + /// Reads a float value from the stream. + /// + /// The stream to read the data from. + /// The float value. + private float ReadSingle(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to read a float value from the stream!"); + } + + int intValue = BinaryPrimitives.ReadInt32BigEndian(this.buffer); + + return Unsafe.As(ref intValue); + } +} diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs b/src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs new file mode 100644 index 000000000..a8cda0b35 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Image decoder options for decoding OpenExr streams. +/// +public sealed class ExrDecoderOptions : ISpecializedDecoderOptions +{ + /// + public DecoderOptions GeneralOptions { get; init; } = new(); +} diff --git a/src/ImageSharp/Formats/Exr/ExrEncoder.cs b/src/ImageSharp/Formats/Exr/ExrEncoder.cs new file mode 100644 index 000000000..2ea1b9116 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrEncoder.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Image encoder for writing an image to a stream in the OpenExr Format. +/// +public sealed class ExrEncoder : ImageEncoder +{ + /// + /// Gets or sets the pixel type of the image. + /// + public ExrPixelType? PixelType { get; set; } + + /// + /// Gets the compression type to use. + /// + public ExrCompression? Compression { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + ExrEncoderCore encoder = new(this, image.Configuration, image.Configuration.MemoryAllocator); + encoder.Encode(image, stream, cancellationToken); + } +} diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs new file mode 100644 index 000000000..bd74a46b7 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -0,0 +1,710 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Exr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Image encoder for writing an image to a stream in the OpenExr format. +/// +internal sealed class ExrEncoderCore +{ + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[8]; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The encoder with options. + /// + private readonly ExrEncoder encoder; + + /// + /// The pixel type of the image. + /// + private ExrPixelType? pixelType; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder with options. + /// The configuration. + /// The memory manager. + public ExrEncoderCore(ExrEncoder encoder, Configuration configuration, MemoryAllocator memoryAllocator) + { + this.configuration = configuration; + this.encoder = encoder; + this.memoryAllocator = memoryAllocator; + this.Compression = encoder.Compression ?? ExrCompression.None; + this.pixelType = encoder.PixelType; + } + + /// + /// Gets or sets the compression implementation to use when encoding the image. + /// + internal ExrCompression Compression { get; set; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; + + ImageMetadata metadata = image.Metadata; + ExrMetadata exrMetadata = metadata.GetExrMetadata(); + this.pixelType ??= exrMetadata.PixelType; + int width = image.Width; + int height = image.Height; + float aspectRatio = 1.0f; + ExrBox2i dataWindow = new(0, 0, width - 1, height - 1); + ExrBox2i displayWindow = new(0, 0, width - 1, height - 1); + ExrLineOrder lineOrder = ExrLineOrder.IncreasingY; + PointF screenWindowCenter = new(0.0f, 0.0f); + int screenWindowWidth = 1; + List channels = + [ + new(ExrConstants.ChannelNames.Alpha, this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Blue, this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Green, this.pixelType.Value, 0, 1, 1), + new(ExrConstants.ChannelNames.Red, this.pixelType.Value, 0, 1, 1), + ]; + ExrHeaderAttributes header = new( + channels, + this.Compression, + dataWindow, + displayWindow, + lineOrder, + aspectRatio, + screenWindowWidth, + screenWindowCenter); + + // Write magick bytes. + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes); + stream.Write(this.buffer.AsSpan(0, 4)); + + // Version number. + this.buffer[0] = 2; + + // Second, third and fourth bytes store info about the image, set all to default: zero. + this.buffer[1] = 0; + this.buffer[2] = 0; + this.buffer[3] = 0; + stream.Write(this.buffer.AsSpan(0, 4)); + + // Write EXR header. + this.WriteHeader(stream, header); + + // Next is offsets table to each pixel row, which will be written after the pixel data was written. + ulong startOfRowOffsetData = (ulong)stream.Position; + stream.Position += 8 * height; + + // Write pixel data. + switch (this.pixelType) + { + case ExrPixelType.Half: + case ExrPixelType.Float: + { + ulong[] rowOffsets = this.EncodeFloatingPointPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken); + stream.Position = (long)startOfRowOffsetData; + this.WriteRowOffsets(stream, height, rowOffsets); + break; + } + + case ExrPixelType.UnsignedInt: + { + ulong[] rowOffsets = this.EncodeUnsignedIntPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken); + stream.Position = (long)startOfRowOffsetData; + this.WriteRowOffsets(stream, height, rowOffsets); + break; + } + } + } + + /// + /// Encodes and writes pixel data with float pixel data to the stream. + /// + /// The type of the pixels. + /// The stream to write to. + /// The pixel bufer. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The imagechannels. + /// The compression to use. + /// The cancellation token. + /// The array of pixel row offsets. + private ulong[] EncodeFloatingPointPixelData( + Stream stream, + Buffer2D pixels, + int width, + int height, + List channels, + ExrCompression compression, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue) + { + throw new ImageFormatException("Image is too large to encode in EXR format."); + } + + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); + using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); + Span redBuffer = rgbBuffer.GetSpan()[..width]; + Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width); + + ulong[] rowOffsets = new ulong[height]; + for (uint y = 0; y < height; y += rowsPerBlock) + { + rowOffsets[y] = (ulong)stream.Position; + + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. + long pixelDataSizePos = stream.Position; + stream.Position = pixelDataSizePos + 4; + + uint rowsInBlockCount = 0; + for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex); + for (int x = 0; x < width; x++) + { + // OpenEXR stores RGB associated with alpha. Use the native vector domain so floating-point and HDR component + // ranges are preserved instead of being clamped through the scaled [0, 1] representation. + Vector4 vector4 = pixelRowSpan[x].ToAssociatedVector4(); + redBuffer[x] = vector4.X; + greenBuffer[x] = vector4.Y; + blueBuffer[x] = vector4.Z; + alphaBuffer[x] = vector4.W; + } + + // Write pixel data to row block buffer. + Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow); + switch (this.pixelType) + { + case ExrPixelType.Float: + WriteSingleRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer); + break; + case ExrPixelType.Half: + WriteHalfSingleRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer); + break; + } + + rowsInBlockCount++; + } + + // Write compressed pixel row data to the stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount); + long positionAfterPixelData = stream.Position; + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); + stream.Position = pixelDataSizePos; + stream.Write(this.buffer.AsSpan(0, 4)); + stream.Position = positionAfterPixelData; + + cancellationToken.ThrowIfCancellationRequested(); + } + + return rowOffsets; + } + + /// + /// Encodes and writes pixel data with the unsigned int pixel type to the stream. + /// + /// The type of the pixels. + /// The stream to write to. + /// The pixel bufer. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The imagechannels. + /// The compression to use. + /// The cancellation token. + /// The array of pixel row offsets. + private ulong[] EncodeUnsignedIntPixelData( + Stream stream, + Buffer2D pixels, + int width, + int height, + List channels, + ExrCompression compression, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue) + { + throw new ImageFormatException("Image is too large to encode in EXR format."); + } + + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); + using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); + Span redBuffer = rgbBuffer.GetSpan()[..width]; + Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width); + + Rgba128 rgb = default; + ulong[] rowOffsets = new ulong[height]; + for (uint y = 0; y < height; y += rowsPerBlock) + { + rowOffsets[y] = (ulong)stream.Position; + + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. + long pixelDataSizePos = stream.Position; + stream.Position = pixelDataSizePos + 4; + + uint rowsInBlockCount = 0; + for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex); + for (int x = 0; x < width; x++) + { + // OpenEXR channels use associated alpha; the native vector conversion also preserves the integer channel range. + Vector4 vector4 = pixelRowSpan[x].ToAssociatedVector4(); + rgb = Rgba128.FromVector4(vector4); + + redBuffer[x] = rgb.R; + greenBuffer[x] = rgb.G; + blueBuffer[x] = rgb.B; + alphaBuffer[x] = rgb.A; + } + + // Write row data to row block buffer. + Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow); + WriteUnsignedIntRow(rowBlockSpan, width, alphaBuffer, blueBuffer, greenBuffer, redBuffer); + rowsInBlockCount++; + } + + // Write pixel row data compressed to the stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount); + long positionAfterPixelData = stream.Position; + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); + stream.Position = pixelDataSizePos; + stream.Write(this.buffer.AsSpan(0, 4)); + stream.Position = positionAfterPixelData; + + cancellationToken.ThrowIfCancellationRequested(); + } + + return rowOffsets; + } + + /// + /// Writes the image header to the stream. + /// + /// The stream to write to. + /// The header. + private void WriteHeader(Stream stream, ExrHeaderAttributes header) + { + this.WriteChannels(stream, header.Channels); + this.WriteCompression(stream, header.Compression); + this.WriteDataWindow(stream, header.DataWindow); + this.WriteDisplayWindow(stream, header.DisplayWindow); + this.WritePixelAspectRatio(stream, header.AspectRatio); + this.WriteLineOrder(stream, header.LineOrder); + this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter); + this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth); + stream.WriteByte(0); + } + + /// + /// Writes a row of pixels with the FLOAT pixel type to a buffer. + /// + /// The buffer to write to. + /// The width of a row in pixels. + /// The alpha channel buffer. + /// The blue channel buffer. + /// The green channel buffer. + /// The red channel buffer. + private static void WriteSingleRow(Span buffer, int width, Span alphaBuffer, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + WriteSingleToBuffer(buffer.Slice(offset, 4), alphaBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteSingleToBuffer(buffer.Slice(offset, 4), blueBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteSingleToBuffer(buffer.Slice(offset, 4), greenBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteSingleToBuffer(buffer.Slice(offset, 4), redBuffer[x]); + offset += 4; + } + } + + /// + /// Writes a row of pixels with the HALF pixel type to a buffer. + /// + /// The buffer to write to. + /// The width of a row in pixels. + /// The alpha channel buffer. + /// The blue channel buffer. + /// The green channel buffer. + /// The red channel buffer. + private static void WriteHalfSingleRow(Span buffer, int width, Span alphaBuffer, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), alphaBuffer[x]); + offset += 2; + } + + for (int x = 0; x < width; x++) + { + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), blueBuffer[x]); + offset += 2; + } + + for (int x = 0; x < width; x++) + { + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), greenBuffer[x]); + offset += 2; + } + + for (int x = 0; x < width; x++) + { + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), redBuffer[x]); + offset += 2; + } + } + + /// + /// Writes a row of pixels with unsigned int pixel data to a buffer. + /// + /// The buffer to write to. + /// The width of the row in pixels. + /// The alpha channel buffer. + /// The blue channel buffer. + /// The green channel buffer. + /// The red channel buffer. + private static void WriteUnsignedIntRow(Span buffer, int width, Span alphaBuffer, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), alphaBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), blueBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), greenBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), redBuffer[x]); + offset += 4; + } + } + + /// + /// Writes the row offsets to the stream. + /// + /// The stream to write to. + /// The height in pixels of the image. + /// The row offsets. + private void WriteRowOffsets(Stream stream, int height, ulong[] rowOffsets) + { + for (int i = 0; i < height; i++) + { + BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, rowOffsets[i]); + stream.Write(this.buffer); + } + } + + /// + /// Writes the channel infos to the stream. + /// + /// The stream to write to. + /// The channels. + private void WriteChannels(Stream stream, IList channels) + { + int attributeSize = 0; + foreach (ExrChannelInfo channelInfo in channels) + { + attributeSize += channelInfo.ChannelName.Length + 1; + attributeSize += 16; + } + + // Last zero byte. + attributeSize++; + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Channels, ExrConstants.AttibuteTypes.ChannelList, attributeSize); + + foreach (ExrChannelInfo channelInfo in channels) + { + this.WriteChannelInfo(stream, channelInfo); + } + + // Last byte should be zero. + stream.WriteByte(0); + } + + /// + /// Writes info about a single channel to the stream. + /// + /// The stream to write to. + /// The channel information. + private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) + { + WriteString(stream, channelInfo.ChannelName); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType); + stream.Write(this.buffer.AsSpan(0, 4)); + + stream.WriteByte(channelInfo.Linear); + + // Next 3 bytes are reserved and will set to zero. + stream.WriteByte(0); + stream.WriteByte(0); + stream.WriteByte(0); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes the compression type to the stream. + /// + /// The stream to write to. + /// The compression type. + private void WriteCompression(Stream stream, ExrCompression compression) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1); + stream.WriteByte((byte)compression); + } + + /// + /// Writes the pixel aspect ratio to the stream. + /// + /// The stream to write to. + /// The aspect ratio. + private void WritePixelAspectRatio(Stream stream, float aspectRatio) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.PixelAspectRatio, ExrConstants.AttibuteTypes.Float, 4); + this.WriteSingle(stream, aspectRatio); + } + + /// + /// Writes the line order to the stream. + /// + /// The stream to write to. + /// The line order. + private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.LineOrder, ExrConstants.AttibuteTypes.LineOrder, 1); + stream.WriteByte((byte)lineOrder); + } + + /// + /// Writes the screen window center to the stream. + /// + /// The stream to write to. + /// The screen window center. + private void WriteScreenWindowCenter(Stream stream, PointF screenWindowCenter) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowCenter, ExrConstants.AttibuteTypes.TwoFloat, 8); + this.WriteSingle(stream, screenWindowCenter.X); + this.WriteSingle(stream, screenWindowCenter.Y); + } + + /// + /// Writes the screen width to the stream. + /// + /// The stream to write to. + /// Width of the screen window. + private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowWidth, ExrConstants.AttibuteTypes.Float, 4); + this.WriteSingle(stream, screenWindowWidth); + } + + /// + /// Writes the data window to the stream. + /// + /// The stream to write to. + /// The data window. + private void WriteDataWindow(Stream stream, ExrBox2i dataWindow) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DataWindow, ExrConstants.AttibuteTypes.BoxInt, 16); + this.WriteBoxInteger(stream, dataWindow); + } + + /// + /// Writes the display window to the stream. + /// + /// The stream to write to. + /// The display window. + private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DisplayWindow, ExrConstants.AttibuteTypes.BoxInt, 16); + this.WriteBoxInteger(stream, displayWindow); + } + + /// + /// Writes attribute information to the stream. + /// + /// The stream to write to. + /// The name of the attribute. + /// The type of the attribute. + /// The size in bytes of the attribute. + private void WriteAttributeInformation(Stream stream, string name, string type, int size) + { + // Write attribute name. + WriteString(stream, name); + + // Write attribute type. + WriteString(stream, type); + + // Write attribute size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes a string to the stream. + /// + /// The stream to write to. + /// The string to write. + private static void WriteString(Stream stream, string str) + { + foreach (char c in str) + { + stream.WriteByte((byte)c); + } + + // Write termination byte. + stream.WriteByte(0); + } + + /// + /// Writes box struct with xmin, xmax, ymin and y max to the stream. + /// + /// The stream to write to. + /// The box to write. + private void WriteBoxInteger(Stream stream, ExrBox2i box) + { + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes 32 bit float value to the stream. + /// + /// The stream to write to. + /// The float value to write. + [MethodImpl(InliningOptions.ShortMethod)] + private unsafe void WriteSingle(Stream stream, float value) + { + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes a 32 bit float value to a buffer. + /// + /// The buffer to write to. + /// The float value to write. + [MethodImpl(InliningOptions.ShortMethod)] + private static unsafe void WriteSingleToBuffer(Span buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); + + /// + /// Writes a 16 bit float value to a buffer. + /// + /// The buffer to write to. + /// The float value to write. + [MethodImpl(InliningOptions.ShortMethod)] + private static void WriteHalfSingleToBuffer(Span buffer, float value) + { + ushort valueAsShort = HalfTypeHelper.Pack(value); + BinaryPrimitives.WriteUInt16LittleEndian(buffer, valueAsShort); + } + + /// + /// Writes one unsigned int to a buffer. + /// + /// The buffer to write to. + /// The uint value to write. + [MethodImpl(InliningOptions.ShortMethod)] + private static void WriteUnsignedIntToBuffer(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); +} diff --git a/src/ImageSharp/Formats/Exr/ExrFormat.cs b/src/ImageSharp/Formats/Exr/ExrFormat.cs new file mode 100644 index 000000000..4415c15b0 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrFormat.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Registers the image encoders, decoders and mime type detectors for the OpenExr format. +/// +public sealed class ExrFormat : IImageFormat +{ + private ExrFormat() + { + } + + /// + /// Gets the current instance. + /// + public static ExrFormat Instance { get; } = new(); + + /// + public string Name => "EXR"; + + /// + public string DefaultMimeType => "image/x-exr"; + + /// + public IEnumerable MimeTypes => ExrConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => ExrConstants.FileExtensions; + + /// + public ExrMetadata CreateDefaultFormatMetadata() => new(); +} diff --git a/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs new file mode 100644 index 000000000..9a6ce7056 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// The header of an EXR image. +/// +/// +internal class ExrHeaderAttributes +{ + /// + /// Initializes a new instance of the class. + /// + /// The image channels. + /// The compression used. + /// The data window. + /// The display window. + /// The line order. + /// The aspect ratio. + /// Width of the screen window. + /// The screen window center. + /// Size of the tile in x dimension. + /// Size of the tile in y dimension. + /// The chunk count. + public ExrHeaderAttributes( + IList channels, + ExrCompression compression, + ExrBox2i dataWindow, + ExrBox2i displayWindow, + ExrLineOrder lineOrder, + float aspectRatio, + float screenWindowWidth, + PointF screenWindowCenter, + uint? tileXSize = null, + uint? tileYSize = null, + int? chunkCount = null) + { + this.Channels = channels; + this.Compression = compression; + this.DataWindow = dataWindow; + this.DisplayWindow = displayWindow; + this.LineOrder = lineOrder; + this.AspectRatio = aspectRatio; + this.ScreenWindowWidth = screenWindowWidth; + this.ScreenWindowCenter = screenWindowCenter; + this.TileXSize = tileXSize; + this.TileYSize = tileYSize; + this.ChunkCount = chunkCount; + } + + /// + /// Gets or sets a description of the image channels stored in the file. + /// + public IList Channels { get; set; } + + /// + /// Gets or sets the compression method applied to the pixel data of all channels in the file. + /// + public ExrCompression Compression { get; set; } + + /// + /// Gets or sets the image’s data window. + /// + public ExrBox2i DataWindow { get; set; } + + /// + /// Gets or sets the image’s display window. + /// + public ExrBox2i DisplayWindow { get; set; } + + /// + /// Gets or sets in what order the scan lines in the file are stored in the file (increasing Y, decreasing Y, or, for tiled images, also random Y). + /// + public ExrLineOrder LineOrder { get; set; } + + /// + /// Gets or sets the aspect ratio of the image. + /// + public float AspectRatio { get; set; } + + /// + /// Gets or sets the screen width. + /// + public float ScreenWindowWidth { get; set; } + + /// + /// Gets or sets the screen window center. + /// + public PointF ScreenWindowCenter { get; set; } + + /// + /// Gets or sets the number of horizontal tiles. + /// + public uint? TileXSize { get; set; } + + /// + /// Gets or sets the number of vertical tiles. + /// + public uint? TileYSize { get; set; } + + /// + /// Gets or sets the chunk count. Indicates the number of chunks in this part. Required if the multipart bit (12) is set. + /// + public int? ChunkCount { get; set; } +} diff --git a/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs b/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs new file mode 100644 index 000000000..62663a4a7 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Detects OpenExr file headers. +/// +public sealed class ExrImageFormatDetector : IImageFormatDetector +{ + /// + public int HeaderSize => 4; + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) + { + int fileTypeMarker = BinaryPrimitives.ReadInt32LittleEndian(header); + return fileTypeMarker == ExrConstants.MagickBytes; + } + + return false; + } + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null; + return format != null; + } +} diff --git a/src/ImageSharp/Formats/Exr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs new file mode 100644 index 000000000..de1988674 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Provides OpenExr specific metadata information for the image. +/// +public class ExrMetadata : IFormatMetadata +{ + /// + /// Initializes a new instance of the class. + /// + public ExrMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private ExrMetadata(ExrMetadata other) => this.PixelType = other.PixelType; + + /// + /// Gets or sets the pixel format. + /// + public ExrPixelType PixelType { get; set; } = ExrPixelType.Half; + + /// + /// Gets or sets the image data type, either RGB, RGBA or gray. + /// + public ExrImageDataType ImageDataType { get; set; } = ExrImageDataType.Unknown; + + /// + /// Gets or sets the compression method. + /// + public ExrCompression Compression { get; set; } = ExrCompression.None; + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + bool hasAlpha = this.ImageDataType is ExrImageDataType.Rgba; + + int bitsPerComponent = 32; + int bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3; + if (this.PixelType == ExrPixelType.Half) + { + bitsPerComponent = 16; + bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3; + } + + // OpenEXR defines RGBA color channels as premultiplied by alpha, so expose the association stored by the format. + PixelAlphaRepresentation alpha = hasAlpha ? PixelAlphaRepresentation.Associated : PixelAlphaRepresentation.None; + PixelColorType color = PixelColorType.RGB; + + int componentsCount = 0; + int[] precision = []; + switch (this.ImageDataType) + { + case ExrImageDataType.Rgb: + color = PixelColorType.RGB; + componentsCount = 3; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + precision[1] = bitsPerComponent; + precision[2] = bitsPerComponent; + break; + case ExrImageDataType.Rgba: + color = PixelColorType.RGB | PixelColorType.Alpha; + componentsCount = 4; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + precision[1] = bitsPerComponent; + precision[2] = bitsPerComponent; + precision[3] = bitsPerComponent; + break; + case ExrImageDataType.Gray: + color = PixelColorType.Luminance; + componentsCount = 1; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + break; + } + + PixelComponentInfo info = PixelComponentInfo.Create(componentsCount, bitsPerPixel, precision); + return new PixelTypeInfo(bitsPerPixel) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + { + EncodingType type = this.Compression is ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Pxr24 + ? EncodingType.Lossy + : EncodingType.Lossless; + + return new() + { + EncodingType = type, + PixelTypeInfo = this.GetPixelTypeInfo() + }; + } + + /// + public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + PixelTypeInfo pixelTypeInfo = metadata.PixelTypeInfo; + PixelComponentInfo? info = pixelTypeInfo.ComponentInfo; + PixelColorType colorType = pixelTypeInfo.ColorType; + + int bitsPerComponent = info?.GetMaximumComponentPrecision() + ?? (pixelTypeInfo.BitsPerPixel <= 16 ? 16 : 32); + + int componentCount = info?.ComponentCount ?? 0; + ExrImageDataType imageDataType = colorType switch + { + PixelColorType.Luminance => ExrImageDataType.Gray, + PixelColorType.RGB or PixelColorType.BGR => ExrImageDataType.Rgb, + PixelColorType.RGB | PixelColorType.Alpha + or PixelColorType.BGR | PixelColorType.Alpha + or PixelColorType.Luminance | PixelColorType.Alpha => ExrImageDataType.Rgba, + _ => componentCount switch + { + >= 4 => ExrImageDataType.Rgba, + >= 3 => ExrImageDataType.Rgb, + 1 => ExrImageDataType.Gray, + _ => ExrImageDataType.Unknown, + } + }; + + return new() + { + PixelType = bitsPerComponent <= 16 ? ExrPixelType.Half : ExrPixelType.Float, + ImageDataType = imageDataType, + }; + } + + /// + ExrMetadata IDeepCloneable.DeepClone() => new(this); + + /// + public IDeepCloneable DeepClone() => new ExrMetadata(this); + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } +} diff --git a/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs b/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs new file mode 100644 index 000000000..51419ec95 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Exr; + +/// +/// Cold path optimizations for throwing exr format based exceptions. +/// +internal static class ExrThrowHelper +{ + [DoesNotReturn] + public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); + + [DoesNotReturn] + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + [DoesNotReturn] + public static void ThrowNotSupportedVersion() => throw new NotSupportedException("Unsupported EXR version"); + + [DoesNotReturn] + public static void ThrowNotSupported(string msg) => throw new NotSupportedException(msg); + + [DoesNotReturn] + public static void ThrowInvalidImageHeader() => throw new InvalidImageContentException("Invalid EXR image header"); + + [DoesNotReturn] + public static void ThrowInvalidImageHeader(string msg) => throw new InvalidImageContentException(msg); + + [DoesNotReturn] + public static Exception NotSupportedCompressor(string compressionType) => throw new NotSupportedException($"Not supported encoder compression method: {compressionType}"); +} diff --git a/src/ImageSharp/Formats/Exr/ExrUtils.cs b/src/ImageSharp/Formats/Exr/ExrUtils.cs new file mode 100644 index 000000000..532950daa --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrUtils.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; + +namespace SixLabors.ImageSharp.Formats.Exr; + +internal static class ExrUtils +{ + /// + /// Calcualtes the required bytes for a pixel row. + /// + /// The image channels array. + /// The width in pixels of a row. + /// The number of bytes per row. + public static ulong CalculateBytesPerRow(IList channels, uint width) + { + ulong bytesPerRow = 0; + foreach (ExrChannelInfo channelInfo in channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("R", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("G", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("B", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) + { + if (channelInfo.PixelType == ExrPixelType.Half) + { + bytesPerRow += 2UL * width; + } + else + { + bytesPerRow += 4UL * width; + } + } + } + + return bytesPerRow; + } + + /// + /// Determines how many pixel rows there are in a block. This varies depending on the compression used. + /// + /// The compression used. + /// Pixel rows in a block. + public static uint RowsPerBlock(ExrCompression compression) => compression switch + { + ExrCompression.Zip or ExrCompression.Pxr24 => 16, + ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Piz => 32, + _ => 1, + }; +} diff --git a/src/ImageSharp/Formats/Exr/README.md b/src/ImageSharp/Formats/Exr/README.md new file mode 100644 index 000000000..c71ab113d --- /dev/null +++ b/src/ImageSharp/Formats/Exr/README.md @@ -0,0 +1,4 @@ +### Some useful links for documentation about the OpenEXR format: + +- [Technical Introduction](https://openexr.readthedocs.io/en/latest/TechnicalIntroduction.html) +- [OpenExr file layout](https://openexr.readthedocs.io/en/latest/OpenEXRFileLayout.html) \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 78ceb0b23..dc6ce1846 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -146,10 +146,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore this.ReadGraphicalControlExtension(stream); break; case GifConstants.CommentLabel: - this.ReadComments(stream); + this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream)); break; case GifConstants.ApplicationExtensionLabel: - this.ReadApplicationExtension(stream); + this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream)); break; case GifConstants.PlainTextLabel: SkipBlock(stream); // Not supported by any known decoder. @@ -226,10 +226,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore this.ReadGraphicalControlExtension(stream); break; case GifConstants.CommentLabel: - this.ReadComments(stream); + this.ExecuteAncillarySegmentAction(() => this.ReadComments(stream)); break; case GifConstants.ApplicationExtensionLabel: - this.ReadApplicationExtension(stream); + this.ExecuteAncillarySegmentAction(() => this.ReadApplicationExtension(stream)); break; case GifConstants.PlainTextLabel: SkipBlock(stream); // Not supported by any known decoder. @@ -266,6 +266,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore GifThrowHelper.ThrowNoHeader(); } + // Ignoring a malformed ancillary extension must not let identify succeed for a file + // that never contained any readable image frame data. + if (previousFrame is null) + { + GifThrowHelper.ThrowNoData(); + } + return new ImageInfo( new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height), this.metadata, @@ -331,51 +338,128 @@ internal sealed class GifDecoderCore : ImageDecoderCore private void ReadApplicationExtension(BufferedReadStream stream) { int appLength = stream.ReadByte(); + if (appLength == -1) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } + + if (appLength != GifConstants.ApplicationBlockSize) + { + this.ThrowOrIgnoreNonStrictSegmentError($"Gif application extension length '{appLength}' is invalid"); + SkipBlock(stream, appLength); + return; + } // If the length is 11 then it's a valid extension and most likely // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this. long position = stream.Position; - if (appLength == GifConstants.ApplicationBlockSize) + int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize); + if (bytesRead != GifConstants.ApplicationBlockSize) { - stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize); - bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); - if (isXmp && !this.skipMetadata) - { - GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); - if (extension.Data.Length > 0) - { - this.metadata!.XmpProfile = new XmpProfile(extension.Data); - } - else - { - // Reset the stream position and continue. - stream.Position = position; - SkipBlock(stream, appLength); - } + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } - return; - } + bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); + if (isXmp) + { + this.ReadXmpApplicationExtension(stream, position, appLength); + return; + } - int subBlockSize = stream.ReadByte(); + int subBlockSize = stream.ReadByte(); + if (subBlockSize == -1) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } - // TODO: There's also a NETSCAPE buffer extension. - // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension - if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) - { - stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize); - this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount; - stream.Skip(1); // Skip the terminator. - return; - } + // TODO: There's also a NETSCAPE buffer extension. + // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension + if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) + { + this.ReadNetscapeApplicationExtension(stream); + return; + } + + // Could be something else not supported yet. + // Skip the subblock and terminator. + SkipBlock(stream, subBlockSize); + } + + /// + /// Reads the GIF XMP application extension. + /// + /// The containing image data. + /// The stream position where the application identifier begins. + /// The application block length. + private void ReadXmpApplicationExtension(BufferedReadStream stream, long applicationPosition, int appLength) + { + if (this.skipMetadata) + { + stream.Position = applicationPosition; + SkipBlock(stream, appLength); + return; + } - // Could be something else not supported yet. - // Skip the subblock and terminator. - SkipBlock(stream, subBlockSize); + bool completed = false; + this.ExecuteAncillarySegmentAction( + () => + { + this.ReadXmpApplicationExtensionData(stream, applicationPosition, appLength); + completed = true; + }); + if (!completed) + { + stream.Position = applicationPosition; + SkipBlock(stream, appLength); + } + } + + /// + /// Reads the GIF XMP application extension data. + /// + /// The containing image data. + /// The stream position where the application identifier begins. + /// The application block length. + private void ReadXmpApplicationExtensionData(BufferedReadStream stream, long applicationPosition, int appLength) + { + GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); + if (extension.Data.Length > 0) + { + this.metadata!.XmpProfile = new XmpProfile(extension.Data); return; } - SkipBlock(stream, appLength); // Not supported by any known decoder. + stream.Position = applicationPosition; + SkipBlock(stream, appLength); + } + + /// + /// Reads the GIF NETSCAPE looping application extension. + /// + /// The containing image data. + private void ReadNetscapeApplicationExtension(BufferedReadStream stream) => + this.ExecuteAncillarySegmentAction(() => this.ReadNetscapeApplicationExtensionData(stream)); + + /// + /// Reads the GIF NETSCAPE looping application extension data. + /// + /// The containing image data. + private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize); + if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize) + { + throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } + + this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount; + + int terminator = stream.ReadByte(); + if (terminator == -1) + { + throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension"); + } } /// @@ -428,7 +512,12 @@ internal sealed class GifDecoderCore : ImageDecoderCore using IMemoryOwner commentsBuffer = this.memoryAllocator.Allocate(length); Span commentsSpan = commentsBuffer.GetSpan(); - stream.Read(commentsSpan); + int bytesRead = stream.Read(commentsSpan); + if (bytesRead != length) + { + GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment"); + } + string commentPart = GifConstants.Encoding.GetString(commentsSpan); stringBuilder.Append(commentPart); } @@ -468,7 +557,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); stream.Read(this.currentLocalColorTable.GetSpan()[..length]); - rawColorTable = this.currentLocalColorTable!.GetSpan()[..length]; + rawColorTable = this.currentLocalColorTable.GetSpan()[..length]; } else if (this.globalColorTable != null) { @@ -901,7 +990,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore byte index = this.logicalScreenDescriptor.BackgroundColorIndex; this.backgroundColorIndex = index; - this.gifMetadata.BackgroundColorIndex = index; + ReadOnlyMemory? globalColorTable = this.gifMetadata.GlobalColorTable; + if (globalColorTable.HasValue && index < globalColorTable.Value.Length) + { + this.gifMetadata.BackgroundColor = globalColorTable.Value.Span[index]; + } } private unsafe struct ScratchBuffer diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 07c73dcf2..d4a6be000 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -45,14 +45,6 @@ internal sealed class GifEncoderCore /// private readonly IPixelSamplingStrategy pixelSamplingStrategy; - /// - /// The default background color of the canvas when animating. - /// This color may be used to fill the unused space on the canvas around the frames, - /// as well as the transparent pixels of the first frame. - /// The background color is also used when a frame disposal mode is . - /// - private readonly Color? backgroundColor; - /// /// The number of times any animation is repeated. /// @@ -76,7 +68,6 @@ internal sealed class GifEncoderCore this.skipMetadata = encoder.SkipMetadata; this.colorTableMode = encoder.ColorTableMode; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; - this.backgroundColor = encoder.BackgroundColor; this.repeatCount = encoder.RepeatCount; this.transparentColorMode = encoder.TransparentColorMode; } @@ -113,11 +104,21 @@ internal sealed class GifEncoderCore TransparentColorMode mode = this.transparentColorMode; // Create a new quantizer options instance augmenting the transparent color mode to match the encoder. - QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new QuantizerOptions()).DeepClone(o => o.TransparentColorMode = mode); + QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new QuantizerOptions()).DeepClone(o => + { + o.TransparentColorMode = mode; + + // Animated GIF delta frames can use one padded color-table index as transparency. + // Express that through MaxColors so custom quantizers receive the same budget. + if (image.Frames.Count > 1 && o.MaxColors == QuantizerConstants.MaxColors) + { + o.MaxColors = QuantizerConstants.MaxColors - 1; + } + }); if (globalQuantizer is null) { - // Is this a gif with color information. If so use that, otherwise use octree. + // Is this a gif with color information. If so use that, otherwise use the adaptive hexadecatree quantizer. if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) { int ti = GetTransparentIndex(quantized, frameMetadata); @@ -132,12 +133,12 @@ internal sealed class GifEncoderCore } else { - globalQuantizer = new OctreeQuantizer(options); + globalQuantizer = new HexadecatreeQuantizer(options); } } else { - globalQuantizer = new OctreeQuantizer(options); + globalQuantizer = new HexadecatreeQuantizer(options); } } @@ -145,6 +146,11 @@ internal sealed class GifEncoderCore IPixelSamplingStrategy strategy = this.pixelSamplingStrategy; ImageFrame encodingFrame = image.Frames.RootFrame; + + // This color is encoded as the logical-screen background index and is also + // used when de-duplicating frames that restore to the GIF background. + Color backgroundColor = this.encoder.BackgroundColor ?? gifMetadata.BackgroundColor ?? Color.Transparent; + byte backgroundIndex = 0; if (useGlobalTableForFirstFrame) { using IQuantizer firstFrameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer(this.configuration, options); @@ -158,6 +164,8 @@ internal sealed class GifEncoderCore } quantized = firstFrameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); + TPixel backgroundPixel = backgroundColor.ToPixel(); + backgroundIndex = firstFrameQuantizer.GetQuantizedColor(backgroundPixel, out _); } else { @@ -184,8 +192,6 @@ internal sealed class GifEncoderCore frameMetadata.TransparencyIndex = ClampIndex(transparencyIndex); } - byte backgroundIndex = GetBackgroundIndex(quantized, gifMetadata, this.backgroundColor); - // Get the number of bits. int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); this.WriteLogicalScreenDescriptor(image.Metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream); @@ -222,6 +228,7 @@ internal sealed class GifEncoderCore image, globalQuantizer, globalFrameQuantizer, + backgroundColor, transparencyIndex, frameMetadata.DisposalMode, cancellationToken); @@ -253,6 +260,7 @@ internal sealed class GifEncoderCore Image image, IQuantizer globalQuantizer, PaletteQuantizer globalFrameQuantizer, + Color backgroundColor, int globalTransparencyIndex, FrameDisposalMode previousDisposalMode, CancellationToken cancellationToken) @@ -284,6 +292,7 @@ internal sealed class GifEncoderCore globalFrameQuantizer, useLocal, gifMetadata, + backgroundColor, previousDisposalMode); previousFrame = currentFrame; @@ -303,7 +312,7 @@ internal sealed class GifEncoderCore this.WriteGraphicalControlExtension(metadata, stream); Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; - Rectangle interest = indices.FullRectangle(); + Rectangle interest = indices.Bounds; bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (metadata.ColorTableMode == FrameColorTableMode.Local); int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); @@ -327,6 +336,7 @@ internal sealed class GifEncoderCore PaletteQuantizer globalFrameQuantizer, bool useLocal, GifFrameMetadata metadata, + Color backgroundColor, FrameDisposalMode previousDisposalMode) where TPixel : unmanaged, IPixel { @@ -347,11 +357,14 @@ internal sealed class GifEncoderCore previous.Metadata.GetGifMetadata().DisposalMode; Color background = !useTransparency && disposalMode == FrameDisposalMode.RestoreToBackground - ? this.backgroundColor ?? Color.Transparent + ? backgroundColor : Color.Transparent; // Deduplicate and quantize the frame capturing only required parts. - (bool difference, Rectangle bounds) = + // Pixels matching the previous frame are replaced with the transparent placeholder. + // When the entire frame matches there is no captured difference, but every pixel is + // still a placeholder, so a transparent index is always required for additional frames. + (_, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels( this.configuration, previous, @@ -368,7 +381,7 @@ internal sealed class GifEncoderCore bounds, metadata, useLocal, - difference, + true, transparencyIndex, background); @@ -393,7 +406,7 @@ internal sealed class GifEncoderCore Rectangle bounds, GifFrameMetadata metadata, bool useLocal, - bool hasDuplicates, + bool requiresTransparency, int transparencyIndex, Color transparentColor) where TPixel : unmanaged, IPixel @@ -407,9 +420,11 @@ internal sealed class GifEncoderCore // We can use the color data from the decoded metadata here. // We avoid dithering by default to preserve the original colors. ReadOnlyMemory palette = metadata.LocalColorTable.Value; - if (hasDuplicates && !metadata.HasTransparency) + if (requiresTransparency && !metadata.HasTransparency) { - // Duplicates were captured but the metadata does not have transparency. + // The frame was de-duplicated against the previous frame, replacing matching + // pixels with the transparent placeholder, but the metadata does not yet carry + // a transparent index. Reserve one so those pixels encode as transparent. metadata.HasTransparency = true; if (palette.Length < 256) @@ -470,7 +485,7 @@ internal sealed class GifEncoderCore metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); - if (hasDuplicates) + if (requiresTransparency) { metadata.HasTransparency = true; } @@ -482,11 +497,19 @@ internal sealed class GifEncoderCore // Individual frames, though using the shared palette, can use a different transparent index // to represent transparency. - // A difference was captured but the metadata does not have transparency. - if (hasDuplicates && !metadata.HasTransparency) + // The frame was de-duplicated against the previous frame, replacing matching pixels with + // the transparent placeholder. When the whole frame matches there is no captured difference, + // yet every pixel is still a placeholder, so we must always reserve a transparent index here; + // otherwise the placeholder pixels are matched to the nearest (typically darkest) palette color. + if (requiresTransparency && !metadata.HasTransparency) { metadata.HasTransparency = true; - transparencyIndex = globalFrameQuantizer.Palette.Length; + + // Normally we pad one index past the palette so the (out of range) value is treated as + // transparent by decoders without growing the color table. A full 256-color palette leaves + // no room to pad within the 8-bit index space (index 256 wraps to 0 when written and exceeds + // the maximum GIF bit depth), so reuse the last in-range index for transparency instead. + transparencyIndex = Math.Min(globalFrameQuantizer.Palette.Length, byte.MaxValue); metadata.TransparencyIndex = ClampIndex(transparencyIndex); } @@ -518,7 +541,7 @@ internal sealed class GifEncoderCore int index = -1; if (quantized != null) { - TPixel transparentPixel = TPixel.FromScaledVector4(Vector4.Zero); + TPixel transparentPixel = TPixel.FromUnassociatedScaledVector4(Vector4.Zero); ReadOnlySpan palette = quantized.Palette.Span; // Transparent pixels are much more likely to be found at the end of a palette. @@ -534,44 +557,6 @@ internal sealed class GifEncoderCore return index; } - /// - /// Returns the index of the background color in the palette. - /// - /// The current quantized frame. - /// The gif metadata - /// The background color to match. - /// The pixel format. - /// The index of the background color. - private static byte GetBackgroundIndex(IndexedImageFrame? quantized, GifMetadata metadata, Color? background) - where TPixel : unmanaged, IPixel - { - int match = -1; - if (quantized != null) - { - if (background.HasValue) - { - TPixel backgroundPixel = background.Value.ToPixel(); - ReadOnlySpan palette = quantized.Palette.Span; - for (int i = 0; i < palette.Length; i++) - { - if (!backgroundPixel.Equals(palette[i])) - { - continue; - } - - match = i; - break; - } - } - else if (metadata.BackgroundColorIndex < quantized.Palette.Length) - { - match = metadata.BackgroundColorIndex; - } - } - - return ClampIndex(match); - } - /// /// Writes the file header signature and version to the stream. /// diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 978209b23..50a29775d 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -26,7 +26,7 @@ public class GifMetadata : IFormatMetadata { this.RepeatCount = other.RepeatCount; this.ColorTableMode = other.ColorTableMode; - this.BackgroundColorIndex = other.BackgroundColorIndex; + this.BackgroundColor = other.BackgroundColor; if (other.GlobalColorTable?.Length > 0) { @@ -59,10 +59,9 @@ public class GifMetadata : IFormatMetadata public ReadOnlyMemory? GlobalColorTable { get; set; } /// - /// Gets or sets the index at the for the background color. - /// The background color is the color used for those pixels on the screen that are not covered by an image. + /// Gets or sets the background color used for pixels on the screen that are not covered by an image. /// - public byte BackgroundColorIndex { get; set; } + public Color? BackgroundColor { get; set; } /// /// Gets or sets the collection of comments about the graphics, credits, descriptions or any @@ -101,6 +100,7 @@ public class GifMetadata : IFormatMetadata { AnimateRootFrame = true, ColorTableMode = this.ColorTableMode, + BackgroundColor = this.BackgroundColor ?? Color.Transparent, PixelTypeInfo = this.GetPixelTypeInfo(), RepeatCount = this.RepeatCount, }; diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs index dc78f28bf..85c4959a9 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -30,7 +30,11 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension /// The XMP metadata public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) { - byte[] xmpBytes = ReadXmpData(stream, allocator); + byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated); + if (!terminated) + { + throw new InvalidImageContentException("Unexpected end of stream while reading gif XMP data"); + } // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 @@ -71,7 +75,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension return this.ContentLength; } - private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator) + private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated) { using ChunkedMemoryStream bytes = new(allocator); @@ -83,8 +87,15 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension while (true) { int b = stream.ReadByte(); - if (b <= 0) + if (b == 0) + { + terminated = true; + return bytes.ToArray(); + } + + if (b < 0) { + terminated = false; return bytes.ToArray(); } diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs index f6b1a92a0..23b517568 100644 --- a/src/ImageSharp/Formats/ImageDecoderCore.cs +++ b/src/ImageSharp/Formats/ImageDecoderCore.cs @@ -33,6 +33,74 @@ internal abstract class ImageDecoderCore /// public Size Dimensions { get; protected internal set; } + /// + /// Executes a known ancillary segment parsing action using the configured integrity policy. + /// + /// The action. + protected void ExecuteAncillarySegmentAction(Action action) + { + if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict) + { + action(); + return; + } + + try + { + action(); + } + catch (Exception ex) when (ex + is ImageFormatException + or InvalidIccProfileException + or InvalidImageContentException + or InvalidOperationException + or NotSupportedException) + { + // Intentionally ignored in non-strict segment integrity modes. + } + } + + /// + /// Executes a known image data segment parsing action using the configured integrity policy. + /// + /// The action. + protected void ExecuteImageDataSegmentAction(Action action) + { + if (this.Options.SegmentIntegrityHandling is not SegmentIntegrityHandling.IgnoreImageData) + { + action(); + return; + } + + try + { + action(); + } + catch (Exception ex) when (ex + is ImageFormatException + or InvalidIccProfileException + or InvalidImageContentException + or InvalidOperationException + or NotSupportedException) + { + // Intentionally ignored when image data integrity handling is set to IgnoreImageData. + } + } + + /// + /// Throws unless the decoder is running in a non-strict segment integrity mode. + /// Use this only from within when local control flow + /// must continue after the error. + /// + /// The exception message. + protected void ThrowOrIgnoreNonStrictSegmentError(string message) + { + if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict) + { + throw new InvalidImageContentException(message); + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs index 179f9aa28..160e6a4f9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs @@ -14,7 +14,7 @@ internal partial struct Block8x8F public void ScaledCopyFrom(ref float areaOrigin, int areaStride) => CopyFrom1x1Scale(ref Unsafe.As(ref areaOrigin), ref Unsafe.As(ref this), areaStride); - [MethodImpl(InliningOptions.ShortMethod)] + [MethodImpl(InliningOptions.ColdPath)] public void ScaledCopyTo(ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) { if (horizontalScale == 1 && verticalScale == 1) @@ -29,7 +29,50 @@ internal partial struct Block8x8F return; } - // TODO: Optimize: implement all cases with scale-specific, loopless code! + if (horizontalScale == 2 && verticalScale == 1) + { + this.CopyTo2x1Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 1 && verticalScale == 2) + { + this.CopyTo1x2Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 4 && verticalScale == 1) + { + this.CopyTo4x1Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 4 && verticalScale == 2) + { + this.CopyTo4x2Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 1 && verticalScale == 4) + { + this.CopyTo1x4Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 2 && verticalScale == 4) + { + this.CopyTo2x4Scale(ref areaOrigin, (uint)areaStride); + return; + } + + if (horizontalScale == 4 && verticalScale == 4) + { + this.CopyTo4x4Scale(ref areaOrigin, (uint)areaStride); + return; + } + + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. this.CopyArbitraryScale(ref areaOrigin, (uint)areaStride, (uint)horizontalScale, (uint)verticalScale); } @@ -85,6 +128,285 @@ internal partial struct Block8x8F } } + /// + /// Copies the full 8x8 block into the destination buffer while doubling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo2x1Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 4u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 5u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 6u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 7u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while doubling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo1x2Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 8u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 9u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 10u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 11u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 12u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 13u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 14u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 15u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while quadrupling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo4x1Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 4u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 5u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 6u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 7u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while quadrupling horizontally and doubling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo4x2Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 8u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 9u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 10u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 11u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 12u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 13u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 14u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 15u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while quadrupling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo1x4Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride); + CopyRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while doubling horizontally and quadrupling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo2x4Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride); + WidenRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride); + } + + /// + /// Copies the full 8x8 block into the destination buffer while quadrupling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private void CopyTo4x4Scale(ref float areaOrigin, uint areaStride) + { + ref Vector4 sourceBase = ref this.V0L; + + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 16u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 17u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 18u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 4u, 19u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 20u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 21u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 22u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 5u, 23u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 24u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 25u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 26u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 6u, 27u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 28u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 29u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 30u, areaStride); + ExpandRow8(ref sourceBase, ref areaOrigin, 7u, 31u, areaStride); + } + + /// + /// Copies one eight-sample row from the full block to the destination row. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref Vector4 source = ref Unsafe.Add(ref sourceBase, sourceRow * 2u); + ref Vector4 dest = ref Unsafe.As(ref Unsafe.Add(ref areaOrigin, destRow * areaStride)); + + dest = source; + Unsafe.Add(ref dest, 1u) = Unsafe.Add(ref source, 1u); + } + + /// + /// Expands one eight-sample row to sixteen samples by duplicating each source value horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void WidenRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref Vector4 sourceLeft = ref Unsafe.Add(ref sourceBase, sourceRow * 2u); + ref Vector4 sourceRight = ref Unsafe.Add(ref sourceLeft, 1u); + ref Vector4 dest = ref Unsafe.As(ref Unsafe.Add(ref areaOrigin, destRow * areaStride)); + + Vector4 xyLeft = new(sourceLeft.X); + xyLeft.Z = sourceLeft.Y; + xyLeft.W = sourceLeft.Y; + + Vector4 zwLeft = new(sourceLeft.Z); + zwLeft.Z = sourceLeft.W; + zwLeft.W = sourceLeft.W; + + Vector4 xyRight = new(sourceRight.X); + xyRight.Z = sourceRight.Y; + xyRight.W = sourceRight.Y; + + Vector4 zwRight = new(sourceRight.Z); + zwRight.Z = sourceRight.W; + zwRight.W = sourceRight.W; + + dest = xyLeft; + Unsafe.Add(ref dest, 1u) = zwLeft; + Unsafe.Add(ref dest, 2u) = xyRight; + Unsafe.Add(ref dest, 3u) = zwRight; + } + + /// + /// Expands one eight-sample row to thirty-two samples by duplicating each source value four times horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void ExpandRow8(ref Vector4 sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref Vector4 sourceLeft = ref Unsafe.Add(ref sourceBase, sourceRow * 2u); + ref Vector4 sourceRight = ref Unsafe.Add(ref sourceLeft, 1u); + ref Vector4 dest = ref Unsafe.As(ref Unsafe.Add(ref areaOrigin, destRow * areaStride)); + + dest = new Vector4(sourceLeft.X); + Unsafe.Add(ref dest, 1u) = new Vector4(sourceLeft.Y); + Unsafe.Add(ref dest, 2u) = new Vector4(sourceLeft.Z); + Unsafe.Add(ref dest, 3u) = new Vector4(sourceLeft.W); + Unsafe.Add(ref dest, 4u) = new Vector4(sourceRight.X); + Unsafe.Add(ref dest, 5u) = new Vector4(sourceRight.Y); + Unsafe.Add(ref dest, 6u) = new Vector4(sourceRight.Z); + Unsafe.Add(ref dest, 7u) = new Vector4(sourceRight.W); + } + [MethodImpl(InliningOptions.ColdPath)] private void CopyArbitraryScale(ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs index d4c0398d9..a80dcf86e 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector128.cs @@ -13,31 +13,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components; internal partial struct Block8x8F { /// - /// version of and . + /// version of . /// /// The maximum value to normalize to. [MethodImpl(InliningOptions.ShortMethod)] - public void NormalizeColorsAndRoundInPlaceVector128(float maximum) + public void NormalizeColorsInPlaceVector128(float maximum) { Vector128 max = Vector128.Create(maximum); Vector128 off = Vector128.Ceiling(max * .5F); - this.V0L = NormalizeAndRoundVector128(this.V0L.AsVector128(), off, max).AsVector4(); - this.V0R = NormalizeAndRoundVector128(this.V0R.AsVector128(), off, max).AsVector4(); - this.V1L = NormalizeAndRoundVector128(this.V1L.AsVector128(), off, max).AsVector4(); - this.V1R = NormalizeAndRoundVector128(this.V1R.AsVector128(), off, max).AsVector4(); - this.V2L = NormalizeAndRoundVector128(this.V2L.AsVector128(), off, max).AsVector4(); - this.V2R = NormalizeAndRoundVector128(this.V2R.AsVector128(), off, max).AsVector4(); - this.V3L = NormalizeAndRoundVector128(this.V3L.AsVector128(), off, max).AsVector4(); - this.V3R = NormalizeAndRoundVector128(this.V3R.AsVector128(), off, max).AsVector4(); - this.V4L = NormalizeAndRoundVector128(this.V4L.AsVector128(), off, max).AsVector4(); - this.V4R = NormalizeAndRoundVector128(this.V4R.AsVector128(), off, max).AsVector4(); - this.V5L = NormalizeAndRoundVector128(this.V5L.AsVector128(), off, max).AsVector4(); - this.V5R = NormalizeAndRoundVector128(this.V5R.AsVector128(), off, max).AsVector4(); - this.V6L = NormalizeAndRoundVector128(this.V6L.AsVector128(), off, max).AsVector4(); - this.V6R = NormalizeAndRoundVector128(this.V6R.AsVector128(), off, max).AsVector4(); - this.V7L = NormalizeAndRoundVector128(this.V7L.AsVector128(), off, max).AsVector4(); - this.V7R = NormalizeAndRoundVector128(this.V7R.AsVector128(), off, max).AsVector4(); + this.V0L = NormalizeVector128(this.V0L.AsVector128(), off, max).AsVector4(); + this.V0R = NormalizeVector128(this.V0R.AsVector128(), off, max).AsVector4(); + this.V1L = NormalizeVector128(this.V1L.AsVector128(), off, max).AsVector4(); + this.V1R = NormalizeVector128(this.V1R.AsVector128(), off, max).AsVector4(); + this.V2L = NormalizeVector128(this.V2L.AsVector128(), off, max).AsVector4(); + this.V2R = NormalizeVector128(this.V2R.AsVector128(), off, max).AsVector4(); + this.V3L = NormalizeVector128(this.V3L.AsVector128(), off, max).AsVector4(); + this.V3R = NormalizeVector128(this.V3R.AsVector128(), off, max).AsVector4(); + this.V4L = NormalizeVector128(this.V4L.AsVector128(), off, max).AsVector4(); + this.V4R = NormalizeVector128(this.V4R.AsVector128(), off, max).AsVector4(); + this.V5L = NormalizeVector128(this.V5L.AsVector128(), off, max).AsVector4(); + this.V5R = NormalizeVector128(this.V5R.AsVector128(), off, max).AsVector4(); + this.V6L = NormalizeVector128(this.V6L.AsVector128(), off, max).AsVector4(); + this.V6R = NormalizeVector128(this.V6R.AsVector128(), off, max).AsVector4(); + this.V7L = NormalizeVector128(this.V7L.AsVector128(), off, max).AsVector4(); + this.V7R = NormalizeVector128(this.V7R.AsVector128(), off, max).AsVector4(); } /// @@ -71,8 +71,8 @@ internal partial struct Block8x8F } [MethodImpl(InliningOptions.ShortMethod)] - private static Vector128 NormalizeAndRoundVector128(Vector128 value, Vector128 off, Vector128 max) - => Vector128_.RoundToNearestInteger(Vector128_.Clamp(value + off, Vector128.Zero, max)); + private static Vector128 NormalizeVector128(Vector128 value, Vector128 off, Vector128 max) + => Vector128_.Clamp(value + off, Vector128.Zero, max); private static void MultiplyIntoInt16Vector128(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector256.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector256.cs index 2aaf5c943..f16452ed5 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.Vector256.cs @@ -39,23 +39,23 @@ internal partial struct Block8x8F #pragma warning restore SA1310 // Field names should not contain underscore /// - /// version of and . + /// version of . /// /// The maximum value to normalize to. [MethodImpl(InliningOptions.ShortMethod)] - public void NormalizeColorsAndRoundInPlaceVector256(float maximum) + public void NormalizeColorsInPlaceVector256(float maximum) { Vector256 max = Vector256.Create(maximum); Vector256 off = Vector256.Ceiling(max * .5F); - this.V256_0 = NormalizeAndRoundVector256(this.V256_0, off, max); - this.V256_1 = NormalizeAndRoundVector256(this.V256_1, off, max); - this.V256_2 = NormalizeAndRoundVector256(this.V256_2, off, max); - this.V256_3 = NormalizeAndRoundVector256(this.V256_3, off, max); - this.V256_4 = NormalizeAndRoundVector256(this.V256_4, off, max); - this.V256_5 = NormalizeAndRoundVector256(this.V256_5, off, max); - this.V256_6 = NormalizeAndRoundVector256(this.V256_6, off, max); - this.V256_7 = NormalizeAndRoundVector256(this.V256_7, off, max); + this.V256_0 = NormalizeVector256(this.V256_0, off, max); + this.V256_1 = NormalizeVector256(this.V256_1, off, max); + this.V256_2 = NormalizeVector256(this.V256_2, off, max); + this.V256_3 = NormalizeVector256(this.V256_3, off, max); + this.V256_4 = NormalizeVector256(this.V256_4, off, max); + this.V256_5 = NormalizeVector256(this.V256_5, off, max); + this.V256_6 = NormalizeVector256(this.V256_6, off, max); + this.V256_7 = NormalizeVector256(this.V256_7, off, max); } /// @@ -95,10 +95,10 @@ internal partial struct Block8x8F } [MethodImpl(InliningOptions.ShortMethod)] - private static Vector256 NormalizeAndRoundVector256(Vector256 value, Vector256 off, Vector256 max) - => Vector256_.RoundToNearestInteger(Vector256_.Clamp(value + off, Vector256.Zero, max)); + private static Vector256 NormalizeVector256(Vector256 value, Vector256 off, Vector256 max) + => Vector256_.Clamp(value + off, Vector256.Zero, max); - private static unsafe void MultiplyIntoInt16Vector256(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest) + private static void MultiplyIntoInt16Vector256(ref Block8x8F a, ref Block8x8F b, ref Block8x8 dest) { DebugGuard.IsTrue(Vector256.IsHardwareAccelerated, "Vector256 support is required to run this operation!"); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 49b519201..f3767c888 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -132,7 +132,7 @@ internal partial struct Block8x8F : IEquatable /// /// Destination [MethodImpl(InliningOptions.ShortMethod)] - public unsafe void ScaledCopyTo(float[] dest) + public readonly void ScaledCopyTo(float[] dest) { DebugGuard.MustBeGreaterThanOrEqualTo(dest.Length, Size, "dest is too small"); @@ -193,7 +193,7 @@ internal partial struct Block8x8F : IEquatable /// /// The other block. [MethodImpl(InliningOptions.ShortMethod)] - public unsafe void MultiplyInPlace(ref Block8x8F other) + public void MultiplyInPlace(ref Block8x8F other) { if (Vector256.IsHardwareAccelerated) { @@ -324,62 +324,43 @@ internal partial struct Block8x8F : IEquatable } /// - /// Level shift by +maximum/2, clip to [0..maximum], and round all the values in the block. + /// Level shift by +maximum/2, clip to [0, maximum] /// - /// The maximum value. - public void NormalizeColorsAndRoundInPlace(float maximum) + /// The maximum value to normalize to. + public void NormalizeColorsInPlace(float maximum) { if (Vector256.IsHardwareAccelerated) { - this.NormalizeColorsAndRoundInPlaceVector256(maximum); + this.NormalizeColorsInPlaceVector256(maximum); + return; } else if (Vector128.IsHardwareAccelerated) { - this.NormalizeColorsAndRoundInPlaceVector128(maximum); + this.NormalizeColorsInPlaceVector128(maximum); + return; } else { - this.NormalizeColorsInPlace(maximum); - this.RoundInPlace(); - } - } - - /// - /// Level shift by +maximum/2, clip to [0, maximum] - /// - /// The maximum value to normalize to. - public void NormalizeColorsInPlace(float maximum) - { - Vector4 min = Vector4.Zero; - Vector4 max = new(maximum); - Vector4 off = new(MathF.Ceiling(maximum * 0.5F)); - - this.V0L = Vector4.Clamp(this.V0L + off, min, max); - this.V0R = Vector4.Clamp(this.V0R + off, min, max); - this.V1L = Vector4.Clamp(this.V1L + off, min, max); - this.V1R = Vector4.Clamp(this.V1R + off, min, max); - this.V2L = Vector4.Clamp(this.V2L + off, min, max); - this.V2R = Vector4.Clamp(this.V2R + off, min, max); - this.V3L = Vector4.Clamp(this.V3L + off, min, max); - this.V3R = Vector4.Clamp(this.V3R + off, min, max); - this.V4L = Vector4.Clamp(this.V4L + off, min, max); - this.V4R = Vector4.Clamp(this.V4R + off, min, max); - this.V5L = Vector4.Clamp(this.V5L + off, min, max); - this.V5R = Vector4.Clamp(this.V5R + off, min, max); - this.V6L = Vector4.Clamp(this.V6L + off, min, max); - this.V6R = Vector4.Clamp(this.V6R + off, min, max); - this.V7L = Vector4.Clamp(this.V7L + off, min, max); - this.V7R = Vector4.Clamp(this.V7R + off, min, max); - } - - /// - /// Rounds all values in the block. - /// - public void RoundInPlace() - { - for (int i = 0; i < Size; i++) - { - this[i] = MathF.Round(this[i]); + Vector4 min = Vector4.Zero; + Vector4 max = new(maximum); + Vector4 off = new(MathF.Ceiling(maximum * 0.5F)); + + this.V0L = Vector4.Clamp(this.V0L + off, min, max); + this.V0R = Vector4.Clamp(this.V0R + off, min, max); + this.V1L = Vector4.Clamp(this.V1L + off, min, max); + this.V1R = Vector4.Clamp(this.V1R + off, min, max); + this.V2L = Vector4.Clamp(this.V2L + off, min, max); + this.V2R = Vector4.Clamp(this.V2R + off, min, max); + this.V3L = Vector4.Clamp(this.V3L + off, min, max); + this.V3R = Vector4.Clamp(this.V3R + off, min, max); + this.V4L = Vector4.Clamp(this.V4L + off, min, max); + this.V4R = Vector4.Clamp(this.V4R + off, min, max); + this.V5L = Vector4.Clamp(this.V5L + off, min, max); + this.V5R = Vector4.Clamp(this.V5R + off, min, max); + this.V6L = Vector4.Clamp(this.V6L + off, min, max); + this.V6R = Vector4.Clamp(this.V6R + off, min, max); + this.V7L = Vector4.Clamp(this.V7L + off, min, max); + this.V7R = Vector4.Clamp(this.V7R + off, min, max); } } @@ -533,7 +514,7 @@ internal partial struct Block8x8F : IEquatable } /// - public bool Equals(Block8x8F other) + public readonly bool Equals(Block8x8F other) => this.V0L == other.V0L && this.V0R == other.V0R && this.V1L == other.V1L diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs new file mode 100644 index 000000000..03469b2e6 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykOperator.cs @@ -0,0 +1,168 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements inverted JPEG CMYK conversion for scalar and SIMD lanes. + /// + internal readonly struct CmykOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.Cmyk; + + /// + public static int ComponentCount => 4; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + // Adobe-style CMYK stores inverted component samples. Multiplying K by scale twice folds the + // two sample-domain divisions into one factor before it modulates the C, M, and Y planes. + float scaledK = c3 * scale * scale; + c0 *= scaledK; + c1 *= scaledK; + c2 *= scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + // Each K lane supplies the common modulation factor for the corresponding C, M, and Y lanes. + Vector128 scaledK = c3 * scale * scale; + c0 *= scaledK; + c1 *= scaledK; + c2 *= scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + // Eight independent CMYK samples remain lane-aligned throughout the modulation. + Vector256 scaledK = c3 * scale * scale; + c0 *= scaledK; + c1 *= scaledK; + c2 *= scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + // Sixteen independent CMYK samples remain lane-aligned throughout the modulation. + Vector512 scaledK = c3 * scale * scale; + c0 *= scaledK; + c1 *= scaledK; + c2 *= scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + float c = maximumValue - r; + float m = maximumValue - g; + float y = maximumValue - b; + float k = MathF.Min(c, MathF.Min(m, y)); + + // Pure black makes the chromatic divisor zero. In that case chromatic ink is defined as zero; + // otherwise remove K and normalize the remaining C, M, and Y contributions. + if (k >= maximumValue) + { + c = 0; + m = 0; + y = 0; + } + else + { + // The same remaining range normalizes every chromatic channel. Computing its reciprocal once + // replaces three divisions with one division and three multiplies. + float reciprocal = 1F / (maximumValue - k); + c = (c - k) * reciprocal; + m = (m - k) * reciprocal; + y = (y - k) * reciprocal; + } + + // JPEG CMYK is inverted, including K, so normalized chromatic values are reflected around max. + c0 = maximumValue - (c * maximumValue); + c1 = maximumValue - (m * maximumValue); + c2 = maximumValue - (y * maximumValue); + c3 = maximumValue - k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + Vector128 c = maximumValue - r; + Vector128 m = maximumValue - g; + Vector128 y = maximumValue - b; + Vector128 k = Vector128.Min(c, Vector128.Min(m, y)); + + // The all-bits mask clears the undefined zero-divisor result for pure-black lanes without a branch. + Vector128 nonBlack = ~Vector128.Equals(k, maximumValue); + Vector128 reciprocal = Vector128.One / (maximumValue - k); + c = ((c - k) * reciprocal) & nonBlack; + m = ((m - k) * reciprocal) & nonBlack; + y = ((y - k) * reciprocal) & nonBlack; + + c0 = maximumValue - (c * maximumValue); + c1 = maximumValue - (m * maximumValue); + c2 = maximumValue - (y * maximumValue); + c3 = maximumValue - k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + Vector256 c = maximumValue - r; + Vector256 m = maximumValue - g; + Vector256 y = maximumValue - b; + Vector256 k = Vector256.Min(c, Vector256.Min(m, y)); + + // Masking preserves lane independence when a vector mixes pure black with chromatic pixels. + Vector256 nonBlack = ~Vector256.Equals(k, maximumValue); + Vector256 reciprocal = Vector256.One / (maximumValue - k); + c = ((c - k) * reciprocal) & nonBlack; + m = ((m - k) * reciprocal) & nonBlack; + y = ((y - k) * reciprocal) & nonBlack; + + c0 = maximumValue - (c * maximumValue); + c1 = maximumValue - (m * maximumValue); + c2 = maximumValue - (y * maximumValue); + c3 = maximumValue - k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + Vector512 c = maximumValue - r; + Vector512 m = maximumValue - g; + Vector512 y = maximumValue - b; + Vector512 k = Vector512.Min(c, Vector512.Min(m, y)); + + // AVX-512 still uses a full floating-point mask value here because bitwise clearing exactly matches + // the narrower operator semantics and lets the JIT select the most suitable native instructions. + Vector512 nonBlack = ~Vector512.Equals(k, maximumValue); + Vector512 reciprocal = Vector512.One / (maximumValue - k); + c = ((c - k) * reciprocal) & nonBlack; + m = ((m - k) * reciprocal) & nonBlack; + y = ((y - k) * reciprocal) & nonBlack; + + c0 = maximumValue - (c * maximumValue); + c1 = maximumValue - (m * maximumValue); + c2 = maximumValue - (y * maximumValue); + c3 = maximumValue - k; + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs deleted file mode 100644 index ebaa7c4b0..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class CmykScalar : JpegColorConverterScalar - { - public CmykScalar(int precision) - : base(JpegColorSpace.Cmyk, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) => - ConvertToRgbInPlace(values, this.MaximumValue); - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(values, this.MaximumValue, rLane, gLane, bLane); - - public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue) - { - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - Span c3 = values.Component3; - - float scale = 1 / (maxValue * maxValue); - for (int i = 0; i < c0.Length; i++) - { - float c = c0[i]; - float m = c1[i]; - float y = c2[i]; - float k = c3[i]; - - k *= scale; - c0[i] = c * k; - c1[i] = m * k; - c2[i] = y * k; - } - } - - public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) - { - Span c = values.Component0; - Span m = values.Component1; - Span y = values.Component2; - Span k = values.Component3; - - for (int i = 0; i < c.Length; i++) - { - float ctmp = 255f - rLane[i]; - float mtmp = 255f - gLane[i]; - float ytmp = 255f - bLane[i]; - float ktmp = MathF.Min(MathF.Min(ctmp, mtmp), ytmp); - - if (ktmp >= 255f) - { - ctmp = 0f; - mtmp = 0f; - ytmp = 0f; - } - else - { - ctmp = (ctmp - ktmp) / (255f - ktmp); - mtmp = (mtmp - ktmp) / (255f - ktmp); - ytmp = (ytmp - ktmp) / (255f - ktmp); - } - - c[i] = maxValue - (ctmp * maxValue); - m[i] = maxValue - (mtmp * maxValue); - y[i] = maxValue - (ytmp * maxValue); - k[i] = maxValue - ktmp; - } - } - - public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) - { - using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); - Span packed = memoryOwner.Memory.Span; - - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - Span c3 = values.Component3; - - PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); - - Span source = MemoryMarshal.Cast(packed); - Span destination = MemoryMarshal.Cast(packed)[..source.Length]; - - ColorConversionOptions options = new() - { - SourceIccProfile = profile, - TargetIccProfile = CompactSrgbV4Profile.Profile, - }; - ColorProfileConverter converter = new(options); - converter.Convert(source, destination); - - UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs deleted file mode 100644 index 14addafc1..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class CmykVector128 : JpegColorConverterVector128 - { - public CmykVector128(int precision) - : base(JpegColorSpace.Cmyk, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector128 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector128 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - // Used for the color conversion - Vector128 scale = Vector128.Create(1 / (this.MaximumValue * this.MaximumValue)); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector128 c = ref Unsafe.Add(ref c0Base, i); - ref Vector128 m = ref Unsafe.Add(ref c1Base, i); - ref Vector128 y = ref Unsafe.Add(ref c2Base, i); - Vector128 k = Unsafe.Add(ref c3Base, i); - - k *= scale; - c *= k; - m *= k; - y *= k; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); - - public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) - { - ref Vector128 destC = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 destM = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector128 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - ref Vector128 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector128 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector128 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector128 scale = Vector128.Create(maxValue); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - Vector128 ctmp = scale - Unsafe.Add(ref srcR, i); - Vector128 mtmp = scale - Unsafe.Add(ref srcG, i); - Vector128 ytmp = scale - Unsafe.Add(ref srcB, i); - Vector128 ktmp = Vector128.Min(ctmp, Vector128.Min(mtmp, ytmp)); - - Vector128 kMask = ~Vector128.Equals(ktmp, scale); - Vector128 divisor = scale - ktmp; - - ctmp = ((ctmp - ktmp) / divisor) & kMask; - mtmp = ((mtmp - ktmp) / divisor) & kMask; - ytmp = ((ytmp - ktmp) / divisor) & kMask; - - Unsafe.Add(ref destC, i) = scale - (ctmp * scale); - Unsafe.Add(ref destM, i) = scale - (mtmp * scale); - Unsafe.Add(ref destY, i) = scale - (ytmp * scale); - Unsafe.Add(ref destK, i) = scale - ktmp; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs deleted file mode 100644 index 98bda53d2..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class CmykVector256 : JpegColorConverterVector256 - { - public CmykVector256(int precision) - : base(JpegColorSpace.Cmyk, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector256 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector256 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - // Used for the color conversion - Vector256 scale = Vector256.Create(1 / (this.MaximumValue * this.MaximumValue)); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector256 c = ref Unsafe.Add(ref c0Base, i); - ref Vector256 m = ref Unsafe.Add(ref c1Base, i); - ref Vector256 y = ref Unsafe.Add(ref c2Base, i); - Vector256 k = Unsafe.Add(ref c3Base, i); - - k *= scale; - c *= k; - m *= k; - y *= k; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); - - public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) - { - ref Vector256 destC = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 destM = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector256 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - ref Vector256 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector256 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector256 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector256 scale = Vector256.Create(maxValue); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - Vector256 ctmp = scale - Unsafe.Add(ref srcR, i); - Vector256 mtmp = scale - Unsafe.Add(ref srcG, i); - Vector256 ytmp = scale - Unsafe.Add(ref srcB, i); - Vector256 ktmp = Vector256.Min(ctmp, Vector256.Min(mtmp, ytmp)); - - Vector256 kMask = ~Vector256.Equals(ktmp, scale); - Vector256 divisor = scale - ktmp; - - ctmp = ((ctmp - ktmp) / divisor) & kMask; - mtmp = ((mtmp - ktmp) / divisor) & kMask; - ytmp = ((ytmp - ktmp) / divisor) & kMask; - - Unsafe.Add(ref destC, i) = scale - (ctmp * scale); - Unsafe.Add(ref destM, i) = scale - (mtmp * scale); - Unsafe.Add(ref destY, i) = scale - (ytmp * scale); - Unsafe.Add(ref destK, i) = scale - ktmp; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs deleted file mode 100644 index c72af2faf..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class CmykVector512 : JpegColorConverterVector512 - { - public CmykVector512(int precision) - : base(JpegColorSpace.Cmyk, precision) - { - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) - { - ref Vector512 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector512 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - // Used for the color conversion - Vector512 scale = Vector512.Create(1 / (this.MaximumValue * this.MaximumValue)); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector512 c = ref Unsafe.Add(ref c0Base, i); - ref Vector512 m = ref Unsafe.Add(ref c1Base, i); - ref Vector512 y = ref Unsafe.Add(ref c2Base, i); - Vector512 k = Unsafe.Add(ref c3Base, i); - - k *= scale; - c *= k; - m *= k; - y *= k; - } - } - - /// - protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgbVectorized(in values, this.MaximumValue, rLane, gLane, bLane); - - /// - protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => CmykScalar.ConvertToRgbInPlace(values, this.MaximumValue); - - /// - protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => CmykScalar.ConvertFromRgb(values, this.MaximumValue, rLane, gLane, bLane); - - internal static void ConvertFromRgbVectorized(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) - { - ref Vector512 destC = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 destM = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector512 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - ref Vector512 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector512 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector512 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector512 scale = Vector512.Create(maxValue); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - Vector512 ctmp = scale - Unsafe.Add(ref srcR, i); - Vector512 mtmp = scale - Unsafe.Add(ref srcG, i); - Vector512 ytmp = scale - Unsafe.Add(ref srcB, i); - Vector512 ktmp = Vector512.Min(ctmp, Vector512.Min(mtmp, ytmp)); - - Vector512 kMask = ~Vector512.Equals(ktmp, scale); - Vector512 divisor = scale - ktmp; - - ctmp = ((ctmp - ktmp) / divisor) & kMask; - mtmp = ((mtmp - ktmp) / divisor) & kMask; - ytmp = ((ytmp - ktmp) / divisor) & kMask; - - Unsafe.Add(ref destC, i) = scale - (ctmp * scale); - Unsafe.Add(ref destM, i) = scale - (mtmp * scale); - Unsafe.Add(ref destY, i) = scale - (ytmp * scale); - Unsafe.Add(ref destK, i) = scale - ktmp; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs new file mode 100644 index 000000000..6e5753d2f --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleOperator.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements grayscale expansion and RGB luminance reduction for scalar and SIMD lanes. + /// + internal readonly struct GrayScaleOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.Grayscale; + + /// + public static int ComponentCount => 1; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + // JPEG stores luminance in the integer sample domain. Normalize it once, then duplicate the + // same value into all three RGB planes. Keeping it local also prevents potentially aliasing + // byref stores from forcing the JIT to reload c0 between assignments. + float luminance = c0 * scale; + c0 = luminance; + c1 = luminance; + c2 = luminance; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + // Each XMM lane is one independent luminance sample. Reusing the normalized vector for R, G, + // and B avoids recomputing the scale and keeps it live across potentially aliasing byref stores. + Vector128 luminance = c0 * scale; + c0 = luminance; + c1 = luminance; + c2 = luminance; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + // Eight luminance samples occupy the YMM lanes. The local retains the normalized vector across + // all three output stores even when the destination planes alias. + Vector256 luminance = c0 * scale; + c0 = luminance; + c1 = luminance; + c2 = luminance; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + // Sixteen luminance samples occupy the ZMM lanes. The local retains the normalized vector across + // all three output stores without shuffles, interleaving, or source reloads. + Vector512 luminance = c0 * scale; + c0 = luminance; + c1 = luminance; + c2 = luminance; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + // Rec.601 luma weights operate directly in the encoder sample domain. Only c0 is stored for a + // one-component model; the remaining out values exist solely to satisfy the common operator shape. + c0 = (0.299F * r) + (0.587F * g) + (0.114F * b); + c1 = 0; + c2 = 0; + c3 = 0; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + // The nested estimate gives each pixel the same multiply-add grouping as the scalar Rec.601 formula. + c0 = Vector128_.MultiplyAddEstimate(Vector128.Create(0.299F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(0.587F), g, Vector128.Create(0.114F) * b)); + c1 = default; + c2 = default; + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + // YMM lanes evaluate the same Rec.601 equation independently, with no horizontal lane reduction. + c0 = Vector256_.MultiplyAddEstimate(Vector256.Create(0.299F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(0.587F), g, Vector256.Create(0.114F) * b)); + c1 = default; + c2 = default; + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + // ZMM lanes retain the same arithmetic order as narrower paths so only SIMD width changes. + c0 = Vector512_.MultiplyAddEstimate(Vector512.Create(0.299F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(0.587F), g, Vector512.Create(0.114F) * b)); + c1 = default; + c2 = default; + c3 = default; + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs deleted file mode 100644 index 74869c93c..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class GrayScaleScalar : JpegColorConverterScalar - { - public GrayScaleScalar(int precision) - : base(JpegColorSpace.Grayscale, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - => ConvertToRgbInPlace(in values, this.MaximumValue); - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgbScalar(values, rLane, gLane, bLane); - - internal static void ConvertToRgbInPlace(in ComponentValues values, float maxValue) - { - ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); - ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); - ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); - - float scale = 1F / maxValue; - for (nuint i = 0; i < (nuint)values.Component0.Length; i++) - { - float c = Unsafe.Add(ref c0Base, i) * scale; - - Unsafe.Add(ref c0Base, i) = c; - Unsafe.Add(ref c1Base, i) = c; - Unsafe.Add(ref c2Base, i) = c; - } - } - - public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) - { - using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); - Span packed = memoryOwner.Memory.Span; - - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - - ref float c0Base = ref MemoryMarshal.GetReference(c0); - ref float c1Base = ref MemoryMarshal.GetReference(c1); - ref float c2Base = ref MemoryMarshal.GetReference(c2); - - float scale = 1F / maxValue; - for (nuint i = 0; i < (nuint)values.Component0.Length; i++) - { - ref float c = ref Unsafe.Add(ref c0Base, i); - c *= scale; - } - - Span source = MemoryMarshal.Cast(values.Component0); - Span destination = MemoryMarshal.Cast(packed); - - ColorConversionOptions options = new() - { - SourceIccProfile = profile, - TargetIccProfile = CompactSrgbV4Profile.Profile, - }; - ColorProfileConverter converter = new(options); - converter.Convert(source, destination); - - UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); - } - - internal static void ConvertFromRgbScalar(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - Span c0 = values.Component0; - - for (int i = 0; i < c0.Length; i++) - { - // luminosity = (0.299 * r) + (0.587 * g) + (0.114 * b) - c0[i] = (float)((0.299f * rLane[i]) + (0.587f * gLane[i]) + (0.114f * bLane[i])); - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs deleted file mode 100644 index 633080706..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class GrayScaleVector128 : JpegColorConverterVector128 - { - public GrayScaleVector128(int precision) - : base(JpegColorSpace.Grayscale, precision) - { - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector128 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - - ref Vector128 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - - ref Vector128 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - // Used for the color conversion - Vector128 scale = Vector128.Create(1 / this.MaximumValue); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - Vector128 c = Unsafe.Add(ref c0Base, i) * scale; - - Unsafe.Add(ref c0Base, i) = c; - Unsafe.Add(ref c1Base, i) = c; - Unsafe.Add(ref c2Base, i) = c; - } - } - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - ref Vector128 destLuminance = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - - ref Vector128 srcRed = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector128 srcGreen = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector128 srcBlue = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - // Used for the color conversion - Vector128 f0299 = Vector128.Create(0.299f); - Vector128 f0587 = Vector128.Create(0.587f); - Vector128 f0114 = Vector128.Create(0.114f); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector128 r = ref Unsafe.Add(ref srcRed, i); - ref Vector128 g = ref Unsafe.Add(ref srcGreen, i); - ref Vector128 b = ref Unsafe.Add(ref srcBlue, i); - - // luminosity = (0.299 * r) + (0.587 * g) + (0.114 * b) - Unsafe.Add(ref destLuminance, i) = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs deleted file mode 100644 index 0b2b8a119..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class GrayScaleVector256 : JpegColorConverterVector256 - { - public GrayScaleVector256(int precision) - : base(JpegColorSpace.Grayscale, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector256 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - - ref Vector256 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - - ref Vector256 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - // Used for the color conversion - Vector256 scale = Vector256.Create(1 / this.MaximumValue); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - Vector256 c = Unsafe.Add(ref c0Base, i) * scale; - - Unsafe.Add(ref c0Base, i) = c; - Unsafe.Add(ref c1Base, i) = c; - Unsafe.Add(ref c2Base, i) = c; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - ref Vector256 destLuminance = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - - ref Vector256 srcRed = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector256 srcGreen = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector256 srcBlue = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - // Used for the color conversion - Vector256 f0299 = Vector256.Create(0.299f); - Vector256 f0587 = Vector256.Create(0.587f); - Vector256 f0114 = Vector256.Create(0.114f); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector256 r = ref Unsafe.Add(ref srcRed, i); - ref Vector256 g = ref Unsafe.Add(ref srcGreen, i); - ref Vector256 b = ref Unsafe.Add(ref srcBlue, i); - - // luminosity = (0.299 * r) + (0.587 * g) + (0.114 * b) - Unsafe.Add(ref destLuminance, i) = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs deleted file mode 100644 index 0a58b0196..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class GrayScaleVector512 : JpegColorConverterVector512 - { - public GrayScaleVector512(int precision) - : base(JpegColorSpace.Grayscale, precision) - { - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => GrayScaleScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) - { - ref Vector512 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - - ref Vector512 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - - ref Vector512 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - // Used for the color conversion - Vector512 scale = Vector512.Create(1 / this.MaximumValue); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - Vector512 c = Unsafe.Add(ref c0Base, i) * scale; - - Unsafe.Add(ref c0Base, i) = c; - Unsafe.Add(ref c1Base, i) = c; - Unsafe.Add(ref c2Base, i) = c; - } - } - - /// - protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - ref Vector512 destLuminance = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - - ref Vector512 srcRed = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector512 srcGreen = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector512 srcBlue = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - // Used for the color conversion - Vector512 f0299 = Vector512.Create(0.299f); - Vector512 f0587 = Vector512.Create(0.587f); - Vector512 f0114 = Vector512.Create(0.114f); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector512 r = ref Unsafe.Add(ref srcRed, i); - ref Vector512 g = ref Unsafe.Add(ref srcGreen, i); - ref Vector512 b = ref Unsafe.Add(ref srcBlue, i); - - // luminosity = (0.299 * r) + (0.587 * g) + (0.114 * b) - Unsafe.Add(ref destLuminance, i) = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - } - } - - /// - protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => GrayScaleScalar.ConvertToRgbInPlace(in values, this.MaximumValue); - - /// - protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => GrayScaleScalar.ConvertFromRgbScalar(values, rLane, gLane, bLane); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs new file mode 100644 index 000000000..3b2ab914b --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs @@ -0,0 +1,449 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Defines the color-model-specific arithmetic used by . + /// + /// + /// Each overload describes the same lane-wise transform. The generic traversal selects the widest + /// available overload and the JIT resolves these static interface calls for each closed converter type. + /// + internal interface IJpegColorConverterOperator + { + /// + /// Gets the JPEG color space handled by the operator. + /// + public static abstract JpegColorSpace ColorSpace { get; } + + /// + /// Gets the number of component planes used by the color space. + /// + public static abstract int ComponentCount { get; } + + /// + /// Converts one JPEG sample to normalized RGB. + /// + /// The first component, replaced by red. + /// The second component, replaced by green. + /// The third component, replaced by blue. + /// The fourth component, or zero for a three-component color space. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of . + public static abstract void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale); + + /// + /// Converts four JPEG samples to normalized RGB. + /// + /// The first component lanes, replaced by red. + /// The second component lanes, replaced by green. + /// The third component lanes, replaced by blue. + /// The fourth component lanes, or zero for a three-component color space. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + public static abstract void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale); + + /// + /// Converts eight JPEG samples to normalized RGB. + /// + /// The first component lanes, replaced by red. + /// The second component lanes, replaced by green. + /// The third component lanes, replaced by blue. + /// The fourth component lanes, or zero for a three-component color space. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + public static abstract void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale); + + /// + /// Converts sixteen JPEG samples to normalized RGB. + /// + /// The first component lanes, replaced by red. + /// The second component lanes, replaced by green. + /// The third component lanes, replaced by blue. + /// The fourth component lanes, or zero for a three-component color space. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + public static abstract void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale); + + /// + /// Converts one RGB sample to JPEG components. + /// + /// The red value. + /// The green value. + /// The blue value. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of . + /// The first converted component. + /// The second converted component. + /// The third converted component. + /// The fourth converted component, if used. + public static abstract void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3); + + /// + /// Converts four RGB samples to JPEG components. + /// + /// The red lanes. + /// The green lanes. + /// The blue lanes. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + /// The fourth converted component lanes, if used. + public static abstract void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3); + + /// + /// Converts eight RGB samples to JPEG components. + /// + /// The red lanes. + /// The green lanes. + /// The blue lanes. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + /// The fourth converted component lanes, if used. + public static abstract void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3); + + /// + /// Converts sixteen RGB samples to JPEG components. + /// + /// The red lanes. + /// The green lanes. + /// The blue lanes. + /// The maximum component value for the configured precision. + /// The midpoint component value for the configured precision. + /// The reciprocal of in every lane. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + /// The fourth converted component lanes, if used. + public static abstract void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3); + } + + /// + /// Converts a JPEG color model using a single operator-driven traversal for all SIMD widths. + /// + /// The color-model-specific arithmetic. + internal sealed class JpegColorConverter : JpegColorConverterBase + where TOperator : struct, IJpegColorConverterOperator + { + /// + /// Initializes a new instance of the class. + /// + /// The precision in bits. + public JpegColorConverter(int precision) + : base(TOperator.ColorSpace, precision) + { + } + + /// + public override bool IsAvailable => true; + + /// + public override int ElementsPerBatch + => Vector512.IsHardwareAccelerated ? Vector512.Count : Vector256.IsHardwareAccelerated ? Vector256.Count : Vector128.IsHardwareAccelerated ? Vector128.Count : 1; + + /// + public override void ConvertToRgbInPlace(in ComponentValues values) + { + // JPEG component processors own equally sized planar buffers. Capturing their first elements + // as byrefs lets every width share the same offset without introducing Span bounds checks in + // the hot loops. Component3 may be empty; its byref is only dereferenced for four-component operators. + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + ref float c3Base = ref MemoryMarshal.GetReference(values.Component3); + + int length = values.Component0.Length; + int i = 0; + float scale = 1F / this.MaximumValue; + + // Descending widths keep one traversal while allowing an AVX-512 machine to process + // an eight-pixel JPEG block with AVX2 rather than sending the entire block to scalar code. + if (Vector512.IsHardwareAccelerated) + { + // Subtracting the lane count turns the loop condition into a single signed comparison. + // A negative value naturally skips this width, and i <= end proves every unaligned + // 64-byte reinterpretation remains entirely inside its component buffer. + int oneVectorFromEnd = length - Vector512.Count; + + if (i <= oneVectorFromEnd) + { + // Precision-derived values are broadcast only when this width has work. Keeping them outside + // the loop avoids repeated setup without penalizing rows handled entirely by narrower widths. + Vector512 maximumValue = Vector512.Create(this.MaximumValue); + Vector512 halfValue = Vector512.Create(this.HalfValue); + Vector512 scaleVector = Vector512.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + ref Vector512 c0 = ref Unsafe.As>(ref Unsafe.Add(ref c0Base, i)); + ref Vector512 c1 = ref Unsafe.As>(ref Unsafe.Add(ref c1Base, i)); + ref Vector512 c2 = ref Unsafe.As>(ref Unsafe.Add(ref c2Base, i)); + + // ComponentCount is a static property on the closed operator type, so the JIT removes + // this choice. Three-component models never dereference the empty Component3 byref. + Vector512 c3 = TOperator.ComponentCount == 4 ? Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) : default; + + // c0-c2 alias the planar source vectors and are replaced in place with normalized RGB. + // c3 is passed by value because the fourth JPEG component must remain unchanged. + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, c3, maximumValue, halfValue, scaleVector); + } + } + } + + if (Vector256.IsHardwareAccelerated) + { + // The shared offset continues where AVX-512 stopped. At this point fewer than sixteen + // samples remain, so this stage consumes the complete eight-sample remainder when present. + int oneVectorFromEnd = length - Vector256.Count; + + if (i <= oneVectorFromEnd) + { + // YMM precision state is materialized only for an eight-sample remainder or an AVX2-only loop. + Vector256 maximumValue = Vector256.Create(this.MaximumValue); + Vector256 halfValue = Vector256.Create(this.HalfValue); + Vector256 scaleVector = Vector256.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + ref Vector256 c0 = ref Unsafe.As>(ref Unsafe.Add(ref c0Base, i)); + ref Vector256 c1 = ref Unsafe.As>(ref Unsafe.Add(ref c1Base, i)); + ref Vector256 c2 = ref Unsafe.As>(ref Unsafe.Add(ref c2Base, i)); + + // The closed operator makes this a compile-time color-model choice, not a per-vector + // runtime abstraction or interface dispatch. + Vector256 c3 = TOperator.ComponentCount == 4 ? Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) : default; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, c3, maximumValue, halfValue, scaleVector); + } + } + } + + if (Vector128.IsHardwareAccelerated) + { + // SSE/AdvSimd handles the final four complete samples. This also gives non-AVX machines + // the same traversal without duplicating the control flow for another register width. + int oneVectorFromEnd = length - Vector128.Count; + + if (i <= oneVectorFromEnd) + { + // XMM state is likewise created only when four samples remain for this stage. + Vector128 maximumValue = Vector128.Create(this.MaximumValue); + Vector128 halfValue = Vector128.Create(this.HalfValue); + Vector128 scaleVector = Vector128.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ref Vector128 c0 = ref Unsafe.As>(ref Unsafe.Add(ref c0Base, i)); + ref Vector128 c1 = ref Unsafe.As>(ref Unsafe.Add(ref c1Base, i)); + ref Vector128 c2 = ref Unsafe.As>(ref Unsafe.Add(ref c2Base, i)); + + // As at the wider stages, the fourth vector is loaded only for CMYK-shaped operators. + Vector128 c3 = TOperator.ComponentCount == 4 ? Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) : default; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, c3, maximumValue, halfValue, scaleVector); + } + } + } + + // Fewer than four samples remain after the SIMD cascade. Processing from the shared offset + // guarantees each sample is visited exactly once for arbitrary test lengths and JPEG block rows. + for (; i < length; i++) + { + float c3 = TOperator.ComponentCount == 4 ? Unsafe.Add(ref c3Base, i) : 0; + + TOperator.ConvertToRgb(ref Unsafe.Add(ref c0Base, i), ref Unsafe.Add(ref c1Base, i), ref Unsafe.Add(ref c2Base, i), c3, this.MaximumValue, this.HalfValue, scale); + } + } + + /// + public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) + { + // The encoder supplies equally sized RGB planes and destination component planes. Byrefs preserve + // contiguous access and allow the same proven vector boundary to govern every participating lane. + // Component3 is empty for three-component formats and is only written by four-component operators. + ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); + ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); + ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); + ref float c3Base = ref MemoryMarshal.GetReference(values.Component3); + ref float rBase = ref MemoryMarshal.GetReference(rLane); + ref float gBase = ref MemoryMarshal.GetReference(gLane); + ref float bBase = ref MemoryMarshal.GetReference(bLane); + + int length = values.Component0.Length; + int i = 0; + float scale = 1F / this.MaximumValue; + + // Each vector overload returns planar component vectors. Storing them here keeps the + // operator concerned only with color arithmetic and preserves contiguous lane access. + if (Vector512.IsHardwareAccelerated) + { + // The end offset proves all three 64-byte RGB reads and all component writes are in range. + // A short row yields a negative end and falls through to the next supported width. + int oneVectorFromEnd = length - Vector512.Count; + + if (i <= oneVectorFromEnd) + { + // Operators receive width-matched precision state only when this width has work, keeping + // invariant broadcasts outside the loop without charging narrower or scalar rows for them. + Vector512 maximumValue = Vector512.Create(this.MaximumValue); + Vector512 halfValue = Vector512.Create(this.HalfValue); + Vector512 scaleVector = Vector512.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 r = Unsafe.As>(ref Unsafe.Add(ref rBase, i)); + Vector512 g = Unsafe.As>(ref Unsafe.Add(ref gBase, i)); + Vector512 b = Unsafe.As>(ref Unsafe.Add(ref bBase, i)); + + TOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scaleVector, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3); + + // Outputs remain planar: each vector contains sixteen consecutive samples from one + // JPEG component. Static count checks prevent grayscale from touching absent planes + // while disappearing completely from three- and four-component specializations. + Unsafe.As>(ref Unsafe.Add(ref c0Base, i)) = c0; + + if (TOperator.ComponentCount >= 2) + { + Unsafe.As>(ref Unsafe.Add(ref c1Base, i)) = c1; + } + + if (TOperator.ComponentCount >= 3) + { + Unsafe.As>(ref Unsafe.Add(ref c2Base, i)) = c2; + } + + if (TOperator.ComponentCount >= 4) + { + Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) = c3; + } + } + } + } + + if (Vector256.IsHardwareAccelerated) + { + // Continue from the AVX-512 offset so an eight-sample tail stays vectorized on AVX-512 CPUs. + int oneVectorFromEnd = length - Vector256.Count; + + if (i <= oneVectorFromEnd) + { + // Materialize YMM state only for an eight-sample remainder or an AVX2-only loop. + Vector256 maximumValue = Vector256.Create(this.MaximumValue); + Vector256 halfValue = Vector256.Create(this.HalfValue); + Vector256 scaleVector = Vector256.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 r = Unsafe.As>(ref Unsafe.Add(ref rBase, i)); + Vector256 g = Unsafe.As>(ref Unsafe.Add(ref gBase, i)); + Vector256 b = Unsafe.As>(ref Unsafe.Add(ref bBase, i)); + + TOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scaleVector, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3); + + // Static count checks write only planes owned by this color model. + Unsafe.As>(ref Unsafe.Add(ref c0Base, i)) = c0; + + if (TOperator.ComponentCount >= 2) + { + Unsafe.As>(ref Unsafe.Add(ref c1Base, i)) = c1; + } + + if (TOperator.ComponentCount >= 3) + { + Unsafe.As>(ref Unsafe.Add(ref c2Base, i)) = c2; + } + + if (TOperator.ComponentCount >= 4) + { + Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) = c3; + } + } + } + } + + if (Vector128.IsHardwareAccelerated) + { + // The final SIMD stage consumes four complete RGB samples on SSE or AdvSimd hardware. + int oneVectorFromEnd = length - Vector128.Count; + + if (i <= oneVectorFromEnd) + { + // Materialize XMM state only when the final SIMD stage can consume four samples. + Vector128 maximumValue = Vector128.Create(this.MaximumValue); + Vector128 halfValue = Vector128.Create(this.HalfValue); + Vector128 scaleVector = Vector128.Create(scale); + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 r = Unsafe.As>(ref Unsafe.Add(ref rBase, i)); + Vector128 g = Unsafe.As>(ref Unsafe.Add(ref gBase, i)); + Vector128 b = Unsafe.As>(ref Unsafe.Add(ref bBase, i)); + + TOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scaleVector, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3); + + // Four results are stored only for the planes represented by the closed operator. + Unsafe.As>(ref Unsafe.Add(ref c0Base, i)) = c0; + + if (TOperator.ComponentCount >= 2) + { + Unsafe.As>(ref Unsafe.Add(ref c1Base, i)) = c1; + } + + if (TOperator.ComponentCount >= 3) + { + Unsafe.As>(ref Unsafe.Add(ref c2Base, i)) = c2; + } + + if (TOperator.ComponentCount >= 4) + { + Unsafe.As>(ref Unsafe.Add(ref c3Base, i)) = c3; + } + } + } + } + + // Scalar conversion is reserved for the zero-to-three samples that cannot fill Vector128. + for (; i < length; i++) + { + TOperator.ConvertFromRgb(Unsafe.Add(ref rBase, i), Unsafe.Add(ref gBase, i), Unsafe.Add(ref bBase, i), this.MaximumValue, this.HalfValue, scale, out float c0, out float c1, out float c2, out float c3); + + Unsafe.Add(ref c0Base, i) = c0; + + if (TOperator.ComponentCount >= 2) + { + Unsafe.Add(ref c1Base, i) = c1; + } + + if (TOperator.ComponentCount >= 3) + { + Unsafe.Add(ref c2Base, i) = c2; + } + + if (TOperator.ComponentCount >= 4) + { + Unsafe.Add(ref c3Base, i) = c3; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Packing.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Packing.cs new file mode 100644 index 000000000..749e4773d --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Packing.cs @@ -0,0 +1,305 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Normalizes three planar component lanes and interleaves them into packed XYZ values. + /// + /// The planar X components. + /// The planar Y components. + /// The planar Z components. + /// The destination ordered as consecutive XYZ triples. + /// The normalization factor applied to every component. + public static void PackedNormalizeInterleave3(ReadOnlySpan xLane, ReadOnlySpan yLane, ReadOnlySpan zLane, Span packed, float scale) + { + DebugGuard.IsTrue(packed.Length % 3 == 0, "Packed length must be divisible by 3."); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 3, xLane.Length, nameof(packed)); + + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + int i = 0; + + if (Vector128.IsHardwareAccelerated) + { + Vector128 scaleVector = Vector128.Create(scale); + int oneVectorFromEnd = xLane.Length - Vector128.Count; + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + // Each source vector contains four consecutive samples from one plane: + // x = [X0 X1 X2 X3] + // y = [Y0 Y1 Y2 Y3] + // z = [Z0 Z1 Z2 Z3] + // Shifting X by one sample supplies the value that follows each XYZ triple: + // shiftedX = [X1 X2 X3 0] + // The transpose therefore produces overlapping rows [Xn Yn Zn Xn+1]. + // AlignRight joins those rows into three complete destination vectors, avoiding + // the scalar-sized stores that writing four independent Vector3 values requires. + Vector128 x = Unsafe.As>(ref Unsafe.Add(ref xLaneRef, i)) * scaleVector; + Vector128 y = Unsafe.As>(ref Unsafe.Add(ref yLaneRef, i)) * scaleVector; + Vector128 z = Unsafe.As>(ref Unsafe.Add(ref zLaneRef, i)) * scaleVector; + Vector128 shiftedX = Vector128_.ShiftRightBytesInVector(x.AsByte(), sizeof(float)).AsSingle(); + + Transpose4(x, y, z, shiftedX, out Vector128 pixel0, out Vector128 pixel1, out Vector128 pixel2, out Vector128 pixel3); + + // Dropping pixel2.X lets [Y2] complete [Y1 Z1 X2] from pixel1. + Vector128 shiftedPixel2 = Vector128_.ShiftRightBytesInVector(pixel2.AsByte(), sizeof(float)); + Vector128 packed1 = Vector128_.AlignRight(shiftedPixel2, pixel1.AsByte(), sizeof(float)).AsSingle(); + + // Dropping pixel3.X leaves [Y3 Z3] to complete [Z2 X3] from pixel2. + Vector128 shiftedPixel3 = Vector128_.ShiftRightBytesInVector(pixel3.AsByte(), sizeof(float)); + Vector128 packed2 = Vector128_.AlignRight(shiftedPixel3, pixel2.AsByte(), sizeof(float) * 2).AsSingle(); + + ref Vector128 destination = ref Unsafe.As>(ref Unsafe.Add(ref packedRef, (uint)i * 3)); + + destination = pixel0; + Unsafe.Add(ref destination, 1) = packed1; + Unsafe.Add(ref destination, 2) = packed2; + } + } + + // Fewer than four pixels remain after SIMD, or every pixel reaches this path + // when the runtime cannot accelerate the cross-vector transpose. + for (; i < xLane.Length; i++) + { + nuint sourceOffset = (uint)i; + nuint packedOffset = sourceOffset * 3; + Unsafe.Add(ref packedRef, packedOffset) = Unsafe.Add(ref xLaneRef, sourceOffset) * scale; + Unsafe.Add(ref packedRef, packedOffset + 1) = Unsafe.Add(ref yLaneRef, sourceOffset) * scale; + Unsafe.Add(ref packedRef, packedOffset + 2) = Unsafe.Add(ref zLaneRef, sourceOffset) * scale; + } + } + + /// + /// Deinterleaves packed XYZ values into three planar component lanes. + /// + /// The source ordered as consecutive XYZ triples. + /// The destination X components. + /// The destination Y components. + /// The destination Z components. + public static void UnpackDeinterleave3(ReadOnlySpan packed, Span xLane, Span yLane, Span zLane) + { + DebugGuard.IsTrue(packed.Length == xLane.Length, nameof(packed), "Channels must be of same size!"); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + + ref float packedRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(packed)); + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + int i = 0; + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = packed.Length - Vector128.Count; + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + // A Vector3 occupies twelve contiguous bytes, so a sixteen-byte load beginning + // at one pixel also reads the X component of the following pixel: + // pixel0 = [X0 Y0 Z0 X1] + // pixel1 = [X1 Y1 Z1 X2] + // The transpose discards this fourth column, making the overlap useful padding + // and avoiding two insert instructions per pixel. The final row needs explicit + // zero padding only when pixel3 is the last element in the source span. + nuint packedOffset = (uint)i * 3; + Vector128 pixel0 = Unsafe.As>(ref Unsafe.Add(ref packedRef, packedOffset)); + Vector128 pixel1 = Unsafe.As>(ref Unsafe.Add(ref packedRef, packedOffset + 3)); + Vector128 pixel2 = Unsafe.As>(ref Unsafe.Add(ref packedRef, packedOffset + 6)); + ref float pixel3Ref = ref Unsafe.Add(ref packedRef, packedOffset + 9); + Vector128 pixel3 = i + Vector128.Count < packed.Length ? Unsafe.As>(ref pixel3Ref) : Unsafe.As(ref pixel3Ref).AsVector128(); + + Transpose4(pixel0, pixel1, pixel2, pixel3, out Vector128 x, out Vector128 y, out Vector128 z, out _); + + Unsafe.As>(ref Unsafe.Add(ref xLaneRef, i)) = x; + Unsafe.As>(ref Unsafe.Add(ref yLaneRef, i)) = y; + Unsafe.As>(ref Unsafe.Add(ref zLaneRef, i)) = z; + } + } + + // The scalar remainder preserves the original scatter behavior for zero to + // three pixels and provides the complete fallback on unsupported hardware. + for (; i < packed.Length; i++) + { + nuint packedOffset = (uint)i * 3; + Unsafe.Add(ref xLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset); + Unsafe.Add(ref yLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset + 1); + Unsafe.Add(ref zLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset + 2); + } + } + + /// + /// Normalizes four planar component lanes and interleaves them into packed XYZW values. + /// + /// The planar X components. + /// The planar Y components. + /// The planar Z components. + /// The planar W components. + /// The destination ordered as consecutive XYZW groups. + /// The maximum component value used to normalize each component. + public static void PackedNormalizeInterleave4(ReadOnlySpan xLane, ReadOnlySpan yLane, ReadOnlySpan zLane, ReadOnlySpan wLane, Span packed, float maxValue) + { + DebugGuard.IsTrue(packed.Length % 4 == 0, "Packed length must be divisible by 4."); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + DebugGuard.IsTrue(wLane.Length == xLane.Length, nameof(wLane), "Channels must be of same size!"); + DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 4, xLane.Length, nameof(packed)); + + float scale = 1F / maxValue; + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + int i = 0; + + if (Vector128.IsHardwareAccelerated) + { + Vector128 scaleVector = Vector128.Create(scale); + int oneVectorFromEnd = xLane.Length - Vector128.Count; + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + // Four planar vectors form the rows of a 4x4 matrix. Transposition + // converts them into four complete [Xn Yn Zn Wn] pixel vectors, so + // normalization and interleaving require only four loads, four + // multiplies, the register transpose, and four contiguous stores. + Vector128 x = Unsafe.As>(ref Unsafe.Add(ref xLaneRef, i)) * scaleVector; + Vector128 y = Unsafe.As>(ref Unsafe.Add(ref yLaneRef, i)) * scaleVector; + Vector128 z = Unsafe.As>(ref Unsafe.Add(ref zLaneRef, i)) * scaleVector; + Vector128 w = Unsafe.As>(ref Unsafe.Add(ref wLaneRef, i)) * scaleVector; + + Transpose4(x, y, z, w, out Vector128 pixel0, out Vector128 pixel1, out Vector128 pixel2, out Vector128 pixel3); + + ref Vector128 destination = ref Unsafe.As>(ref Unsafe.Add(ref packedRef, (uint)i * 4)); + + destination = pixel0; + Unsafe.Add(ref destination, 1) = pixel1; + Unsafe.Add(ref destination, 2) = pixel2; + Unsafe.Add(ref destination, 3) = pixel3; + } + } + + // Process the zero-to-three trailing pixels with the same normalization + // and component order as the vector transpose. + for (; i < xLane.Length; i++) + { + nuint sourceOffset = (uint)i; + nuint packedOffset = sourceOffset * 4; + Unsafe.Add(ref packedRef, packedOffset) = Unsafe.Add(ref xLaneRef, sourceOffset) * scale; + Unsafe.Add(ref packedRef, packedOffset + 1) = Unsafe.Add(ref yLaneRef, sourceOffset) * scale; + Unsafe.Add(ref packedRef, packedOffset + 2) = Unsafe.Add(ref zLaneRef, sourceOffset) * scale; + Unsafe.Add(ref packedRef, packedOffset + 3) = Unsafe.Add(ref wLaneRef, sourceOffset) * scale; + } + } + + /// + /// Inverts and normalizes four planar component lanes before interleaving them into packed XYZW values. + /// + /// The inverted planar X components. + /// The inverted planar Y components. + /// The inverted planar Z components. + /// The inverted planar W components. + /// The destination ordered as consecutive conventional XYZW groups. + /// The maximum component value used for inversion and normalization. + public static void PackedInvertNormalizeInterleave4(ReadOnlySpan xLane, ReadOnlySpan yLane, ReadOnlySpan zLane, ReadOnlySpan wLane, Span packed, float maxValue) + { + DebugGuard.IsTrue(packed.Length % 4 == 0, "Packed length must be divisible by 4."); + DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); + DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); + DebugGuard.IsTrue(wLane.Length == xLane.Length, nameof(wLane), "Channels must be of same size!"); + DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 4, xLane.Length, nameof(packed)); + + float scale = 1F / maxValue; + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + int i = 0; + + if (Vector128.IsHardwareAccelerated) + { + Vector128 maximumVector = Vector128.Create(maxValue); + Vector128 scaleVector = Vector128.Create(scale); + int oneVectorFromEnd = xLane.Length - Vector128.Count; + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + // Adobe JPEG stores all four components inverted in the sample + // domain. Reflecting and normalizing the planar vectors before the + // transpose keeps both arithmetic operations lane-wise and leaves + // the transpose responsible only for the planar-to-packed layout. + Vector128 x = (maximumVector - Unsafe.As>(ref Unsafe.Add(ref xLaneRef, i))) * scaleVector; + Vector128 y = (maximumVector - Unsafe.As>(ref Unsafe.Add(ref yLaneRef, i))) * scaleVector; + Vector128 z = (maximumVector - Unsafe.As>(ref Unsafe.Add(ref zLaneRef, i))) * scaleVector; + Vector128 w = (maximumVector - Unsafe.As>(ref Unsafe.Add(ref wLaneRef, i))) * scaleVector; + + Transpose4(x, y, z, w, out Vector128 pixel0, out Vector128 pixel1, out Vector128 pixel2, out Vector128 pixel3); + + ref Vector128 destination = ref Unsafe.As>(ref Unsafe.Add(ref packedRef, (uint)i * 4)); + + destination = pixel0; + Unsafe.Add(ref destination, 1) = pixel1; + Unsafe.Add(ref destination, 2) = pixel2; + Unsafe.Add(ref destination, 3) = pixel3; + } + } + + // Preserve the original operation order for the zero-to-three trailing pixels: + // subtract in the sample domain, then multiply by the reciprocal maximum. + for (; i < xLane.Length; i++) + { + nuint sourceOffset = (uint)i; + nuint packedOffset = sourceOffset * 4; + Unsafe.Add(ref packedRef, packedOffset) = (maxValue - Unsafe.Add(ref xLaneRef, sourceOffset)) * scale; + Unsafe.Add(ref packedRef, packedOffset + 1) = (maxValue - Unsafe.Add(ref yLaneRef, sourceOffset)) * scale; + Unsafe.Add(ref packedRef, packedOffset + 2) = (maxValue - Unsafe.Add(ref zLaneRef, sourceOffset)) * scale; + Unsafe.Add(ref packedRef, packedOffset + 3) = (maxValue - Unsafe.Add(ref wLaneRef, sourceOffset)) * scale; + } + } + + /// + /// Transposes four four-lane rows into four four-lane columns. + /// + /// The first matrix row. + /// The second matrix row. + /// The third matrix row. + /// The fourth matrix row. + /// The first matrix column. + /// The second matrix column. + /// The third matrix column. + /// The fourth matrix column. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Transpose4(Vector128 row0, Vector128 row1, Vector128 row2, Vector128 row3, out Vector128 column0, out Vector128 column1, out Vector128 column2, out Vector128 column3) + { + // The first unpack interleaves adjacent 32-bit lanes from rows 0/1 and 2/3: + // row01Low = [r0c0 r1c0 r0c1 r1c1] + // row23Low = [r2c0 r3c0 r2c1 r3c1] + // A second unpack treats each adjacent pair as one 64-bit lane and combines + // the row01 and row23 pairs into complete columns. The integer views only + // expose the cross-platform unpack helpers; every floating-point bit is preserved. + Vector128 row01Low = Vector128_.UnpackLow(row0.AsInt32(), row1.AsInt32()); + Vector128 row01High = Vector128_.UnpackHigh(row0.AsInt32(), row1.AsInt32()); + Vector128 row23Low = Vector128_.UnpackLow(row2.AsInt32(), row3.AsInt32()); + Vector128 row23High = Vector128_.UnpackHigh(row2.AsInt32(), row3.AsInt32()); + + column0 = Vector128_.UnpackLow(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); + column1 = Vector128_.UnpackHigh(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); + column2 = Vector128_.UnpackLow(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); + column3 = Vector128_.UnpackHigh(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs new file mode 100644 index 000000000..7f05a5d08 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements direct JPEG RGB normalization and planar RGB copying for scalar and SIMD lanes. + /// + internal readonly struct RgbOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.RGB; + + /// + public static int ComponentCount => 3; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + // The JPEG planes already represent R, G, and B. Conversion therefore consists only of moving + // each integer-domain sample into the normalized floating-point domain consumed by pixel packing. + c0 *= scale; + c1 *= scale; + c2 *= scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + // Four samples from each planar channel remain in their lanes while sharing one normalization vector. + c0 *= scale; + c1 *= scale; + c2 *= scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + // Eight samples per plane are normalized independently without channel shuffles. + c0 *= scale; + c1 *= scale; + c2 *= scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + // Sixteen samples per plane are normalized independently without changing planar ordering. + c0 *= scale; + c1 *= scale; + c2 *= scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + // Encoder RGB lanes already use the JPEG sample domain, so the direct color model copies them. + c0 = r; + c1 = g; + c2 = b; + c3 = 0; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + // The planar vectors map one-to-one to JPEG components; the fourth result is statically discarded. + c0 = r; + c1 = g; + c2 = b; + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + // The planar vectors map one-to-one to JPEG components; no arithmetic or rearrangement is required. + c0 = r; + c1 = g; + c2 = b; + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + // The widest path is likewise a register-to-register planar copy for sixteen pixels. + c0 = r; + c1 = g; + c2 = b; + c3 = default; + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs deleted file mode 100644 index 92be9e896..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class RgbScalar : JpegColorConverterScalar - { - public RgbScalar(int precision) - : base(JpegColorSpace.RGB, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - => ConvertToRgbInPlace(values, this.MaximumValue); - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(values, rLane, gLane, bLane); - - public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) - { - using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); - Span packed = memoryOwner.Memory.Span; - - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - - PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maxValue); - - Span source = MemoryMarshal.Cast(packed); - Span destination = MemoryMarshal.Cast(packed); - - ColorConversionOptions options = new() - { - SourceIccProfile = profile, - TargetIccProfile = CompactSrgbV4Profile.Profile, - }; - ColorProfileConverter converter = new(options); - converter.Convert(source, destination); - - UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); - } - - internal static void ConvertToRgbInPlace(ComponentValues values, float maxValue) - { - ref float c0Base = ref MemoryMarshal.GetReference(values.Component0); - ref float c1Base = ref MemoryMarshal.GetReference(values.Component1); - ref float c2Base = ref MemoryMarshal.GetReference(values.Component2); - - float scale = 1F / maxValue; - - for (nuint i = 0; i < (nuint)values.Component0.Length; i++) - { - Unsafe.Add(ref c0Base, i) *= scale; - Unsafe.Add(ref c1Base, i) *= scale; - Unsafe.Add(ref c2Base, i) *= scale; - } - } - - internal static void ConvertFromRgb(ComponentValues values, Span rLane, Span gLane, Span bLane) - { - // TODO: This doesn't seem correct. We should be scaling to the maximum value here. - rLane.CopyTo(values.Component0); - gLane.CopyTo(values.Component1); - bLane.CopyTo(values.Component2); - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs deleted file mode 100644 index 6cbbc7c7c..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class RgbVector128 : JpegColorConverterVector128 - { - public RgbVector128(int precision) - : base(JpegColorSpace.RGB, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector128 rBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 gBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 bBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - // Used for the color conversion - Vector128 scale = Vector128.Create(1 / this.MaximumValue); - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector128 r = ref Unsafe.Add(ref rBase, i); - ref Vector128 g = ref Unsafe.Add(ref gBase, i); - ref Vector128 b = ref Unsafe.Add(ref bBase, i); - r *= scale; - g *= scale; - b *= scale; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - rLane.CopyTo(values.Component0); - gLane.CopyTo(values.Component1); - bLane.CopyTo(values.Component2); - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs deleted file mode 100644 index 10bc2be5f..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class RgbVector256 : JpegColorConverterVector256 - { - public RgbVector256(int precision) - : base(JpegColorSpace.RGB, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector256 rBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 gBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 bBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - // Used for the color conversion - Vector256 scale = Vector256.Create(1 / this.MaximumValue); - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector256 r = ref Unsafe.Add(ref rBase, i); - ref Vector256 g = ref Unsafe.Add(ref gBase, i); - ref Vector256 b = ref Unsafe.Add(ref bBase, i); - r *= scale; - g *= scale; - b *= scale; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - rLane.CopyTo(values.Component0); - gLane.CopyTo(values.Component1); - bLane.CopyTo(values.Component2); - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs deleted file mode 100644 index 6e01ad7cb..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class RgbVector512 : JpegColorConverterVector512 - { - public RgbVector512(int precision) - : base(JpegColorSpace.RGB, precision) - { - } - - /// - protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) - { - ref Vector512 rBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 gBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 bBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - // Used for the color conversion - Vector512 scale = Vector512.Create(1 / this.MaximumValue); - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector512 r = ref Unsafe.Add(ref rBase, i); - ref Vector512 g = ref Unsafe.Add(ref gBase, i); - ref Vector512 b = ref Unsafe.Add(ref bBase, i); - r *= scale; - g *= scale; - b *= scale; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => RgbScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - rLane.CopyTo(values.Component0); - gLane.CopyTo(values.Component1); - bLane.CopyTo(values.Component2); - } - - /// - protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => RgbScalar.ConvertToRgbInPlace(values, this.MaximumValue); - - /// - protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => RgbScalar.ConvertFromRgb(values, rLane, gLane, bLane); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs new file mode 100644 index 000000000..a3ba9ecb7 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykOperator.cs @@ -0,0 +1,154 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements non-inverted TIFF JPEG CMYK conversion for scalar and SIMD lanes. + /// + internal readonly struct TiffCmykOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.TiffCmyk; + + /// + public static int ComponentCount => 4; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + // TIFF stores conventional CMYK rather than Adobe's inverted representation. Normalize every + // component, invert C/M/Y, and let the remaining light after K modulate each RGB channel. + float k = 1F - (c3 * scale); + c0 = (1F - (c0 * scale)) * k; + c1 = (1F - (c1 * scale)) * k; + c2 = (1F - (c2 * scale)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + // K remains lane-aligned with its C/M/Y sample while one-minus performs the non-inverted CMYK mapping. + Vector128 k = Vector128.One - (c3 * scale); + c0 = (Vector128.One - (c0 * scale)) * k; + c1 = (Vector128.One - (c1 * scale)) * k; + c2 = (Vector128.One - (c2 * scale)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + // Eight conventional CMYK samples convert independently without channel rearrangement. + Vector256 k = Vector256.One - (c3 * scale); + c0 = (Vector256.One - (c0 * scale)) * k; + c1 = (Vector256.One - (c1 * scale)) * k; + c2 = (Vector256.One - (c2 * scale)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + // Sixteen conventional CMYK samples convert independently without channel rearrangement. + Vector512 k = Vector512.One - (c3 * scale); + c0 = (Vector512.One - (c0 * scale)) * k; + c1 = (Vector512.One - (c1 * scale)) * k; + c2 = (Vector512.One - (c2 * scale)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + float c = maximumValue - r; + float m = maximumValue - g; + float y = maximumValue - b; + float k = MathF.Min(c, MathF.Min(m, y)); + + // Removing the shared black contribution requires division by the remaining range. Pure black + // consumes that range completely, so its chromatic components are defined as zero. + if (k >= maximumValue) + { + c = 0; + m = 0; + y = 0; + } + else + { + // One reciprocal normalizes C, M, and Y against their shared remaining range. + float reciprocal = 1F / (maximumValue - k); + c = (c - k) * reciprocal; + m = (m - k) * reciprocal; + y = (y - k) * reciprocal; + } + + // TIFF stores conventional CMYK: scale normalized C/M/Y back into the sample domain and retain K. + c0 = c * maximumValue; + c1 = m * maximumValue; + c2 = y * maximumValue; + c3 = k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + Vector128 c = maximumValue - r; + Vector128 m = maximumValue - g; + Vector128 y = maximumValue - b; + Vector128 k = Vector128.Min(c, Vector128.Min(m, y)); + + // The all-bits mask clears the undefined zero-divisor result only in pure-black lanes. + Vector128 nonBlack = ~Vector128.Equals(k, maximumValue); + Vector128 reciprocal = Vector128.One / (maximumValue - k); + c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue; + c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue; + c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue; + c3 = k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + Vector256 c = maximumValue - r; + Vector256 m = maximumValue - g; + Vector256 y = maximumValue - b; + Vector256 k = Vector256.Min(c, Vector256.Min(m, y)); + + // Eight lanes independently clear the pure-black singularity before returning conventional CMYK. + Vector256 nonBlack = ~Vector256.Equals(k, maximumValue); + Vector256 reciprocal = Vector256.One / (maximumValue - k); + c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue; + c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue; + c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue; + c3 = k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + Vector512 c = maximumValue - r; + Vector512 m = maximumValue - g; + Vector512 y = maximumValue - b; + Vector512 k = Vector512.Min(c, Vector512.Min(m, y)); + + // Sixteen lanes retain the same branchless singularity handling and component layout. + Vector512 nonBlack = ~Vector512.Equals(k, maximumValue); + Vector512 reciprocal = Vector512.One / (maximumValue - k); + c0 = (((c - k) * reciprocal) & nonBlack) * maximumValue; + c1 = (((m - k) * reciprocal) & nonBlack) * maximumValue; + c2 = (((y - k) * reciprocal) & nonBlack) * maximumValue; + c3 = k; + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs deleted file mode 100644 index 27449a368..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - /// - /// Color converter for tiff images, which use the jpeg compression and CMYK colorspace. - /// - internal sealed class TiffCmykScalar : JpegColorConverterScalar - { - public TiffCmykScalar(int precision) - : base(JpegColorSpace.TiffCmyk, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - => ConvertToRgbInPlace(in values, this.MaximumValue); - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); - - public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue) - { - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - Span c3 = values.Component3; - - float scale = 1 / maxValue; - for (int i = 0; i < c0.Length; i++) - { - float c = c0[i] * scale; - float m = c1[i] * scale; - float y = c2[i] * scale; - float k = 1 - (c3[i] * scale); - - c0[i] = (1 - c) * k; - c1[i] = (1 - m) * k; - c2[i] = (1 - y) * k; - } - } - - public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) - { - Span c = values.Component0; - Span m = values.Component1; - Span y = values.Component2; - Span k = values.Component3; - - for (int i = 0; i < c.Length; i++) - { - float ctmp = 255F - rLane[i]; - float mtmp = 255F - gLane[i]; - float ytmp = 255F - bLane[i]; - float ktmp = MathF.Min(MathF.Min(ctmp, mtmp), ytmp); - - if (ktmp >= 255F) - { - ctmp = 0F; - mtmp = 0F; - ytmp = 0F; - } - else - { - float divisor = 1 / (255F - ktmp); - ctmp = (ctmp - ktmp) * divisor; - mtmp = (mtmp - ktmp) * divisor; - ytmp = (ytmp - ktmp) * divisor; - } - - c[i] = ctmp * maxValue; - m[i] = mtmp * maxValue; - y[i] = ytmp * maxValue; - k[i] = ktmp; - } - } - - public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) - { - using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); - Span packed = memoryOwner.Memory.Span; - - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - Span c3 = values.Component3; - - PackedNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); - - Span source = MemoryMarshal.Cast(packed); - Span destination = MemoryMarshal.Cast(packed)[..source.Length]; - - ColorConversionOptions options = new() - { - SourceIccProfile = profile, - TargetIccProfile = CompactSrgbV4Profile.Profile, - }; - ColorProfileConverter converter = new(options); - converter.Convert(source, destination); - - UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector128.cs deleted file mode 100644 index 6d52d5c72..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector128.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class TiffCmykVector128 : JpegColorConverterVector128 - { - public TiffCmykVector128(int precision) - : base(JpegColorSpace.TiffCmyk, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector128 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector128 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - Vector128 scale = Vector128.Create(1 / this.MaximumValue); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector128 c = ref Unsafe.Add(ref c0Base, i); - ref Vector128 m = ref Unsafe.Add(ref c1Base, i); - ref Vector128 y = ref Unsafe.Add(ref c2Base, i); - Vector128 k = Unsafe.Add(ref c3Base, i); - - k = Vector128.One - (k * scale); - c = (Vector128.One - (c * scale)) * k; - m = (Vector128.One - (m * scale)) * k; - y = (Vector128.One - (y * scale)) * k; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => TiffCmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); - - public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) - { - ref Vector128 destC = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 destM = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector128 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - ref Vector128 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector128 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector128 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector128 scale = Vector128.Create(maxValue); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - Vector128 ctmp = scale - Unsafe.Add(ref srcR, i); - Vector128 mtmp = scale - Unsafe.Add(ref srcG, i); - Vector128 ytmp = scale - Unsafe.Add(ref srcB, i); - Vector128 ktmp = Vector128.Min(ctmp, Vector128.Min(mtmp, ytmp)); - - Vector128 kMask = ~Vector128.Equals(ktmp, scale); - Vector128 divisor = Vector128.One / (scale - ktmp); - - ctmp = ((ctmp - ktmp) * divisor) & kMask; - mtmp = ((mtmp - ktmp) * divisor) & kMask; - ytmp = ((ytmp - ktmp) * divisor) & kMask; - - Unsafe.Add(ref destC, i) = ctmp * scale; - Unsafe.Add(ref destM, i) = mtmp * scale; - Unsafe.Add(ref destY, i) = ytmp * scale; - Unsafe.Add(ref destK, i) = ktmp; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector256.cs deleted file mode 100644 index 61b312a06..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector256.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class TiffCmykVector256 : JpegColorConverterVector256 - { - public TiffCmykVector256(int precision) - : base(JpegColorSpace.TiffCmyk, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector256 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector256 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - Vector256 scale = Vector256.Create(1 / this.MaximumValue); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector256 c = ref Unsafe.Add(ref c0Base, i); - ref Vector256 m = ref Unsafe.Add(ref c1Base, i); - ref Vector256 y = ref Unsafe.Add(ref c2Base, i); - Vector256 k = Unsafe.Add(ref c3Base, i); - - k = Vector256.One - (k * scale); - c = (Vector256.One - (c * scale)) * k; - m = (Vector256.One - (m * scale)) * k; - y = (Vector256.One - (y * scale)) * k; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => CmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); - - public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) - { - ref Vector256 destC = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 destM = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector256 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - ref Vector256 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector256 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector256 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector256 scale = Vector256.Create(maxValue); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - Vector256 ctmp = scale - Unsafe.Add(ref srcR, i); - Vector256 mtmp = scale - Unsafe.Add(ref srcG, i); - Vector256 ytmp = scale - Unsafe.Add(ref srcB, i); - Vector256 ktmp = Vector256.Min(ctmp, Vector256.Min(mtmp, ytmp)); - - Vector256 kMask = ~Vector256.Equals(ktmp, scale); - Vector256 divisor = Vector256.One / (scale - ktmp); - - ctmp = ((ctmp - ktmp) * divisor) & kMask; - mtmp = ((mtmp - ktmp) * divisor) & kMask; - ytmp = ((ytmp - ktmp) * divisor) & kMask; - - Unsafe.Add(ref destC, i) = ctmp * scale; - Unsafe.Add(ref destM, i) = mtmp * scale; - Unsafe.Add(ref destY, i) = ytmp * scale; - Unsafe.Add(ref destK, i) = ktmp; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector512.cs deleted file mode 100644 index 51d5cc76d..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector512.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class TiffCmykVector512 : JpegColorConverterVector512 - { - public TiffCmykVector512(int precision) - : base(JpegColorSpace.TiffCmyk, precision) - { - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => TiffCmykScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) - { - ref Vector512 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector512 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - // Used for the color conversion - Vector512 scale = Vector512.Create(1 / this.MaximumValue); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector512 c = ref Unsafe.Add(ref c0Base, i); - ref Vector512 m = ref Unsafe.Add(ref c1Base, i); - ref Vector512 y = ref Unsafe.Add(ref c2Base, i); - Vector512 k = Unsafe.Add(ref c3Base, i); - - k = Vector512.One - (k * scale); - c = (Vector512.One - (c * scale)) * k; - m = (Vector512.One - (m * scale)) * k; - y = (Vector512.One - (y * scale)) * k; - } - } - - /// - protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgbVectorized(in values, this.MaximumValue, rLane, gLane, bLane); - - /// - protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => TiffCmykScalar.ConvertToRgbInPlace(values, this.MaximumValue); - - /// - protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => TiffCmykScalar.ConvertFromRgb(values, this.MaximumValue, rLane, gLane, bLane); - - internal static void ConvertFromRgbVectorized(in ComponentValues values, float maxValue, Span rLane, Span gLane, Span bLane) - { - ref Vector512 destC = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 destM = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector512 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - ref Vector512 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector512 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector512 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector512 scale = Vector512.Create(maxValue); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - Vector512 ctmp = scale - Unsafe.Add(ref srcR, i); - Vector512 mtmp = scale - Unsafe.Add(ref srcG, i); - Vector512 ytmp = scale - Unsafe.Add(ref srcB, i); - Vector512 ktmp = Vector512.Min(ctmp, Vector512.Min(mtmp, ytmp)); - - Vector512 kMask = ~Vector512.Equals(ktmp, scale); - Vector512 divisor = Vector512.One / (scale - ktmp); - - ctmp = ((ctmp - ktmp) * divisor) & kMask; - mtmp = ((mtmp - ktmp) * divisor) & kMask; - ytmp = ((ytmp - ktmp) * divisor) & kMask; - - Unsafe.Add(ref destC, i) = ctmp * scale; - Unsafe.Add(ref destM, i) = mtmp * scale; - Unsafe.Add(ref destY, i) = ytmp * scale; - Unsafe.Add(ref destK, i) = ktmp; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs new file mode 100644 index 000000000..90cc84048 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKOperator.cs @@ -0,0 +1,182 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements non-inverted TIFF JPEG YccK conversion for scalar and SIMD lanes. + /// + internal readonly struct TiffYccKOperator : IJpegColorConverterOperator + { + private const float SourceScale = 1F / 255F; + + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.TiffYccK; + + /// + public static int ComponentCount => 4; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + float y = c0 * scale; + float cb = (c1 - halfValue) * scale; + float cr = (c2 - halfValue) * scale; + float k = 1F - (c3 * scale); + + // TIFF YccK is non-inverted: decode normalized YCbCr without integer rounding, then let the + // remaining light after K modulate all three channels. + c0 = (y + (YCbCrOperator.RCrMult * cr)) * k; + c1 = (y - (YCbCrOperator.GCbMult * cb) - (YCbCrOperator.GCrMult * cr)) * k; + c2 = (y + (YCbCrOperator.BCbMult * cb)) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + Vector128 y = c0 * scale; + Vector128 cb = (c1 - halfValue) * scale; + Vector128 cr = (c2 - halfValue) * scale; + Vector128 k = Vector128.One - (c3 * scale); + + // Four lanes apply the non-rounded YCbCr matrix before their lane-aligned K modulation. + c0 = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(YCbCrOperator.RCrMult), y) * k; + c1 = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(-YCbCrOperator.GCrMult), Vector128_.MultiplyAddEstimate(cb, Vector128.Create(-YCbCrOperator.GCbMult), y)) * k; + c2 = Vector128_.MultiplyAddEstimate(cb, Vector128.Create(YCbCrOperator.BCbMult), y) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + Vector256 y = c0 * scale; + Vector256 cb = (c1 - halfValue) * scale; + Vector256 cr = (c2 - halfValue) * scale; + Vector256 k = Vector256.One - (c3 * scale); + + // Eight lanes apply the non-rounded YCbCr matrix before their lane-aligned K modulation. + c0 = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(YCbCrOperator.RCrMult), y) * k; + c1 = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(-YCbCrOperator.GCrMult), Vector256_.MultiplyAddEstimate(cb, Vector256.Create(-YCbCrOperator.GCbMult), y)) * k; + c2 = Vector256_.MultiplyAddEstimate(cb, Vector256.Create(YCbCrOperator.BCbMult), y) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + Vector512 y = c0 * scale; + Vector512 cb = (c1 - halfValue) * scale; + Vector512 cr = (c2 - halfValue) * scale; + Vector512 k = Vector512.One - (c3 * scale); + + // Sixteen lanes apply the non-rounded YCbCr matrix before their lane-aligned K modulation. + c0 = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(YCbCrOperator.RCrMult), y) * k; + c1 = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(-YCbCrOperator.GCrMult), Vector512_.MultiplyAddEstimate(cb, Vector512.Create(-YCbCrOperator.GCbMult), y)) * k; + c2 = Vector512_.MultiplyAddEstimate(cb, Vector512.Create(YCbCrOperator.BCbMult), y) * k; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + r *= SourceScale; + g *= SourceScale; + b *= SourceScale; + float k = 1F - MathF.Max(r, MathF.Max(g, b)); + + // Dividing by the brightest channel removes K before YCbCr projection. Pure black has no + // chromatic direction, so it maps to zero luma and the neutral chroma midpoint. + if (k >= 1F) + { + c0 = 0; + c1 = halfValue; + c2 = halfValue; + c3 = maximumValue; + return; + } + + float divisor = 1F / (1F - k); + r *= divisor; + g *= divisor; + b *= divisor; + c0 = ((0.299F * r) + (0.587F * g) + (0.114F * b)) * maximumValue; + c1 = halfValue + (((-0.168736F * r) + (-0.331264F * g) + (0.5F * b)) * maximumValue); + c2 = halfValue + (((0.5F * r) + (-0.418688F * g) + (-0.081312F * b)) * maximumValue); + c3 = k * maximumValue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + Vector128 sourceScale = Vector128.Create(SourceScale); + r *= sourceScale; + g *= sourceScale; + b *= sourceScale; + Vector128 k = Vector128.One - Vector128.Max(r, Vector128.Max(g, b)); + + // The mask assigns no chromatic direction to pure-black lanes while preserving neighboring pixels. + Vector128 nonBlack = ~Vector128.Equals(k, Vector128.One); + Vector128 divisor = Vector128.One / (Vector128.One - k); + r = (r * divisor) & nonBlack; + g = (g * divisor) & nonBlack; + b = (b * divisor) & nonBlack; + c0 = Vector128_.MultiplyAddEstimate(Vector128.Create(0.299F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(0.587F), g, Vector128.Create(0.114F) * b)) * maximumValue; + c1 = halfValue + (Vector128_.MultiplyAddEstimate(Vector128.Create(-0.168736F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(-0.331264F), g, Vector128.Create(0.5F) * b)) * maximumValue); + c2 = halfValue + (Vector128_.MultiplyAddEstimate(Vector128.Create(0.5F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(-0.418688F), g, Vector128.Create(-0.081312F) * b)) * maximumValue); + c3 = k * maximumValue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + Vector256 sourceScale = Vector256.Create(SourceScale); + r *= sourceScale; + g *= sourceScale; + b *= sourceScale; + Vector256 k = Vector256.One - Vector256.Max(r, Vector256.Max(g, b)); + + // Eight lanes normalize chromatic direction independently and retain neutral chroma for black. + Vector256 nonBlack = ~Vector256.Equals(k, Vector256.One); + Vector256 divisor = Vector256.One / (Vector256.One - k); + r = (r * divisor) & nonBlack; + g = (g * divisor) & nonBlack; + b = (b * divisor) & nonBlack; + c0 = Vector256_.MultiplyAddEstimate(Vector256.Create(0.299F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(0.587F), g, Vector256.Create(0.114F) * b)) * maximumValue; + c1 = halfValue + (Vector256_.MultiplyAddEstimate(Vector256.Create(-0.168736F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(-0.331264F), g, Vector256.Create(0.5F) * b)) * maximumValue); + c2 = halfValue + (Vector256_.MultiplyAddEstimate(Vector256.Create(0.5F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(-0.418688F), g, Vector256.Create(-0.081312F) * b)) * maximumValue); + c3 = k * maximumValue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + Vector512 sourceScale = Vector512.Create(SourceScale); + r *= sourceScale; + g *= sourceScale; + b *= sourceScale; + Vector512 k = Vector512.One - Vector512.Max(r, Vector512.Max(g, b)); + + // Sixteen lanes normalize chromatic direction independently and retain neutral chroma for black. + Vector512 nonBlack = ~Vector512.Equals(k, Vector512.One); + Vector512 divisor = Vector512.One / (Vector512.One - k); + r = (r * divisor) & nonBlack; + g = (g * divisor) & nonBlack; + b = (b * divisor) & nonBlack; + c0 = Vector512_.MultiplyAddEstimate(Vector512.Create(0.299F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(0.587F), g, Vector512.Create(0.114F) * b)) * maximumValue; + c1 = halfValue + (Vector512_.MultiplyAddEstimate(Vector512.Create(-0.168736F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(-0.331264F), g, Vector512.Create(0.5F) * b)) * maximumValue); + c2 = halfValue + (Vector512_.MultiplyAddEstimate(Vector512.Create(0.5F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(-0.418688F), g, Vector512.Create(-0.081312F) * b)) * maximumValue); + c3 = k * maximumValue; + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs deleted file mode 100644 index 01bfc0875..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - /// - /// Color converter for tiff images, which use the jpeg compression and CMYK colorspace. - /// - internal sealed class TiffYccKScalar : JpegColorConverterScalar - { - // Derived from ITU-T Rec. T.871 - internal const float RCrMult = 1.402f; - internal const float GCbMult = (float)(0.114 * 1.772 / 0.587); - internal const float GCrMult = (float)(0.299 * 1.402 / 0.587); - internal const float BCbMult = 1.772f; - - public TiffYccKScalar(int precision) - : base(JpegColorSpace.TiffYccK, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - => ConvertToRgbInPlace(in values, this.MaximumValue, this.HalfValue); - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); - - public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue, float halfValue) - { - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - Span c3 = values.Component3; - - float scale = 1F / maxValue; - halfValue *= scale; - - for (int i = 0; i < values.Component0.Length; i++) - { - float y = c0[i] * scale; - float cb = (c1[i] * scale) - halfValue; - float cr = (c2[i] * scale) - halfValue; - float scaledK = 1 - (c3[i] * scale); - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - c0[i] = (y + (RCrMult * cr)) * scaledK; - c1[i] = (y - (GCbMult * cb) - (GCrMult * cr)) * scaledK; - c2[i] = (y + (BCbMult * cb)) * scaledK; - } - } - - public static void ConvertFromRgb(in ComponentValues values, float halfValue, float maxValue, Span rLane, Span gLane, Span bLane) - { - Span y = values.Component0; - Span cb = values.Component1; - Span cr = values.Component2; - Span k = values.Component3; - - for (int i = 0; i < cr.Length; i++) - { - // Scale down to [0-1] - const float divisor = 1F / 255F; - float r = rLane[i] * divisor; - float g = gLane[i] * divisor; - float b = bLane[i] * divisor; - - float ytmp; - float cbtmp; - float crtmp; - float ktmp = 1F - MathF.Max(r, MathF.Max(g, b)); - - if (ktmp >= 1F) - { - ytmp = 0F; - cbtmp = 0.5F; - crtmp = 0.5F; - ktmp = maxValue; - } - else - { - float kmask = 1F / (1F - ktmp); - r *= kmask; - g *= kmask; - b *= kmask; - - // Scale to [0-maxValue] - ytmp = ((0.299f * r) + (0.587f * g) + (0.114f * b)) * maxValue; - cbtmp = halfValue - (((0.168736f * r) - (0.331264f * g) + (0.5f * b)) * maxValue); - crtmp = halfValue + (((0.5f * r) - (0.418688f * g) - (0.081312f * b)) * maxValue); - ktmp *= maxValue; - } - - y[i] = ytmp; - cb[i] = cbtmp; - cr[i] = crtmp; - k[i] = ktmp; - } - } - - public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) - { - using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); - Span packed = memoryOwner.Memory.Span; - - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - Span c3 = values.Component3; - - PackedNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); - - ColorProfileConverter converter = new(); - Span source = MemoryMarshal.Cast(packed); - - // YccK is not a defined ICC color space � it's a JPEG-specific encoding used in Adobe-style CMYK JPEGs. - // ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform. - // This transform assumes Rec.601 YCbCr coefficients and an inverted K channel. - // - // The YccK => Cmyk conversion is independent of any embedded ICC profile. - // Since the same RGB working space is used during conversion to and from XYZ, - // colorimetric accuracy is preserved. - converter.Convert(MemoryMarshal.Cast(source), source); - - Span destination = MemoryMarshal.Cast(packed)[..source.Length]; - - ColorConversionOptions options = new() - { - SourceIccProfile = profile, - TargetIccProfile = CompactSrgbV4Profile.Profile, - }; - converter = new ColorProfileConverter(options); - converter.Convert(source, destination); - - UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector128.cs deleted file mode 100644 index b360c373a..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector128.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class TiffYccKVector128 : JpegColorConverterVector128 - { - public TiffYccKVector128(int precision) - : base(JpegColorSpace.TiffYccK, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector128 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector128 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - Vector128 scale = Vector128.Create(1F / this.MaximumValue); - Vector128 chromaOffset = Vector128.Create(this.HalfValue) * scale; - Vector128 rCrMult = Vector128.Create(YCbCrScalar.RCrMult); - Vector128 gCbMult = Vector128.Create(-YCbCrScalar.GCbMult); - Vector128 gCrMult = Vector128.Create(-YCbCrScalar.GCrMult); - Vector128 bCbMult = Vector128.Create(YCbCrScalar.BCbMult); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector128 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector128 c2 = ref Unsafe.Add(ref c2Base, i); - ref Vector128 c3 = ref Unsafe.Add(ref c3Base, i); - - Vector128 y = c0 * scale; - Vector128 cb = (c1 * scale) - chromaOffset; - Vector128 cr = (c2 * scale) - chromaOffset; - Vector128 scaledK = Vector128.One - (c3 * scale); - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector128 r = Vector128_.MultiplyAdd(y, cr, rCrMult) * scaledK; - Vector128 g = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult) * scaledK; - Vector128 b = Vector128_.MultiplyAdd(y, cb, bCbMult) * scaledK; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => TiffYccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - ref Vector128 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector128 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector128 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - ref Vector128 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector128 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - Vector128 maxSourceValue = Vector128.Create(1 / 255F); - Vector128 maxSampleValue = Vector128.Create(this.MaximumValue); - Vector128 chromaOffset = Vector128.Create(this.HalfValue); - - Vector128 f0299 = Vector128.Create(0.299f); - Vector128 f0587 = Vector128.Create(0.587f); - Vector128 f0114 = Vector128.Create(0.114f); - Vector128 fn0168736 = Vector128.Create(-0.168736f); - Vector128 fn0331264 = Vector128.Create(-0.331264f); - Vector128 fn0418688 = Vector128.Create(-0.418688f); - Vector128 fn0081312F = Vector128.Create(-0.081312F); - Vector128 f05 = Vector128.Create(0.5f); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - Vector128 r = Unsafe.Add(ref srcR, i) * maxSourceValue; - Vector128 g = Unsafe.Add(ref srcG, i) * maxSourceValue; - Vector128 b = Unsafe.Add(ref srcB, i) * maxSourceValue; - Vector128 ktmp = Vector128.One - Vector128.Max(r, Vector128.Min(g, b)); - - Vector128 kMask = ~Vector128.Equals(ktmp, Vector128.One); - Vector128 divisor = Vector128.One / (Vector128.One - ktmp); - - r = (r * divisor) & kMask; - g = (g * divisor) & kMask; - b = (b * divisor) & kMask; - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector128 y = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector128 cb = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector128 cr = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y * maxSampleValue; - Unsafe.Add(ref destCb, i) = chromaOffset + (cb * maxSampleValue); - Unsafe.Add(ref destCr, i) = chromaOffset + (cr * maxSampleValue); - Unsafe.Add(ref destK, i) = ktmp * maxSampleValue; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector256.cs deleted file mode 100644 index f996522d3..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector256.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class TiffYccKVector256 : JpegColorConverterVector256 - { - public TiffYccKVector256(int precision) - : base(JpegColorSpace.TiffYccK, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector256 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector256 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - Vector256 scale = Vector256.Create(1F / this.MaximumValue); - Vector256 chromaOffset = Vector256.Create(this.HalfValue) * scale; - Vector256 rCrMult = Vector256.Create(YCbCrScalar.RCrMult); - Vector256 gCbMult = Vector256.Create(-YCbCrScalar.GCbMult); - Vector256 gCrMult = Vector256.Create(-YCbCrScalar.GCrMult); - Vector256 bCbMult = Vector256.Create(YCbCrScalar.BCbMult); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector256 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector256 c2 = ref Unsafe.Add(ref c2Base, i); - ref Vector256 c3 = ref Unsafe.Add(ref c3Base, i); - - Vector256 y = c0 * scale; - Vector256 cb = (c1 * scale) - chromaOffset; - Vector256 cr = (c2 * scale) - chromaOffset; - Vector256 scaledK = Vector256.One - (c3 * scale); - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector256 r = Vector256_.MultiplyAdd(y, cr, rCrMult) * scaledK; - Vector256 g = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult) * scaledK; - Vector256 b = Vector256_.MultiplyAdd(y, cb, bCbMult) * scaledK; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => TiffYccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - ref Vector256 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector256 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector256 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - ref Vector256 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector256 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - Vector256 maxSourceValue = Vector256.Create(255F); - Vector256 maxSampleValue = Vector256.Create(this.MaximumValue); - Vector256 chromaOffset = Vector256.Create(this.HalfValue); - - Vector256 f0299 = Vector256.Create(0.299f); - Vector256 f0587 = Vector256.Create(0.587f); - Vector256 f0114 = Vector256.Create(0.114f); - Vector256 fn0168736 = Vector256.Create(-0.168736f); - Vector256 fn0331264 = Vector256.Create(-0.331264f); - Vector256 fn0418688 = Vector256.Create(-0.418688f); - Vector256 fn0081312F = Vector256.Create(-0.081312F); - Vector256 f05 = Vector256.Create(0.5f); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - Vector256 r = Unsafe.Add(ref srcR, i) / maxSourceValue; - Vector256 g = Unsafe.Add(ref srcG, i) / maxSourceValue; - Vector256 b = Unsafe.Add(ref srcB, i) / maxSourceValue; - Vector256 ktmp = Vector256.One - Vector256.Max(r, Vector256.Min(g, b)); - - Vector256 kMask = ~Vector256.Equals(ktmp, Vector256.One); - Vector256 divisor = Vector256.One / (Vector256.One - ktmp); - - r = (r * divisor) & kMask; - g = (g * divisor) & kMask; - b = (b * divisor) & kMask; - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector256 y = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector256 cb = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector256 cr = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y * maxSampleValue; - Unsafe.Add(ref destCb, i) = chromaOffset + (cb * maxSampleValue); - Unsafe.Add(ref destCr, i) = chromaOffset + (cr * maxSampleValue); - Unsafe.Add(ref destK, i) = ktmp * maxSampleValue; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector512.cs deleted file mode 100644 index 47168a739..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector512.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class TiffYccKVector512 : JpegColorConverterVector512 - { - public TiffYccKVector512(int precision) - : base(JpegColorSpace.TiffYccK, precision) - { - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => TiffYccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) - { - ref Vector512 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector512 c3Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - Vector512 scale = Vector512.Create(1F / this.MaximumValue); - Vector512 chromaOffset = Vector512.Create(this.HalfValue) * scale; - Vector512 rCrMult = Vector512.Create(YCbCrScalar.RCrMult); - Vector512 gCbMult = Vector512.Create(-YCbCrScalar.GCbMult); - Vector512 gCrMult = Vector512.Create(-YCbCrScalar.GCrMult); - Vector512 bCbMult = Vector512.Create(YCbCrScalar.BCbMult); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector512 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector512 c2 = ref Unsafe.Add(ref c2Base, i); - ref Vector512 c3 = ref Unsafe.Add(ref c3Base, i); - - Vector512 y = c0 * scale; - Vector512 cb = (c1 * scale) - chromaOffset; - Vector512 cr = (c2 * scale) - chromaOffset; - Vector512 scaledK = Vector512.One - (c3 * scale); - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector512 r = Vector512_.MultiplyAdd(y, cr, rCrMult) * scaledK; - Vector512 g = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult) * scaledK; - Vector512 b = Vector512_.MultiplyAdd(y, cb, bCbMult) * scaledK; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgbVectorized(in values, this.MaximumValue, this.HalfValue, rLane, gLane, bLane); - - /// - protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => TiffYccKScalar.ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); - - /// - protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => TiffYccKScalar.ConvertFromRgb(values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); - - internal static void ConvertFromRgbVectorized(in ComponentValues values, float maxValue, float halfValue, Span rLane, Span gLane, Span bLane) - { - ref Vector512 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector512 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector512 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - ref Vector512 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector512 destK = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - Vector512 maxSourceValue = Vector512.Create(255F); - Vector512 maxSampleValue = Vector512.Create(maxValue); - Vector512 chromaOffset = Vector512.Create(halfValue); - - Vector512 f0299 = Vector512.Create(0.299f); - Vector512 f0587 = Vector512.Create(0.587f); - Vector512 f0114 = Vector512.Create(0.114f); - Vector512 fn0168736 = Vector512.Create(-0.168736f); - Vector512 fn0331264 = Vector512.Create(-0.331264f); - Vector512 fn0418688 = Vector512.Create(-0.418688f); - Vector512 fn0081312F = Vector512.Create(-0.081312F); - Vector512 f05 = Vector512.Create(0.5f); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - Vector512 r = Unsafe.Add(ref srcR, i) / maxSourceValue; - Vector512 g = Unsafe.Add(ref srcG, i) / maxSourceValue; - Vector512 b = Unsafe.Add(ref srcB, i) / maxSourceValue; - Vector512 ktmp = Vector512.One - Vector512.Max(r, Vector512.Min(g, b)); - - Vector512 kMask = ~Vector512.Equals(ktmp, Vector512.One); - Vector512 divisor = Vector512.One / (Vector512.One - ktmp); - - r = (r * divisor) & kMask; - g = (g * divisor) & kMask; - b = (b * divisor) & kMask; - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector512 y = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector512 cb = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector512 cr = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y * maxSampleValue; - Unsafe.Add(ref destCb, i) = chromaOffset + (cb * maxSampleValue); - Unsafe.Add(ref destCr, i) = chromaOffset + (cr * maxSampleValue); - Unsafe.Add(ref destK, i) = ktmp * maxSampleValue; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs new file mode 100644 index 000000000..e5bb6db03 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrOperator.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements the JPEG YCbCr conversion formula for scalar and SIMD lanes. + /// + internal readonly struct YCbCrOperator : IJpegColorConverterOperator + { + /// + /// The BT.601 red contribution from centered Cr. + /// + public const float RCrMult = 1.402F; + + /// + /// The BT.601 green contribution from centered Cb. + /// + public const float GCbMult = (float)(0.114 * 1.772 / 0.587); + + /// + /// The BT.601 green contribution from centered Cr. + /// + public const float GCrMult = (float)(0.299 * 1.402 / 0.587); + + /// + /// The BT.601 blue contribution from centered Cb. + /// + public const float BCbMult = 1.772F; + + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.YCbCr; + + /// + public static int ComponentCount => 3; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + float y = c0; + float cb = c1 - halfValue; + float cr = c2 - halfValue; + + // c0/c1/c2 initially mean Y/Cb/Cr. Chroma is centered around zero before applying + // the BT.601 matrix, then integer-domain RGB is rounded away from zero and normalized + // to [nominally] 0..1. Values intentionally remain unclamped because quantizing RGB into the + // destination pixel format owns saturation; retaining overshoot avoids discarding color information. + c0 = MathF.Round(y + (RCrMult * cr), MidpointRounding.AwayFromZero) * scale; + c1 = MathF.Round(y - (GCbMult * cb) - (GCrMult * cr), MidpointRounding.AwayFromZero) * scale; + c2 = MathF.Round(y + (BCbMult * cb), MidpointRounding.AwayFromZero) * scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + Vector128 y = c0; + Vector128 cb = c1 - halfValue; + Vector128 cr = c2 - halfValue; + + // Lanes are four independent Y/Cb/Cr samples. MultiplyAddEstimate maps to FMA where available: + // R uses Cr, B uses Cb, and G subtracts both chroma contributions. Rounding occurs in the sample + // domain before the common normalization scale so all precisions use integer JPEG sample semantics. + Vector128 r = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(RCrMult), y); + Vector128 g = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(-GCrMult), Vector128_.MultiplyAddEstimate(cb, Vector128.Create(-GCbMult), y)); + Vector128 b = Vector128_.MultiplyAddEstimate(cb, Vector128.Create(BCbMult), y); + + c0 = Vector128_.RoundToNearestInteger(r) * scale; + c1 = Vector128_.RoundToNearestInteger(g) * scale; + c2 = Vector128_.RoundToNearestInteger(b) * scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + Vector256 y = c0; + Vector256 cb = c1 - halfValue; + Vector256 cr = c2 - halfValue; + + // These eight lanes have the same layout and BT.601 arithmetic as the Vector128 overload. + // Keeping an explicit overload allows the JIT to emit native YMM operations without a width + // switch or decomposing the vector into smaller values. + Vector256 r = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(RCrMult), y); + Vector256 g = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(-GCrMult), Vector256_.MultiplyAddEstimate(cb, Vector256.Create(-GCbMult), y)); + Vector256 b = Vector256_.MultiplyAddEstimate(cb, Vector256.Create(BCbMult), y); + + c0 = Vector256_.RoundToNearestInteger(r) * scale; + c1 = Vector256_.RoundToNearestInteger(g) * scale; + c2 = Vector256_.RoundToNearestInteger(b) * scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + Vector512 y = c0; + Vector512 cb = c1 - halfValue; + Vector512 cr = c2 - halfValue; + + // Sixteen independent samples occupy the ZMM lanes. The explicit constants are broadcasts; + // assembly inspection verifies the JIT hoists them from the loop and retains fused operations. + // The formula and rounding order remain identical to the narrower overloads. + Vector512 r = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(RCrMult), y); + Vector512 g = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(-GCrMult), Vector512_.MultiplyAddEstimate(cb, Vector512.Create(-GCbMult), y)); + Vector512 b = Vector512_.MultiplyAddEstimate(cb, Vector512.Create(BCbMult), y); + + c0 = Vector512_.RoundToNearestInteger(r) * scale; + c1 = Vector512_.RoundToNearestInteger(g) * scale; + c2 = Vector512_.RoundToNearestInteger(b) * scale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + // The RGB inputs are unnormalized 0..255 encoder lanes. The BT.601 luma weights form Y, + // while the signed chroma projections are biased by halfValue into the JPEG sample domain. + // YCbCr has no fourth component, so c3 is a compile-time-unused placeholder for the shared loop. + c0 = (0.299F * r) + (0.587F * g) + (0.114F * b); + c1 = halfValue - (0.168736F * r) - (0.331264F * g) + (0.5F * b); + c2 = halfValue + (0.5F * r) - (0.418688F * g) - (0.081312F * b); + c3 = 0; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + // Each vector holds four consecutive values from one RGB plane. The nested multiply-add sequence + // produces four Y lanes, four Cb lanes, and four Cr lanes without transposition. The association + // exposes two FMA opportunities per output while preserving the scalar formula's term grouping. + c0 = Vector128_.MultiplyAddEstimate(Vector128.Create(0.299F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(0.587F), g, Vector128.Create(0.114F) * b)); + c1 = halfValue + Vector128_.MultiplyAddEstimate(Vector128.Create(-0.168736F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(-0.331264F), g, Vector128.Create(0.5F) * b)); + c2 = halfValue + Vector128_.MultiplyAddEstimate(Vector128.Create(0.5F), r, Vector128_.MultiplyAddEstimate(Vector128.Create(-0.418688F), g, Vector128.Create(-0.081312F) * b)); + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + // Eight planar RGB samples use the identical association as Vector128, allowing direct YMM FMA + // generation while preserving the component-per-vector output layout. + c0 = Vector256_.MultiplyAddEstimate(Vector256.Create(0.299F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(0.587F), g, Vector256.Create(0.114F) * b)); + c1 = halfValue + Vector256_.MultiplyAddEstimate(Vector256.Create(-0.168736F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(-0.331264F), g, Vector256.Create(0.5F) * b)); + c2 = halfValue + Vector256_.MultiplyAddEstimate(Vector256.Create(0.5F), r, Vector256_.MultiplyAddEstimate(Vector256.Create(-0.418688F), g, Vector256.Create(-0.081312F) * b)); + c3 = default; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + // Sixteen planar RGB samples use the same nested form. Constants are lane broadcasts and c3 is + // deliberately zero because the shared traversal removes the unused fourth store for this operator. + c0 = Vector512_.MultiplyAddEstimate(Vector512.Create(0.299F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(0.587F), g, Vector512.Create(0.114F) * b)); + c1 = halfValue + Vector512_.MultiplyAddEstimate(Vector512.Create(-0.168736F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(-0.331264F), g, Vector512.Create(0.5F) * b)); + c2 = halfValue + Vector512_.MultiplyAddEstimate(Vector512.Create(0.5F), r, Vector512_.MultiplyAddEstimate(Vector512.Create(-0.418688F), g, Vector512.Create(-0.081312F) * b)); + c3 = default; + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs deleted file mode 100644 index 2b1c1dca3..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class YCbCrScalar : JpegColorConverterScalar - { - // derived from ITU-T Rec. T.871 - internal const float RCrMult = 1.402f; - internal const float GCbMult = (float)(0.114 * 1.772 / 0.587); - internal const float GCrMult = (float)(0.299 * 1.402 / 0.587); - internal const float BCbMult = 1.772f; - - public YCbCrScalar(int precision) - : base(JpegColorSpace.YCbCr, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - => ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(values, this.HalfValue, rLane, gLane, bLane); - - public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue, float halfValue) - { - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - - float scale = 1 / maxValue; - - for (int i = 0; i < c0.Length; i++) - { - float y = c0[i]; - float cb = c1[i] - halfValue; - float cr = c2[i] - halfValue; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - c0[i] = MathF.Round(y + (RCrMult * cr), MidpointRounding.AwayFromZero) * scale; - c1[i] = MathF.Round(y - (GCbMult * cb) - (GCrMult * cr), MidpointRounding.AwayFromZero) * scale; - c2[i] = MathF.Round(y + (BCbMult * cb), MidpointRounding.AwayFromZero) * scale; - } - } - - public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) - { - using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 3); - Span packed = memoryOwner.Memory.Span; - - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - - // Although YCbCr is a defined ICC color space, in practice ICC profiles - // do not implement transforms from it. - // Therefore, we first convert JPEG YCbCr to RGB manually, then perform - // color-managed conversion to the target profile. - // - // The YCbCr => RGB conversion is based on BT.601 and is independent of any embedded ICC profile. - // Since the same RGB working space is used during conversion to and from XYZ, - // colorimetric accuracy is preserved. - ColorProfileConverter converter = new(); - - PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / maxValue); - - Span source = MemoryMarshal.Cast(packed); - Span destination = MemoryMarshal.Cast(packed); - - converter.Convert(source, destination); - - ColorConversionOptions options = new() - { - SourceIccProfile = profile, - TargetIccProfile = CompactSrgbV4Profile.Profile, - }; - converter = new ColorProfileConverter(options); - converter.Convert(destination, destination); - - UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); - } - - public static void ConvertFromRgb(in ComponentValues values, float halfValue, Span rLane, Span gLane, Span bLane) - { - Span y = values.Component0; - Span cb = values.Component1; - Span cr = values.Component2; - - for (int i = 0; i < y.Length; i++) - { - float r = rLane[i]; - float g = gLane[i]; - float b = bLane[i]; - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - y[i] = (0.299f * r) + (0.587f * g) + (0.114f * b); - cb[i] = halfValue - (0.168736f * r) - (0.331264f * g) + (0.5f * b); - cr[i] = halfValue + (0.5f * r) - (0.418688f * g) - (0.081312f * b); - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs deleted file mode 100644 index 01c8508ed..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class YCbCrVector128 : JpegColorConverterVector128 - { - public YCbCrVector128(int precision) - : base(JpegColorSpace.YCbCr, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector128 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - Vector128 chromaOffset = Vector128.Create(-this.HalfValue); - Vector128 scale = Vector128.Create(1 / this.MaximumValue); - Vector128 rCrMult = Vector128.Create(YCbCrScalar.RCrMult); - Vector128 gCbMult = Vector128.Create(-YCbCrScalar.GCbMult); - Vector128 gCrMult = Vector128.Create(-YCbCrScalar.GCrMult); - Vector128 bCbMult = Vector128.Create(YCbCrScalar.BCbMult); - - // Walking 8 elements at one step: - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - // y = yVals[i]; - // cb = cbVals[i] - 128F; - // cr = crVals[i] - 128F; - ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector128 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector128 c2 = ref Unsafe.Add(ref c2Base, i); - - Vector128 y = c0; - Vector128 cb = c1 + chromaOffset; - Vector128 cr = c2 + chromaOffset; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector128 r = Vector128_.MultiplyAdd(y, cr, rCrMult); - Vector128 g = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); - Vector128 b = Vector128_.MultiplyAdd(y, cb, bCbMult); - - r = Vector128_.RoundToNearestInteger(r) * scale; - g = Vector128_.RoundToNearestInteger(g) * scale; - b = Vector128_.RoundToNearestInteger(b) * scale; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - ref Vector128 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - ref Vector128 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector128 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector128 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector128 chromaOffset = Vector128.Create(this.HalfValue); - Vector128 f0299 = Vector128.Create(0.299f); - Vector128 f0587 = Vector128.Create(0.587f); - Vector128 f0114 = Vector128.Create(0.114f); - Vector128 fn0168736 = Vector128.Create(-0.168736f); - Vector128 fn0331264 = Vector128.Create(-0.331264f); - Vector128 fn0418688 = Vector128.Create(-0.418688f); - Vector128 fn0081312F = Vector128.Create(-0.081312F); - Vector128 f05 = Vector128.Create(0.5f); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - Vector128 r = Unsafe.Add(ref srcR, i); - Vector128 g = Unsafe.Add(ref srcG, i); - Vector128 b = Unsafe.Add(ref srcB, i); - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector128 y = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector128 cb = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector128 cr = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y; - Unsafe.Add(ref destCb, i) = cb; - Unsafe.Add(ref destCr, i) = cr; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs deleted file mode 100644 index 8fdf1004d..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class YCbCrVector256 : JpegColorConverterVector256 - { - public YCbCrVector256(int precision) - : base(JpegColorSpace.YCbCr, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector256 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - Vector256 chromaOffset = Vector256.Create(-this.HalfValue); - Vector256 scale = Vector256.Create(1 / this.MaximumValue); - Vector256 rCrMult = Vector256.Create(YCbCrScalar.RCrMult); - Vector256 gCbMult = Vector256.Create(-YCbCrScalar.GCbMult); - Vector256 gCrMult = Vector256.Create(-YCbCrScalar.GCrMult); - Vector256 bCbMult = Vector256.Create(YCbCrScalar.BCbMult); - - // Walking 8 elements at one step: - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - // y = yVals[i]; - // cb = cbVals[i] - 128F; - // cr = crVals[i] - 128F; - ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector256 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector256 c2 = ref Unsafe.Add(ref c2Base, i); - - Vector256 y = c0; - Vector256 cb = c1 + chromaOffset; - Vector256 cr = c2 + chromaOffset; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector256 r = Vector256_.MultiplyAdd(y, cr, rCrMult); - Vector256 g = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); - Vector256 b = Vector256_.MultiplyAdd(y, cb, bCbMult); - - r = Vector256_.RoundToNearestInteger(r) * scale; - g = Vector256_.RoundToNearestInteger(g) * scale; - b = Vector256_.RoundToNearestInteger(b) * scale; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - ref Vector256 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - ref Vector256 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector256 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector256 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector256 chromaOffset = Vector256.Create(this.HalfValue); - Vector256 f0299 = Vector256.Create(0.299f); - Vector256 f0587 = Vector256.Create(0.587f); - Vector256 f0114 = Vector256.Create(0.114f); - Vector256 fn0168736 = Vector256.Create(-0.168736f); - Vector256 fn0331264 = Vector256.Create(-0.331264f); - Vector256 fn0418688 = Vector256.Create(-0.418688f); - Vector256 fn0081312F = Vector256.Create(-0.081312F); - Vector256 f05 = Vector256.Create(0.5f); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - Vector256 r = Unsafe.Add(ref srcR, i); - Vector256 g = Unsafe.Add(ref srcG, i); - Vector256 b = Unsafe.Add(ref srcB, i); - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector256 y = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector256 cb = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector256 cr = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y; - Unsafe.Add(ref destCb, i) = cb; - Unsafe.Add(ref destCr, i) = cr; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs deleted file mode 100644 index 33cb2496c..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class YCbCrVector512 : JpegColorConverterVector512 - { - public YCbCrVector512(int precision) - : base(JpegColorSpace.YCbCr, precision) - { - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => YCbCrScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) - { - ref Vector512 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - Vector512 chromaOffset = Vector512.Create(-this.HalfValue); - Vector512 scale = Vector512.Create(1 / this.MaximumValue); - Vector512 rCrMult = Vector512.Create(YCbCrScalar.RCrMult); - Vector512 gCbMult = Vector512.Create(-YCbCrScalar.GCbMult); - Vector512 gCrMult = Vector512.Create(-YCbCrScalar.GCrMult); - Vector512 bCbMult = Vector512.Create(YCbCrScalar.BCbMult); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - // y = yVals[i]; - // cb = cbVals[i] - 128F; - // cr = crVals[i] - 128F; - ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector512 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector512 c2 = ref Unsafe.Add(ref c2Base, i); - - Vector512 y = c0; - Vector512 cb = c1 + chromaOffset; - Vector512 cr = c2 + chromaOffset; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector512 r = Vector512_.MultiplyAdd(y, cr, rCrMult); - Vector512 g = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); - Vector512 b = Vector512_.MultiplyAdd(y, cb, bCbMult); - - r = Vector512_.RoundToNearestInteger(r) * scale; - g = Vector512_.RoundToNearestInteger(g) * scale; - b = Vector512_.RoundToNearestInteger(b) * scale; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - ref Vector512 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - ref Vector512 srcR = - ref Unsafe.As>(ref MemoryMarshal.GetReference(rLane)); - ref Vector512 srcG = - ref Unsafe.As>(ref MemoryMarshal.GetReference(gLane)); - ref Vector512 srcB = - ref Unsafe.As>(ref MemoryMarshal.GetReference(bLane)); - - Vector512 chromaOffset = Vector512.Create(this.HalfValue); - Vector512 f0299 = Vector512.Create(0.299f); - Vector512 f0587 = Vector512.Create(0.587f); - Vector512 f0114 = Vector512.Create(0.114f); - Vector512 fn0168736 = Vector512.Create(-0.168736f); - Vector512 fn0331264 = Vector512.Create(-0.331264f); - Vector512 fn0418688 = Vector512.Create(-0.418688f); - Vector512 fn0081312F = Vector512.Create(-0.081312F); - Vector512 f05 = Vector512.Create(0.5f); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - Vector512 r = Unsafe.Add(ref srcR, i); - Vector512 g = Unsafe.Add(ref srcG, i); - Vector512 b = Unsafe.Add(ref srcB, i); - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector512 y = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector512 cb = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector512 cr = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y; - Unsafe.Add(ref destCb, i) = cb; - Unsafe.Add(ref destCr, i) = cr; - } - } - - /// - protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => YCbCrScalar.ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); - - /// - protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => YCbCrScalar.ConvertFromRgb(values, this.HalfValue, rLane, gLane, bLane); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs new file mode 100644 index 000000000..90bee0c94 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKOperator.cs @@ -0,0 +1,134 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Implements inverted JPEG YccK conversion for scalar and SIMD lanes. + /// + internal readonly struct YccKOperator : IJpegColorConverterOperator + { + /// + public static JpegColorSpace ColorSpace => JpegColorSpace.Ycck; + + /// + public static int ComponentCount => 4; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float c0, ref float c1, ref float c2, float c3, float maximumValue, float halfValue, float scale) + { + float y = c0; + float cb = c1 - halfValue; + float cr = c2 - halfValue; + float scaledK = c3 * scale * scale; + + // YccK first reconstructs inverted RGB in the integer sample domain. Rounding must occur before + // subtracting from max and applying K because changing that order changes encoded JPEG semantics. + c0 = (maximumValue - MathF.Round(y + (YCbCrOperator.RCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK; + c1 = (maximumValue - MathF.Round(y - (YCbCrOperator.GCbMult * cb) - (YCbCrOperator.GCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK; + c2 = (maximumValue - MathF.Round(y + (YCbCrOperator.BCbMult * cb), MidpointRounding.AwayFromZero)) * scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 c0, ref Vector128 c1, ref Vector128 c2, Vector128 c3, Vector128 maximumValue, Vector128 halfValue, Vector128 scale) + { + Vector128 y = c0; + Vector128 cb = c1 - halfValue; + Vector128 cr = c2 - halfValue; + Vector128 scaledK = c3 * scale * scale; + + // Four lanes reconstruct YCbCr concurrently; each rounded result is inverted and modulated by its K lane. + Vector128 r = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(YCbCrOperator.RCrMult), y); + Vector128 g = Vector128_.MultiplyAddEstimate(cr, Vector128.Create(-YCbCrOperator.GCrMult), Vector128_.MultiplyAddEstimate(cb, Vector128.Create(-YCbCrOperator.GCbMult), y)); + Vector128 b = Vector128_.MultiplyAddEstimate(cb, Vector128.Create(YCbCrOperator.BCbMult), y); + c0 = (maximumValue - Vector128_.RoundToNearestInteger(r)) * scaledK; + c1 = (maximumValue - Vector128_.RoundToNearestInteger(g)) * scaledK; + c2 = (maximumValue - Vector128_.RoundToNearestInteger(b)) * scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 c0, ref Vector256 c1, ref Vector256 c2, Vector256 c3, Vector256 maximumValue, Vector256 halfValue, Vector256 scale) + { + Vector256 y = c0; + Vector256 cb = c1 - halfValue; + Vector256 cr = c2 - halfValue; + Vector256 scaledK = c3 * scale * scale; + + // Eight lanes retain planar alignment from Y/Cb/Cr/K through normalized RGB. + Vector256 r = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(YCbCrOperator.RCrMult), y); + Vector256 g = Vector256_.MultiplyAddEstimate(cr, Vector256.Create(-YCbCrOperator.GCrMult), Vector256_.MultiplyAddEstimate(cb, Vector256.Create(-YCbCrOperator.GCbMult), y)); + Vector256 b = Vector256_.MultiplyAddEstimate(cb, Vector256.Create(YCbCrOperator.BCbMult), y); + c0 = (maximumValue - Vector256_.RoundToNearestInteger(r)) * scaledK; + c1 = (maximumValue - Vector256_.RoundToNearestInteger(g)) * scaledK; + c2 = (maximumValue - Vector256_.RoundToNearestInteger(b)) * scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 c0, ref Vector512 c1, ref Vector512 c2, Vector512 c3, Vector512 maximumValue, Vector512 halfValue, Vector512 scale) + { + Vector512 y = c0; + Vector512 cb = c1 - halfValue; + Vector512 cr = c2 - halfValue; + Vector512 scaledK = c3 * scale * scale; + + // Sixteen lanes use the same matrix, rounding, inversion, and K modulation order as scalar code. + Vector512 r = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(YCbCrOperator.RCrMult), y); + Vector512 g = Vector512_.MultiplyAddEstimate(cr, Vector512.Create(-YCbCrOperator.GCrMult), Vector512_.MultiplyAddEstimate(cb, Vector512.Create(-YCbCrOperator.GCbMult), y)); + Vector512 b = Vector512_.MultiplyAddEstimate(cb, Vector512.Create(YCbCrOperator.BCbMult), y); + c0 = (maximumValue - Vector512_.RoundToNearestInteger(r)) * scaledK; + c1 = (maximumValue - Vector512_.RoundToNearestInteger(g)) * scaledK; + c2 = (maximumValue - Vector512_.RoundToNearestInteger(b)) * scaledK; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(float r, float g, float b, float maximumValue, float halfValue, float scale, out float c0, out float c1, out float c2, out float c3) + { + // CMYK extraction supplies inverted chromatic samples and K. Reflecting the first three results + // reconstructs the chromatic RGB that YCbCr encodes, while K passes through untouched. + CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out float c, out float m, out float y, out c3); + + YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector128 r, Vector128 g, Vector128 b, Vector128 maximumValue, Vector128 halfValue, Vector128 scale, out Vector128 c0, out Vector128 c1, out Vector128 c2, out Vector128 c3) + { + // Static constrained calls inline both stages, keeping four pixels in registers without materializing CMYK planes. + CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out Vector128 c, out Vector128 m, out Vector128 y, out c3); + + YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector256 r, Vector256 g, Vector256 b, Vector256 maximumValue, Vector256 halfValue, Vector256 scale, out Vector256 c0, out Vector256 c1, out Vector256 c2, out Vector256 c3) + { + // Eight pixels flow through CMYK extraction and YCbCr projection entirely in YMM registers. + CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out Vector256 c, out Vector256 m, out Vector256 y, out c3); + + YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb(Vector512 r, Vector512 g, Vector512 b, Vector512 maximumValue, Vector512 halfValue, Vector512 scale, out Vector512 c0, out Vector512 c1, out Vector512 c2, out Vector512 c3) + { + // Sixteen pixels flow through both mathematical stages in registers without materializing intermediate planes. + CmykOperator.ConvertFromRgb(r, g, b, maximumValue, halfValue, scale, out Vector512 c, out Vector512 m, out Vector512 y, out c3); + + YCbCrOperator.ConvertFromRgb(maximumValue - c, maximumValue - m, maximumValue - y, maximumValue, halfValue, scale, out c0, out c1, out c2, out _); + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs deleted file mode 100644 index 3368e52bf..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.ColorProfiles; -using SixLabors.ImageSharp.ColorProfiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class YccKScalar : JpegColorConverterScalar - { - // Derived from ITU-T Rec. T.871 - internal const float RCrMult = 1.402f; - internal const float GCbMult = (float)(0.114 * 1.772 / 0.587); - internal const float GCrMult = (float)(0.299 * 1.402 / 0.587); - internal const float BCbMult = 1.772f; - - public YccKScalar(int precision) - : base(JpegColorSpace.Ycck, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - => ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => ConvertFromRgb(values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); - - public static void ConvertToRgbInPlace(in ComponentValues values, float maxValue, float halfValue) - { - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - Span c3 = values.Component3; - - float scale = 1 / (maxValue * maxValue); - - for (int i = 0; i < values.Component0.Length; i++) - { - float y = c0[i]; - float cb = c1[i] - halfValue; - float cr = c2[i] - halfValue; - float scaledK = c3[i] * scale; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - c0[i] = (maxValue - MathF.Round(y + (RCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK; - c1[i] = (maxValue - MathF.Round(y - (GCbMult * cb) - (GCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK; - c2[i] = (maxValue - MathF.Round(y + (BCbMult * cb), MidpointRounding.AwayFromZero)) * scaledK; - } - } - - public static void ConvertFromRgb(in ComponentValues values, float halfValue, float maxValue, Span rLane, Span gLane, Span bLane) - { - // rgb -> cmyk - CmykScalar.ConvertFromRgb(in values, maxValue, rLane, gLane, bLane); - - // cmyk -> ycck - Span c = values.Component0; - Span m = values.Component1; - Span y = values.Component2; - - for (int i = 0; i < y.Length; i++) - { - float r = maxValue - c[i]; - float g = maxValue - m[i]; - float b = maxValue - y[i]; - - // k value is passed untouched from rgb -> cmyk conversion - c[i] = (0.299f * r) + (0.587f * g) + (0.114f * b); - m[i] = halfValue - (0.168736f * r) - (0.331264f * g) + (0.5f * b); - y[i] = halfValue + (0.5f * r) - (0.418688f * g) - (0.081312f * b); - } - } - - public static void ConvertToRgbInPlaceWithIcc(Configuration configuration, IccProfile profile, in ComponentValues values, float maxValue) - { - using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(values.Component0.Length * 4); - Span packed = memoryOwner.Memory.Span; - - Span c0 = values.Component0; - Span c1 = values.Component1; - Span c2 = values.Component2; - Span c3 = values.Component3; - - PackedInvertNormalizeInterleave4(c0, c1, c2, c3, packed, maxValue); - - ColorProfileConverter converter = new(); - Span source = MemoryMarshal.Cast(packed); - - // YccK is not a defined ICC color space — it's a JPEG-specific encoding used in Adobe-style CMYK JPEGs. - // ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform. - // This transform assumes Rec.601 YCbCr coefficients and an inverted K channel. - // - // The YccK => Cmyk conversion is independent of any embedded ICC profile. - // Since the same RGB working space is used during conversion to and from XYZ, - // colorimetric accuracy is preserved. - converter.Convert(MemoryMarshal.Cast(source), source); - - Span destination = MemoryMarshal.Cast(packed)[..source.Length]; - - ColorConversionOptions options = new() - { - SourceIccProfile = profile, - TargetIccProfile = CompactSrgbV4Profile.Profile, - }; - converter = new ColorProfileConverter(options); - converter.Convert(source, destination); - - UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..source.Length], c0, c1, c2); - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs deleted file mode 100644 index 67b7ee0dc..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class YccKVector128 : JpegColorConverterVector128 - { - public YccKVector128(int precision) - : base(JpegColorSpace.Ycck, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector128 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector128 kBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - // Used for the color conversion - Vector128 chromaOffset = Vector128.Create(-this.HalfValue); - Vector128 scale = Vector128.Create(1 / (this.MaximumValue * this.MaximumValue)); - Vector128 max = Vector128.Create(this.MaximumValue); - Vector128 rCrMult = Vector128.Create(YCbCrScalar.RCrMult); - Vector128 gCbMult = Vector128.Create(-YCbCrScalar.GCbMult); - Vector128 gCrMult = Vector128.Create(-YCbCrScalar.GCrMult); - Vector128 bCbMult = Vector128.Create(YCbCrScalar.BCbMult); - - // Walking 8 elements at one step: - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - // y = yVals[i]; - // cb = cbVals[i] - 128F; - // cr = crVals[i] - 128F; - // k = kVals[i] / 256F; - ref Vector128 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector128 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector128 c2 = ref Unsafe.Add(ref c2Base, i); - Vector128 y = c0; - Vector128 cb = c1 + chromaOffset; - Vector128 cr = c2 + chromaOffset; - Vector128 scaledK = Unsafe.Add(ref kBase, i) * scale; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector128 r = Vector128_.MultiplyAdd(y, cr, rCrMult); - Vector128 g = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); - Vector128 b = Vector128_.MultiplyAdd(y, cb, bCbMult); - - r = max - Vector128_.RoundToNearestInteger(r); - g = max - Vector128_.RoundToNearestInteger(g); - b = max - Vector128_.RoundToNearestInteger(b); - - r *= scaledK; - g *= scaledK; - b *= scaledK; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - // rgb -> cmyk - CmykVector128.ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); - - // cmyk -> ycck - ref Vector128 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector128 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector128 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - ref Vector128 srcR = ref destY; - ref Vector128 srcG = ref destCb; - ref Vector128 srcB = ref destCr; - - // Used for the color conversion - Vector128 maxSampleValue = Vector128.Create(this.MaximumValue); - - Vector128 chromaOffset = Vector128.Create(this.HalfValue); - - Vector128 f0299 = Vector128.Create(0.299f); - Vector128 f0587 = Vector128.Create(0.587f); - Vector128 f0114 = Vector128.Create(0.114f); - Vector128 fn0168736 = Vector128.Create(-0.168736f); - Vector128 fn0331264 = Vector128.Create(-0.331264f); - Vector128 fn0418688 = Vector128.Create(-0.418688f); - Vector128 fn0081312F = Vector128.Create(-0.081312F); - Vector128 f05 = Vector128.Create(0.5f); - - nuint n = values.Component0.Vector128Count(); - for (nuint i = 0; i < n; i++) - { - Vector128 r = maxSampleValue - Unsafe.Add(ref srcR, i); - Vector128 g = maxSampleValue - Unsafe.Add(ref srcG, i); - Vector128 b = maxSampleValue - Unsafe.Add(ref srcB, i); - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector128 y = Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector128 cb = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector128 cr = chromaOffset + Vector128_.MultiplyAdd(Vector128_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y; - Unsafe.Add(ref destCb, i) = cb; - Unsafe.Add(ref destCr, i) = cr; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs deleted file mode 100644 index 3a586d25f..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class YccKVector256 : JpegColorConverterVector256 - { - public YccKVector256(int precision) - : base(JpegColorSpace.Ycck, precision) - { - } - - /// - public override void ConvertToRgbInPlace(in ComponentValues values) - { - ref Vector256 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector256 kBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - // Used for the color conversion - Vector256 chromaOffset = Vector256.Create(-this.HalfValue); - Vector256 scale = Vector256.Create(1 / (this.MaximumValue * this.MaximumValue)); - Vector256 max = Vector256.Create(this.MaximumValue); - Vector256 rCrMult = Vector256.Create(YCbCrScalar.RCrMult); - Vector256 gCbMult = Vector256.Create(-YCbCrScalar.GCbMult); - Vector256 gCrMult = Vector256.Create(-YCbCrScalar.GCrMult); - Vector256 bCbMult = Vector256.Create(YCbCrScalar.BCbMult); - - // Walking 8 elements at one step: - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - // y = yVals[i]; - // cb = cbVals[i] - 128F; - // cr = crVals[i] - 128F; - // k = kVals[i] / 256F; - ref Vector256 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector256 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector256 c2 = ref Unsafe.Add(ref c2Base, i); - Vector256 y = c0; - Vector256 cb = c1 + chromaOffset; - Vector256 cr = c2 + chromaOffset; - Vector256 scaledK = Unsafe.Add(ref kBase, i) * scale; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector256 r = Vector256_.MultiplyAdd(y, cr, rCrMult); - Vector256 g = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); - Vector256 b = Vector256_.MultiplyAdd(y, cb, bCbMult); - - r = max - Vector256_.RoundToNearestInteger(r); - g = max - Vector256_.RoundToNearestInteger(g); - b = max - Vector256_.RoundToNearestInteger(b); - - r *= scaledK; - g *= scaledK; - b *= scaledK; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - // rgb -> cmyk - CmykVector256.ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane); - - // cmyk -> ycck - ref Vector256 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector256 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector256 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - ref Vector256 srcR = ref destY; - ref Vector256 srcG = ref destCb; - ref Vector256 srcB = ref destCr; - - // Used for the color conversion - Vector256 maxSampleValue = Vector256.Create(this.MaximumValue); - - Vector256 chromaOffset = Vector256.Create(this.HalfValue); - - Vector256 f0299 = Vector256.Create(0.299f); - Vector256 f0587 = Vector256.Create(0.587f); - Vector256 f0114 = Vector256.Create(0.114f); - Vector256 fn0168736 = Vector256.Create(-0.168736f); - Vector256 fn0331264 = Vector256.Create(-0.331264f); - Vector256 fn0418688 = Vector256.Create(-0.418688f); - Vector256 fn0081312F = Vector256.Create(-0.081312F); - Vector256 f05 = Vector256.Create(0.5f); - - nuint n = values.Component0.Vector256Count(); - for (nuint i = 0; i < n; i++) - { - Vector256 r = maxSampleValue - Unsafe.Add(ref srcR, i); - Vector256 g = maxSampleValue - Unsafe.Add(ref srcG, i); - Vector256 b = maxSampleValue - Unsafe.Add(ref srcB, i); - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector256 y = Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector256 cb = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector256 cr = chromaOffset + Vector256_.MultiplyAdd(Vector256_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y; - Unsafe.Add(ref destCb, i) = cb; - Unsafe.Add(ref destCr, i) = cr; - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs deleted file mode 100644 index b81a833cd..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - internal sealed class YccKVector512 : JpegColorConverterVector512 - { - public YccKVector512(int precision) - : base(JpegColorSpace.Ycck, precision) - { - } - - /// - protected override void ConvertToRgbInPlaceVectorized(in ComponentValues values) - { - ref Vector512 c0Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 c1Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 c2Base = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - ref Vector512 kBase = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component3)); - - // Used for the color conversion - Vector512 chromaOffset = Vector512.Create(-this.HalfValue); - Vector512 scale = Vector512.Create(1 / (this.MaximumValue * this.MaximumValue)); - Vector512 max = Vector512.Create(this.MaximumValue); - Vector512 rCrMult = Vector512.Create(YCbCrScalar.RCrMult); - Vector512 gCbMult = Vector512.Create(-YCbCrScalar.GCbMult); - Vector512 gCrMult = Vector512.Create(-YCbCrScalar.GCrMult); - Vector512 bCbMult = Vector512.Create(YCbCrScalar.BCbMult); - - // Walking 8 elements at one step: - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - // y = yVals[i]; - // cb = cbVals[i] - 128F; - // cr = crVals[i] - 128F; - // k = kVals[i] / 256F; - ref Vector512 c0 = ref Unsafe.Add(ref c0Base, i); - ref Vector512 c1 = ref Unsafe.Add(ref c1Base, i); - ref Vector512 c2 = ref Unsafe.Add(ref c2Base, i); - Vector512 y = c0; - Vector512 cb = c1 + chromaOffset; - Vector512 cr = c2 + chromaOffset; - Vector512 scaledK = Unsafe.Add(ref kBase, i) * scale; - - // r = y + (1.402F * cr); - // g = y - (0.344136F * cb) - (0.714136F * cr); - // b = y + (1.772F * cb); - Vector512 r = Vector512_.MultiplyAdd(y, cr, rCrMult); - Vector512 g = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(y, cb, gCbMult), cr, gCrMult); - Vector512 b = Vector512_.MultiplyAdd(y, cb, bCbMult); - - r = max - Vector512_.RoundToNearestInteger(r); - g = max - Vector512_.RoundToNearestInteger(g); - b = max - Vector512_.RoundToNearestInteger(b); - - r *= scaledK; - g *= scaledK; - b *= scaledK; - - c0 = r; - c1 = g; - c2 = b; - } - } - - /// - public override void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) - => YccKScalar.ConvertToRgbInPlaceWithIcc(configuration, profile, values, this.MaximumValue); - - /// - protected override void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values) - => YccKScalar.ConvertToRgbInPlace(values, this.MaximumValue, this.HalfValue); - - /// - protected override void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - // rgb -> cmyk - CmykVector512.ConvertFromRgbVectorized(in values, this.MaximumValue, rLane, gLane, bLane); - - // cmyk -> ycck - ref Vector512 destY = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component0)); - ref Vector512 destCb = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component1)); - ref Vector512 destCr = - ref Unsafe.As>(ref MemoryMarshal.GetReference(values.Component2)); - - ref Vector512 srcR = ref destY; - ref Vector512 srcG = ref destCb; - ref Vector512 srcB = ref destCr; - - // Used for the color conversion - Vector512 maxSampleValue = Vector512.Create(this.MaximumValue); - - Vector512 chromaOffset = Vector512.Create(this.HalfValue); - - Vector512 f0299 = Vector512.Create(0.299f); - Vector512 f0587 = Vector512.Create(0.587f); - Vector512 f0114 = Vector512.Create(0.114f); - Vector512 fn0168736 = Vector512.Create(-0.168736f); - Vector512 fn0331264 = Vector512.Create(-0.331264f); - Vector512 fn0418688 = Vector512.Create(-0.418688f); - Vector512 fn0081312F = Vector512.Create(-0.081312F); - Vector512 f05 = Vector512.Create(0.5f); - - nuint n = values.Component0.Vector512Count(); - for (nuint i = 0; i < n; i++) - { - Vector512 r = maxSampleValue - Unsafe.Add(ref srcR, i); - Vector512 g = maxSampleValue - Unsafe.Add(ref srcG, i); - Vector512 b = maxSampleValue - Unsafe.Add(ref srcB, i); - - // y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b) - // cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b) - // cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b) - Vector512 y = Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f0114 * b, f0587, g), f0299, r); - Vector512 cb = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(f05 * b, fn0331264, g), fn0168736, r); - Vector512 cr = chromaOffset + Vector512_.MultiplyAdd(Vector512_.MultiplyAdd(fn0081312F * b, fn0418688, g), f05, r); - - Unsafe.Add(ref destY, i) = y; - Unsafe.Add(ref destCb, i) = cb; - Unsafe.Add(ref destCr, i) = cr; - } - } - - /// - protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane) - => YccKScalar.ConvertFromRgb(in values, this.HalfValue, this.MaximumValue, rLane, gLane, bLane); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.Icc.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.Icc.cs new file mode 100644 index 000000000..37f1297a6 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.Icc.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#nullable disable + +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components; + +internal abstract partial class JpegColorConverterBase +{ + /// + /// Converts planar jpeg component values in to RGB color space in-place using the given ICC profile. + /// + /// The configuration instance to use for the conversion. + /// The input/output as a stack-only struct. + /// The ICC profile to use for the conversion. + public void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile) + { + Span c0 = values.Component0; + Span c1 = values.Component1; + Span c2 = values.Component2; + int length = c0.Length; + + // Four-component JPEG models need room for an interleaved CMYK or YccK source. Every conversion + // finishes with three packed RGB floats, which safely occupy the start of the same temporary buffer. + bool hasFourthComponent = this.ColorSpace is JpegColorSpace.Ycck + or JpegColorSpace.Cmyk + or JpegColorSpace.TiffYccK + or JpegColorSpace.TiffCmyk; + int packedComponentCount = hasFourthComponent ? 4 : 3; + + using IMemoryOwner memoryOwner = configuration.MemoryAllocator.Allocate(length * packedComponentCount); + Span packed = memoryOwner.Memory.Span; + + if (this.ColorSpace == JpegColorSpace.Grayscale) + { + // The single luminance plane is the ICC source, so it is normalized in place. The temporary + // buffer is still RGB-sized because the profile conversion expands each Y sample to three lanes. + TensorPrimitives_.Multiply(c0, 1F / this.MaximumValue, c0); + + Span source = MemoryMarshal.Cast(c0); + Span destination = MemoryMarshal.Cast(packed); + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + + ColorProfileConverter converter = new(options); + converter.Convert(source, destination); + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..length], c0, c1, c2); + return; + } + + // RGB and YCbCr become packed RGB before the profile transform. CMYK models remain packed CMYK, + // because their source profile describes that four-component space rather than an intermediate RGB space. + bool profileSourceIsCmyk = false; + + switch (this.ColorSpace) + { + case JpegColorSpace.RGB: + // JPEG RGB planes use the integer sample domain; ICC RGB values use normalized interleaved lanes. + PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / this.MaximumValue); + break; + + case JpegColorSpace.YCbCr: + // ICC profiles rarely expose YCbCr transforms. BT.601 therefore produces RGB in the profile's + // source space first, and that packed RGB becomes the input to the profile transform below. + PackedNormalizeInterleave3(c0, c1, c2, packed, 1F / this.MaximumValue); + + ColorProfileConverter yCbCrConverter = new(); + Span yCbCr = MemoryMarshal.Cast(packed); + Span yCbCrDestination = MemoryMarshal.Cast(packed); + yCbCrConverter.Convert(yCbCr, yCbCrDestination); + break; + + case JpegColorSpace.Cmyk: + // Adobe-style JPEG CMYK stores inverted samples, while ICC consumes conventional normalized CMYK. + PackedInvertNormalizeInterleave4(c0, c1, c2, values.Component3, packed, this.MaximumValue); + profileSourceIsCmyk = true; + break; + + case JpegColorSpace.TiffCmyk: + // TIFF JPEG CMYK is already non-inverted, so only normalization and interleaving are required. + PackedNormalizeInterleave4(c0, c1, c2, values.Component3, packed, this.MaximumValue); + profileSourceIsCmyk = true; + break; + + case JpegColorSpace.Ycck: + // Adobe-style JPEG YccK is inverted before its format-defined YccK-to-CMYK transform. + PackedInvertNormalizeInterleave4(c0, c1, c2, values.Component3, packed, this.MaximumValue); + + ColorProfileConverter yccKConverter = new(); + Span yccKCmyk = MemoryMarshal.Cast(packed); + yccKConverter.Convert(MemoryMarshal.Cast(yccKCmyk), yccKCmyk); + profileSourceIsCmyk = true; + break; + + case JpegColorSpace.TiffYccK: + // TIFF JPEG YccK is non-inverted, but otherwise uses the same YccK-to-CMYK transform. + PackedNormalizeInterleave4(c0, c1, c2, values.Component3, packed, this.MaximumValue); + + ColorProfileConverter tiffYccKConverter = new(); + Span tiffYccKCmyk = MemoryMarshal.Cast(packed); + tiffYccKConverter.Convert(MemoryMarshal.Cast(tiffYccKCmyk), tiffYccKCmyk); + profileSourceIsCmyk = true; + break; + } + + ColorConversionOptions profileOptions = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + + ColorProfileConverter profileConverter = new(profileOptions); + Span rgb = MemoryMarshal.Cast(packed)[..length]; + + if (profileSourceIsCmyk) + { + // The destination aliases the first three floats of each four-float source item. The converter + // supports this established in-place contraction, and the source span retains its original length. + profileConverter.Convert(MemoryMarshal.Cast(packed), rgb); + } + else + { + profileConverter.Convert(rgb, rgb); + } + + // Only the packed RGB prefix is meaningful after four-component conversion; scatter it back to the + // decoder's three planar output buffers while leaving the fourth source component untouched. + UnpackDeinterleave3(MemoryMarshal.Cast(packed)[..length], c0, c1, c2); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs index 74227c7a6..2b1271c82 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs @@ -2,11 +2,7 @@ // Licensed under the Six Labors Split License. #nullable disable -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -84,14 +80,6 @@ internal abstract partial class JpegColorConverterBase /// The input/output as a stack-only struct public abstract void ConvertToRgbInPlace(in ComponentValues values); - /// - /// Converts planar jpeg component values in to RGB color space in-place using the given ICC profile. - /// - /// The configuration instance to use for the conversion. - /// The input/output as a stack-only struct. - /// The ICC profile to use for the conversion. - public abstract void ConvertToRgbInPlaceWithIcc(Configuration configuration, in ComponentValues values, IccProfile profile); - /// /// Converts RGB lanes to jpeg component values. /// @@ -101,124 +89,6 @@ internal abstract partial class JpegColorConverterBase /// Blue colors lane. public abstract void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane); - public static void PackedNormalizeInterleave3( - ReadOnlySpan xLane, - ReadOnlySpan yLane, - ReadOnlySpan zLane, - Span packed, - float scale) - { - DebugGuard.IsTrue(packed.Length % 3 == 0, "Packed length must be divisible by 3."); - DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); - DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); - DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 3, xLane.Length, nameof(packed)); - - // TODO: Investigate SIMD version of this. - ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); - ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); - ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); - ref float packedRef = ref MemoryMarshal.GetReference(packed); - - for (nuint i = 0; i < (nuint)xLane.Length; i++) - { - nuint baseIdx = i * 3; - Unsafe.Add(ref packedRef, baseIdx) = Unsafe.Add(ref xLaneRef, i) * scale; - Unsafe.Add(ref packedRef, baseIdx + 1) = Unsafe.Add(ref yLaneRef, i) * scale; - Unsafe.Add(ref packedRef, baseIdx + 2) = Unsafe.Add(ref zLaneRef, i) * scale; - } - } - - public static void UnpackDeinterleave3( - ReadOnlySpan packed, - Span xLane, - Span yLane, - Span zLane) - { - DebugGuard.IsTrue(packed.Length == xLane.Length, nameof(packed), "Channels must be of same size!"); - DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); - DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); - - // TODO: Investigate SIMD version of this. - ref float packedRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(packed)); - ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); - ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); - ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); - - for (nuint i = 0; i < (nuint)packed.Length; i++) - { - nuint baseIdx = i * 3; - Unsafe.Add(ref xLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx); - Unsafe.Add(ref yLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx + 1); - Unsafe.Add(ref zLaneRef, i) = Unsafe.Add(ref packedRef, baseIdx + 2); - } - } - - public static void PackedNormalizeInterleave4( - ReadOnlySpan xLane, - ReadOnlySpan yLane, - ReadOnlySpan zLane, - ReadOnlySpan wLane, - Span packed, - float maxValue) - { - DebugGuard.IsTrue(packed.Length % 4 == 0, "Packed length must be divisible by 4."); - DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); - DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); - DebugGuard.IsTrue(wLane.Length == xLane.Length, nameof(wLane), "Channels must be of same size!"); - DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 4, xLane.Length, nameof(packed)); - - float scale = 1F / maxValue; - - // TODO: Investigate SIMD version of this. - ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); - ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); - ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); - ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); - ref float packedRef = ref MemoryMarshal.GetReference(packed); - - for (nuint i = 0; i < (nuint)xLane.Length; i++) - { - nuint baseIdx = i * 4; - Unsafe.Add(ref packedRef, baseIdx) = Unsafe.Add(ref xLaneRef, i) * scale; - Unsafe.Add(ref packedRef, baseIdx + 1) = Unsafe.Add(ref yLaneRef, i) * scale; - Unsafe.Add(ref packedRef, baseIdx + 2) = Unsafe.Add(ref zLaneRef, i) * scale; - Unsafe.Add(ref packedRef, baseIdx + 3) = Unsafe.Add(ref wLaneRef, i) * scale; - } - } - - public static void PackedInvertNormalizeInterleave4( - ReadOnlySpan xLane, - ReadOnlySpan yLane, - ReadOnlySpan zLane, - ReadOnlySpan wLane, - Span packed, - float maxValue) - { - DebugGuard.IsTrue(packed.Length % 4 == 0, "Packed length must be divisible by 4."); - DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); - DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); - DebugGuard.IsTrue(wLane.Length == xLane.Length, nameof(wLane), "Channels must be of same size!"); - DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 4, xLane.Length, nameof(packed)); - - float scale = 1F / maxValue; - - // TODO: Investigate SIMD version of this. - ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); - ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); - ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); - ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); - ref float packedRef = ref MemoryMarshal.GetReference(packed); - - for (nuint i = 0; i < (nuint)xLane.Length; i++) - { - nuint baseIdx = i * 4; - Unsafe.Add(ref packedRef, baseIdx) = (maxValue - Unsafe.Add(ref xLaneRef, i)) * scale; - Unsafe.Add(ref packedRef, baseIdx + 1) = (maxValue - Unsafe.Add(ref yLaneRef, i)) * scale; - Unsafe.Add(ref packedRef, baseIdx + 2) = (maxValue - Unsafe.Add(ref zLaneRef, i)) * scale; - Unsafe.Add(ref packedRef, baseIdx + 3) = (maxValue - Unsafe.Add(ref wLaneRef, i)) * scale; - } - } - /// /// Returns the s for all supported color spaces and precisions. /// @@ -248,161 +118,50 @@ internal abstract partial class JpegColorConverterBase /// Returns the s for the YCbCr colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetYCbCrConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new YCbCrVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new YCbCrVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new YCbCrVector128(precision); - } - - return new YCbCrScalar(precision); - } + private static JpegColorConverter GetYCbCrConverter(int precision) + => new JpegColorConverter(precision); /// /// Returns the s for the YccK colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetYccKConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new YccKVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new YccKVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new YccKVector128(precision); - } - - return new YccKScalar(precision); - } + private static JpegColorConverter GetYccKConverter(int precision) + => new JpegColorConverter(precision); /// /// Returns the s for the CMYK colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetCmykConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new CmykVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new CmykVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new CmykVector128(precision); - } - - return new CmykScalar(precision); - } + private static JpegColorConverter GetCmykConverter(int precision) + => new JpegColorConverter(precision); /// /// Returns the s for the gray scale colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetGrayScaleConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new GrayScaleVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new GrayScaleVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new GrayScaleVector128(precision); - } - - return new GrayScaleScalar(precision); - } + private static JpegColorConverter GetGrayScaleConverter(int precision) + => new JpegColorConverter(precision); /// /// Returns the s for the RGB colorspace. /// /// The precision in bits. - private static JpegColorConverterBase GetRgbConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new RgbVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new RgbVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new RgbVector128(precision); - } - - return new RgbScalar(precision); - } - - private static JpegColorConverterBase GetTiffCmykConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new TiffCmykVector512(precision); - } + private static JpegColorConverter GetRgbConverter(int precision) + => new JpegColorConverter(precision); - if (JpegColorConverterVector256.IsSupported) - { - return new TiffCmykVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new TiffCmykVector128(precision); - } - - return new TiffCmykScalar(precision); - } - - private static JpegColorConverterBase GetTiffYccKConverter(int precision) - { - if (JpegColorConverterVector512.IsSupported) - { - return new TiffYccKVector512(precision); - } - - if (JpegColorConverterVector256.IsSupported) - { - return new TiffYccKVector256(precision); - } - - if (JpegColorConverterVector128.IsSupported) - { - return new TiffYccKVector128(precision); - } + /// + /// Returns the for non-inverted TIFF CMYK. + /// + /// The precision in bits. + private static JpegColorConverter GetTiffCmykConverter(int precision) + => new JpegColorConverter(precision); - return new TiffYccKScalar(precision); - } + /// + /// Returns the for non-inverted TIFF YccK. + /// + /// The precision in bits. + private static JpegColorConverter GetTiffYccKConverter(int precision) + => new JpegColorConverter(precision); /// /// A stack-only struct to reference the input buffers using -s. @@ -493,6 +252,14 @@ internal abstract partial class JpegColorConverterBase this.Component3 = this.ComponentCount > 3 ? processors[3].GetColorBufferRowSpan(row) : []; } + /// + /// Initializes a new instance of the struct from explicitly supplied planar spans. + /// + /// The number of populated component planes. + /// The first component plane. + /// The second component plane, if present. + /// The third component plane, if present. + /// The fourth component plane, if present. internal ComponentValues( int componentCount, Span c0, @@ -507,6 +274,12 @@ internal abstract partial class JpegColorConverterBase this.Component3 = c3; } + /// + /// Creates a view over the same component planes for the requested sample range. + /// + /// The zero-based sample offset. + /// The number of samples in each returned plane. + /// The sliced component values. public ComponentValues Slice(int start, int length) { Span c0 = this.Component0.Slice(start, length); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs deleted file mode 100644 index 13e5c6d5b..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - /// - /// abstract base for implementations - /// based on scalar instructions. - /// - internal abstract class JpegColorConverterScalar : JpegColorConverterBase - { - protected JpegColorConverterScalar(JpegColorSpace colorSpace, int precision) - : base(colorSpace, precision) - { - } - - public sealed override bool IsAvailable => true; - - public sealed override int ElementsPerBatch => 1; - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs deleted file mode 100644 index f3c3eb8db..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - /// - /// abstract base for implementations - /// based on API. - /// - /// - /// Converters of this family can work with data of any size. - /// Even though real life data is guaranteed to be of size - /// divisible by 8 newer SIMD instructions like AVX512 won't work with - /// such data out of the box. These converters have fallback code - /// for 'remainder' data. - /// - internal abstract class JpegColorConverterVector : JpegColorConverterBase - { - protected JpegColorConverterVector(JpegColorSpace colorSpace, int precision) - : base(colorSpace, precision) - { - } - - /// - /// Gets a value indicating whether this converter is supported on current hardware. - /// - public static bool IsSupported => Vector.IsHardwareAccelerated && Vector.Count % 4 == 0; - - /// - public sealed override bool IsAvailable => IsSupported; - - public override int ElementsPerBatch => Vector.Count; - - /// - public sealed override void ConvertToRgbInPlace(in ComponentValues values) - { - DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); - - int length = values.Component0.Length; - int remainder = (int)((uint)length % (uint)Vector.Count); - - int simdCount = length - remainder; - if (simdCount > 0) - { - this.ConvertToRgbInPlaceVectorized(values.Slice(0, simdCount)); - } - - // Jpeg images width is always divisible by 8 without a remainder - // so it's safe to say SSE/AVX1/AVX2 implementations would never have - // 'remainder' pixels - // But some exotic simd implementations e.g. AVX-512 can have - // remainder pixels - if (remainder > 0) - { - this.ConvertToRgbInPlaceScalarRemainder(values.Slice(simdCount, remainder)); - } - } - - /// - public sealed override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); - - int length = values.Component0.Length; - int remainder = (int)((uint)length % (uint)Vector.Count); - - int simdCount = length - remainder; - if (simdCount > 0) - { - this.ConvertFromRgbVectorized( - values.Slice(0, simdCount), - rLane[..simdCount], - gLane[..simdCount], - bLane[..simdCount]); - } - - // Jpeg images width is always divisible by 8 without a remainder - // so it's safe to say SSE/AVX1/AVX2 implementations would never have - // 'remainder' pixels - // But some exotic simd implementations e.g. AVX-512 can have - // remainder pixels - if (remainder > 0) - { - this.ConvertFromRgbScalarRemainder( - values.Slice(simdCount, remainder), - rLane.Slice(simdCount, remainder), - gLane.Slice(simdCount, remainder), - bLane.Slice(simdCount, remainder)); - } - } - - /// - /// Converts planar jpeg component values in - /// to RGB color space in place using API. - /// - /// The input/output as a stack-only struct - protected abstract void ConvertToRgbInPlaceVectorized(in ComponentValues values); - - /// - /// Converts remainder of the planar jpeg component values after - /// conversion in . - /// - /// The input/output as a stack-only struct - protected abstract void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values); - - /// - /// Converts RGB lanes to jpeg component values using API. - /// - /// Jpeg component values. - /// Red colors lane. - /// Green colors lane. - /// Blue colors lane. - protected abstract void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane); - - /// - /// Converts remainder of RGB lanes to jpeg component values after - /// conversion in . - /// - /// Jpeg component values. - /// Red colors lane. - /// Green colors lane. - /// Blue colors lane. - protected abstract void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector128.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector128.cs deleted file mode 100644 index 5cbb376c7..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector128.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.Intrinsics; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - /// - /// abstract base for implementations - /// based on instructions. - /// - /// - /// Converters of this family would expect input buffers lengths to be - /// divisible by 8 without a remainder. - /// This is guaranteed by real-life data as jpeg stores pixels via 8x8 blocks. - /// DO NOT pass test data of invalid size to these converters as they - /// potentially won't do a bound check and return a false positive result. - /// - internal abstract class JpegColorConverterVector128 : JpegColorConverterBase - { - protected JpegColorConverterVector128(JpegColorSpace colorSpace, int precision) - : base(colorSpace, precision) - { - } - - public static bool IsSupported => Vector128.IsHardwareAccelerated; - - public sealed override bool IsAvailable => IsSupported; - - public sealed override int ElementsPerBatch => Vector128.Count; - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector256.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector256.cs deleted file mode 100644 index 61c37d846..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector256.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.Intrinsics; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - /// - /// abstract base for implementations - /// based on instructions. - /// - /// - /// Converters of this family would expect input buffers lengths to be - /// divisible by 8 without a remainder. - /// This is guaranteed by real-life data as jpeg stores pixels via 8x8 blocks. - /// DO NOT pass test data of invalid size to these converters as they - /// potentially won't do a bound check and return a false positive result. - /// - internal abstract class JpegColorConverterVector256 : JpegColorConverterBase - { - protected JpegColorConverterVector256(JpegColorSpace colorSpace, int precision) - : base(colorSpace, precision) - { - } - - public static bool IsSupported => Vector256.IsHardwareAccelerated; - - public sealed override bool IsAvailable => IsSupported; - - public sealed override int ElementsPerBatch => Vector256.Count; - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector512.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector512.cs deleted file mode 100644 index 0c7d032d4..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector512.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.Intrinsics; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components; - -internal abstract partial class JpegColorConverterBase -{ - /// - /// abstract base for implementations - /// based on instructions. - /// - internal abstract class JpegColorConverterVector512 : JpegColorConverterBase - { - protected JpegColorConverterVector512(JpegColorSpace colorSpace, int precision) - : base(colorSpace, precision) - { - } - - public static bool IsSupported => Vector512.IsHardwareAccelerated; - - /// - public override bool IsAvailable => IsSupported; - - /// - public override int ElementsPerBatch => Vector512.Count; - - /// - public sealed override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) - { - DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); - - int length = values.Component0.Length; - int remainder = (int)((uint)length % (uint)Vector512.Count); - - int simdCount = length - remainder; - if (simdCount > 0) - { - this.ConvertFromRgbVectorized( - values.Slice(0, simdCount), - rLane[..simdCount], - gLane[..simdCount], - bLane[..simdCount]); - } - - if (remainder > 0) - { - this.ConvertFromRgbScalarRemainder( - values.Slice(simdCount, remainder), - rLane.Slice(simdCount, remainder), - gLane.Slice(simdCount, remainder), - bLane.Slice(simdCount, remainder)); - } - } - - /// - public sealed override void ConvertToRgbInPlace(in ComponentValues values) - { - DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); - - int length = values.Component0.Length; - int remainder = (int)((uint)length % (uint)Vector512.Count); - - int simdCount = length - remainder; - if (simdCount > 0) - { - this.ConvertToRgbInPlaceVectorized(values.Slice(0, simdCount)); - } - - if (remainder > 0) - { - this.ConvertToRgbInPlaceScalarRemainder(values.Slice(simdCount, remainder)); - } - } - - /// - /// Converts planar jpeg component values in - /// to RGB color space in place using API. - /// - /// The input/output as a stack-only struct - protected abstract void ConvertToRgbInPlaceVectorized(in ComponentValues values); - - /// - /// Converts remainder of the planar jpeg component values after - /// conversion in . - /// - /// The input/output as a stack-only struct - protected abstract void ConvertToRgbInPlaceScalarRemainder(in ComponentValues values); - - /// - /// Converts RGB lanes to jpeg component values using API. - /// - /// Jpeg component values. - /// Red colors lane. - /// Green colors lane. - /// Blue colors lane. - protected abstract void ConvertFromRgbVectorized(in ComponentValues values, Span rLane, Span gLane, Span bLane); - - /// - /// Converts remainder of RGB lanes to jpeg component values after - /// conversion in . - /// - /// Jpeg component values. - /// Red colors lane. - /// Green colors lane. - /// Blue colors lane. - protected abstract void ConvertFromRgbScalarRemainder(in ComponentValues values, Span rLane, Span gLane, Span bLane); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs index 79e25a67a..b4399a69c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs @@ -53,10 +53,10 @@ internal sealed class DirectComponentProcessor : ComponentProcessor // Convert from spectral to color FloatingPointDCT.TransformIDCT(ref workspaceBlock); - // To conform better to libjpeg we actually NEED TO loose precision here. - // This is because they store blocks as Int16 between all the operations. - // To be "more accurate", we need to emulate this by rounding! - workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue); + // Normalize into the component sample range without quantizing away + // fractional precision. The later color conversion / final pack stage + // performs the only rounding we actually need for output samples. + workspaceBlock.NormalizeColorsInPlace(maximumValue); // Write to color buffer acording to sampling factors int xColorBufferStart = xBlock * this.BlockAreaSize.Width; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs index 300a77331..23b81d2ba 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs @@ -66,32 +66,335 @@ internal sealed class DownScalingComponentProcessor2 : ComponentProcessor [MethodImpl(InliningOptions.ShortMethod)] public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) { - // TODO: Optimize: implement all cases with scale-specific, loopless code! + if (horizontalScale == 1 && verticalScale == 1) + { + CopyTo1x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + CopyTo2x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 1) + { + CopyTo2x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 2) + { + CopyTo1x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 1) + { + CopyTo4x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 2) + { + CopyTo4x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 4) + { + CopyTo1x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 4) + { + CopyTo2x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 4) + { + CopyTo4x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. CopyArbitraryScale(ref block, ref destRef, (uint)destStrideWidth, (uint)horizontalScale, (uint)verticalScale); + } + + /// + /// Copies a 4x4 reduced block directly into the destination buffer when no chroma expansion is needed. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while doubling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while doubling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while doubling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + } - [MethodImpl(InliningOptions.ColdPath)] - static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale) + /// + /// Copies a 4x4 reduced block into the destination buffer while quadrupling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 2u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 3u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while quadrupling horizontally and doubling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 4u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 5u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 6u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 7u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while quadrupling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + CopyRow4(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while doubling horizontally and quadrupling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + WidenRow4(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + } + + /// + /// Copies a 4x4 reduced block into the destination buffer while quadrupling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 8u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 9u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 10u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 2u, 11u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 12u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 13u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 14u, areaStride); + ExpandRow4(ref sourceBase, ref areaOrigin, 3u, 15u, areaStride); + } + + /// + /// Copies one four-sample row from the reduced block to the destination row. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyRow4(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + Unsafe.CopyBlock( + ref Unsafe.As(ref dest), + ref Unsafe.As(ref source), + 4u * sizeof(float)); + } + + /// + /// Expands one four-sample row to eight samples by duplicating each source value horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void WidenRow4(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + float value0 = source; + float value1 = Unsafe.Add(ref source, 1u); + float value2 = Unsafe.Add(ref source, 2u); + float value3 = Unsafe.Add(ref source, 3u); + + dest = value0; + Unsafe.Add(ref dest, 1u) = value0; + Unsafe.Add(ref dest, 2u) = value1; + Unsafe.Add(ref dest, 3u) = value1; + Unsafe.Add(ref dest, 4u) = value2; + Unsafe.Add(ref dest, 5u) = value2; + Unsafe.Add(ref dest, 6u) = value3; + Unsafe.Add(ref dest, 7u) = value3; + } + + /// + /// Expands one four-sample row to sixteen samples by duplicating each source value four times horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void ExpandRow4(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + float value0 = source; + float value1 = Unsafe.Add(ref source, 1u); + float value2 = Unsafe.Add(ref source, 2u); + float value3 = Unsafe.Add(ref source, 3u); + + dest = value0; + Unsafe.Add(ref dest, 1u) = value0; + Unsafe.Add(ref dest, 2u) = value0; + Unsafe.Add(ref dest, 3u) = value0; + Unsafe.Add(ref dest, 4u) = value1; + Unsafe.Add(ref dest, 5u) = value1; + Unsafe.Add(ref dest, 6u) = value1; + Unsafe.Add(ref dest, 7u) = value1; + Unsafe.Add(ref dest, 8u) = value2; + Unsafe.Add(ref dest, 9u) = value2; + Unsafe.Add(ref dest, 10u) = value2; + Unsafe.Add(ref dest, 11u) = value2; + Unsafe.Add(ref dest, 12u) = value3; + Unsafe.Add(ref dest, 13u) = value3; + Unsafe.Add(ref dest, 14u) = value3; + Unsafe.Add(ref dest, 15u) = value3; + } + + /// + /// Replicates each reduced sample into an arbitrary integral expansion rectangle for uncommon subsampling ratios. + /// + [MethodImpl(InliningOptions.ColdPath)] + private static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale) + { + for (nuint y = 0u; y < 4u; y++) { - for (nuint y = 0; y < 4; y++) + nuint yy = y * verticalScale; + nuint y8 = y * 8u; + + for (nuint x = 0u; x < 4u; x++) { - nuint yy = y * verticalScale; - nuint y8 = y * 8; + nuint xx = x * horizontalScale; - for (nuint x = 0; x < 4; x++) - { - nuint xx = x * horizontalScale; + float value = block[y8 + x]; - float value = block[y8 + x]; + for (nuint i = 0u; i < verticalScale; i++) + { + nuint baseIdx = ((yy + i) * areaStride) + xx; - for (nuint i = 0; i < verticalScale; i++) + for (nuint j = 0u; j < horizontalScale; j++) { - nuint baseIdx = ((yy + i) * areaStride) + xx; - - for (nuint j = 0; j < horizontalScale; j++) - { - // area[xx + j, yy + i] = value; - Unsafe.Add(ref areaOrigin, baseIdx + j) = value; - } + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, baseIdx + j) = value; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs index 798416990..a645a88c9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs @@ -66,32 +66,277 @@ internal sealed class DownScalingComponentProcessor4 : ComponentProcessor [MethodImpl(InliningOptions.ShortMethod)] public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) { - // TODO: Optimize: implement all cases with scale-specific, loopless code! + if (horizontalScale == 1 && verticalScale == 1) + { + CopyTo1x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + CopyTo2x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 1) + { + CopyTo2x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 2) + { + CopyTo1x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 1) + { + CopyTo4x1Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 2) + { + CopyTo4x2Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 4) + { + CopyTo1x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 4) + { + CopyTo2x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 4) + { + CopyTo4x4Scale(ref block, ref destRef, (uint)destStrideWidth); + return; + } + + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. CopyArbitraryScale(ref block, ref destRef, (uint)destStrideWidth, (uint)horizontalScale, (uint)verticalScale); + } + + /// + /// Copies a 2x2 reduced block directly into the destination buffer when no chroma expansion is needed. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while doubling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while doubling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while doubling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + } - [MethodImpl(InliningOptions.ColdPath)] - static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale) + /// + /// Copies a 2x2 reduced block into the destination buffer while quadrupling only the horizontal axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x1Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 1u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while quadrupling horizontally and doubling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x2Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 2u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 3u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while quadrupling only the vertical axis. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + CopyRow2(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while doubling horizontally and quadrupling vertically. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + WidenRow2(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + } + + /// + /// Copies a 2x2 reduced block into the destination buffer while quadrupling both axes. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x4Scale(ref Block8x8F block, ref float areaOrigin, uint areaStride) + { + ref float sourceBase = ref Unsafe.As(ref block); + + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 0u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 1u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 2u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 0u, 3u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 4u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 5u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 6u, areaStride); + ExpandRow2(ref sourceBase, ref areaOrigin, 1u, 7u, areaStride); + } + + /// + /// Copies one two-sample row from the reduced block to the destination row. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyRow2(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + Unsafe.CopyBlock( + ref Unsafe.As(ref dest), + ref Unsafe.As(ref source), + 2u * sizeof(float)); + } + + /// + /// Expands one two-sample row to four samples by duplicating each source value horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void WidenRow2(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + float value0 = source; + float value1 = Unsafe.Add(ref source, 1u); + + dest = value0; + Unsafe.Add(ref dest, 1u) = value0; + Unsafe.Add(ref dest, 2u) = value1; + Unsafe.Add(ref dest, 3u) = value1; + } + + /// + /// Expands one two-sample row to eight samples by duplicating each source value four times horizontally. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void ExpandRow2(ref float sourceBase, ref float areaOrigin, nuint sourceRow, nuint destRow, uint areaStride) + { + ref float source = ref Unsafe.Add(ref sourceBase, sourceRow * 8u); + ref float dest = ref Unsafe.Add(ref areaOrigin, destRow * areaStride); + + float value0 = source; + float value1 = Unsafe.Add(ref source, 1u); + + dest = value0; + Unsafe.Add(ref dest, 1u) = value0; + Unsafe.Add(ref dest, 2u) = value0; + Unsafe.Add(ref dest, 3u) = value0; + Unsafe.Add(ref dest, 4u) = value1; + Unsafe.Add(ref dest, 5u) = value1; + Unsafe.Add(ref dest, 6u) = value1; + Unsafe.Add(ref dest, 7u) = value1; + } + + /// + /// Replicates each reduced sample into an arbitrary integral expansion rectangle for uncommon subsampling ratios. + /// + [MethodImpl(InliningOptions.ColdPath)] + private static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, uint areaStride, uint horizontalScale, uint verticalScale) + { + for (nuint y = 0u; y < 2u; y++) { - for (nuint y = 0; y < 2; y++) + nuint yy = y * verticalScale; + nuint y8 = y * 8u; + + for (nuint x = 0u; x < 2u; x++) { - nuint yy = y * verticalScale; - nuint y8 = y * 8; + nuint xx = x * horizontalScale; - for (nuint x = 0; x < 2; x++) - { - nuint xx = x * horizontalScale; + float value = block[y8 + x]; - float value = block[y8 + x]; + for (nuint i = 0u; i < verticalScale; i++) + { + nuint baseIdx = ((yy + i) * areaStride) + xx; - for (nuint i = 0; i < verticalScale; i++) + for (nuint j = 0u; j < horizontalScale; j++) { - nuint baseIdx = ((yy + i) * areaStride) + xx; - - for (nuint j = 0; j < horizontalScale; j++) - { - // area[xx + j, yy + i] = value; - Unsafe.Add(ref areaOrigin, baseIdx + j) = value; - } + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, baseIdx + j) = value; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs index f3b09e6b4..0d881977f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs @@ -63,16 +63,64 @@ internal sealed class DownScalingComponentProcessor8 : ComponentProcessor return; } + if (horizontalScale == 2 && verticalScale == 1) + { + CopyTo2x1Scale(value, ref destRef); + return; + } + + if (horizontalScale == 1 && verticalScale == 2) + { + CopyTo1x2Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + if (horizontalScale == 2 && verticalScale == 2) { - destRef = value; - Unsafe.Add(ref destRef, 1) = value; - Unsafe.Add(ref destRef, 0 + (uint)destStrideWidth) = value; - Unsafe.Add(ref destRef, 1 + (uint)destStrideWidth) = value; + CopyTo2x2Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 1) + { + CopyTo4x1Scale(value, ref destRef); + return; + } + + if (horizontalScale == 4 && verticalScale == 2) + { + CopyTo4x2Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 1 && verticalScale == 4) + { + CopyTo1x4Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 2 && verticalScale == 4) + { + CopyTo2x4Scale(value, ref destRef, (uint)destStrideWidth); + return; + } + + if (horizontalScale == 4 && verticalScale == 4) + { + CopyTo4x4Scale(value, ref destRef, (uint)destStrideWidth); return; } - // TODO: Optimize: implement all cases with scale-specific, loopless code! + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. + CopyArbitraryScale(value, ref destRef, destStrideWidth, horizontalScale, verticalScale); + } + + [MethodImpl(InliningOptions.ColdPath)] + private static float CopyArbitraryScale(float value, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + // The common 1x, 2x, and 4x integral scales are specialized above. + // Uncommon legal factor-3 scales use the generic fallback. for (nuint y = 0; y < (uint)verticalScale; y++) { for (nuint x = 0; x < (uint)horizontalScale; x++) @@ -82,5 +130,111 @@ internal sealed class DownScalingComponentProcessor8 : ComponentProcessor destRef = ref Unsafe.Add(ref destRef, (uint)destStrideWidth); } + + return destRef; + } + + /// + /// Writes a single source value to two horizontally adjacent samples. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x1Scale(float value, ref float areaOrigin) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, 1u) = value; + } + + /// + /// Writes a single source value to two vertically adjacent samples. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x2Scale(float value, ref float areaOrigin, uint areaStride) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, areaStride) = value; + } + + /// + /// Writes a single source value to a 2x2 rectangle. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x2Scale(float value, ref float areaOrigin, uint areaStride) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, 1u) = value; + Unsafe.Add(ref areaOrigin, areaStride) = value; + Unsafe.Add(ref areaOrigin, areaStride + 1u) = value; + } + + /// + /// Writes a single source value to four horizontally adjacent samples. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x1Scale(float value, ref float areaOrigin) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, 1u) = value; + Unsafe.Add(ref areaOrigin, 2u) = value; + Unsafe.Add(ref areaOrigin, 3u) = value; + } + + /// + /// Writes a single source value to a 4x2 rectangle. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x2Scale(float value, ref float areaOrigin, uint areaStride) + { + CopyTo4x1Scale(value, ref areaOrigin); + + ref float nextRow = ref Unsafe.Add(ref areaOrigin, areaStride); + CopyTo4x1Scale(value, ref nextRow); + } + + /// + /// Writes a single source value to four vertically adjacent samples. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo1x4Scale(float value, ref float areaOrigin, uint areaStride) + { + areaOrigin = value; + Unsafe.Add(ref areaOrigin, areaStride) = value; + Unsafe.Add(ref areaOrigin, areaStride * 2u) = value; + Unsafe.Add(ref areaOrigin, areaStride * 3u) = value; + } + + /// + /// Writes a single source value to a 2x4 rectangle. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo2x4Scale(float value, ref float areaOrigin, uint areaStride) + { + CopyTo2x1Scale(value, ref areaOrigin); + + ref float row1 = ref Unsafe.Add(ref areaOrigin, areaStride); + CopyTo2x1Scale(value, ref row1); + + ref float row2 = ref Unsafe.Add(ref areaOrigin, areaStride * 2u); + CopyTo2x1Scale(value, ref row2); + + ref float row3 = ref Unsafe.Add(ref areaOrigin, areaStride * 3u); + CopyTo2x1Scale(value, ref row3); + } + + /// + /// Writes a single source value to a 4x4 rectangle. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static void CopyTo4x4Scale(float value, ref float areaOrigin, uint areaStride) + { + CopyTo4x1Scale(value, ref areaOrigin); + + ref float row1 = ref Unsafe.Add(ref areaOrigin, areaStride); + CopyTo4x1Scale(value, ref row1); + + ref float row2 = ref Unsafe.Add(ref areaOrigin, areaStride * 2u); + CopyTo4x1Scale(value, ref row2); + + ref float row3 = ref Unsafe.Add(ref areaOrigin, areaStride * 3u); + CopyTo4x1Scale(value, ref row3); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs index 1b0a17704..c68dd19b4 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs @@ -5,8 +5,8 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; @@ -116,52 +116,9 @@ internal class ComponentProcessor : IDisposable } static void SumVertical(Span target, Span source) - { - if (Avx.IsSupported) - { - ref Vector256 targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); - ref Vector256 sourceVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - - // Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed - DebugGuard.IsTrue(source.Length % 8 == 0, "source must be multiple of 8"); - nuint count = source.Vector256Count(); - for (nuint i = 0; i < count; i++) - { - Unsafe.Add(ref targetVectorRef, i) = Avx.Add(Unsafe.Add(ref targetVectorRef, i), Unsafe.Add(ref sourceVectorRef, i)); - } - } - else if (AdvSimd.IsSupported) - { - ref Vector128 targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); - ref Vector128 sourceVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - // Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed - DebugGuard.IsTrue(source.Length % 8 == 0, "source must be multiple of 8"); - nuint count = source.Vector128Count(); - for (nuint i = 0; i < count; i++) - { - Unsafe.Add(ref targetVectorRef, i) = AdvSimd.Add(Unsafe.Add(ref targetVectorRef, i), Unsafe.Add(ref sourceVectorRef, i)); - } - } - else - { - ref Vector targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); - ref Vector sourceVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - - nuint count = source.VectorCount(); - for (nuint i = 0; i < count; i++) - { - Unsafe.Add(ref targetVectorRef, i) += Unsafe.Add(ref sourceVectorRef, i); - } - - ref float targetRef = ref MemoryMarshal.GetReference(target); - ref float sourceRef = ref MemoryMarshal.GetReference(source); - for (nuint i = count * (uint)Vector.Count; i < (uint)source.Length; i++) - { - Unsafe.Add(ref targetRef, i) += Unsafe.Add(ref sourceRef, i); - } - } - } + // Exact destination overlap is supported, so each accumulated row remains in target. + => TensorPrimitives_.Add(target, source, target); static void SumHorizontal(Span target, int factor) { @@ -209,50 +166,8 @@ internal class ComponentProcessor : IDisposable } static void MultiplyToAverage(Span target, float multiplier) - { - if (Avx.IsSupported) - { - ref Vector256 targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); - - // Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed - DebugGuard.IsTrue(target.Length % 8 == 0, "target must be multiple of 8"); - nuint count = target.Vector256Count(); - Vector256 multiplierVector = Vector256.Create(multiplier); - for (nuint i = 0; i < count; i++) - { - Unsafe.Add(ref targetVectorRef, i) = Avx.Multiply(Unsafe.Add(ref targetVectorRef, i), multiplierVector); - } - } - else if (AdvSimd.IsSupported) - { - ref Vector128 targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); - // Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed - DebugGuard.IsTrue(target.Length % 8 == 0, "target must be multiple of 8"); - nuint count = target.Vector128Count(); - Vector128 multiplierVector = Vector128.Create(multiplier); - for (nuint i = 0; i < count; i++) - { - Unsafe.Add(ref targetVectorRef, i) = AdvSimd.Multiply(Unsafe.Add(ref targetVectorRef, i), multiplierVector); - } - } - else - { - ref Vector targetVectorRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(target)); - - nuint count = target.VectorCount(); - Vector multiplierVector = new(multiplier); - for (nuint i = 0; i < count; i++) - { - Unsafe.Add(ref targetVectorRef, i) *= multiplierVector; - } - - ref float targetRef = ref MemoryMarshal.GetReference(target); - for (nuint i = count * (uint)Vector.Count; i < (uint)target.Length; i++) - { - Unsafe.Add(ref targetRef, i) *= multiplier; - } - } - } + // Apply the subsampling reciprocal in place after all contributing rows have been summed. + => TensorPrimitives_.Multiply(target, multiplier, target); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs index b60ef68f1..8662c5c49 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs @@ -114,9 +114,9 @@ internal class SpectralConverter : SpectralConverter, IDisposable Span sourceRow = this.pixelBuffer.DangerousGetRowSpan(srcIndex); PixelOperations.Instance.UnpackIntoRgbPlanes(rLane, gLane, bLane, sourceRow); - rLane.Slice(paddingStartIndex).Fill(rLane[paddingStartIndex - 1]); - gLane.Slice(paddingStartIndex).Fill(gLane[paddingStartIndex - 1]); - bLane.Slice(paddingStartIndex).Fill(bLane[paddingStartIndex - 1]); + rLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(rLane[paddingStartIndex - 1]); + gLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(gLane[paddingStartIndex - 1]); + bLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(bLane[paddingStartIndex - 1]); // Convert from rgb24 to target pixel type JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y); diff --git a/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Vector256.cs b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Vector256.cs index bcd8c7043..7c342d7a0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Vector256.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Vector256.cs @@ -55,8 +55,8 @@ internal static partial class FloatingPointDCT tmp12 = tmp6 + tmp7; Vector256 z5 = (tmp10 - tmp12) * Vector256.Create(0.382683433f); // mm256_F_0_3826 - Vector256 z2 = Vector256_.MultiplyAdd(z5, Vector256.Create(0.541196100f), tmp10); // mm256_F_0_5411 - Vector256 z4 = Vector256_.MultiplyAdd(z5, Vector256.Create(1.306562965f), tmp12); // mm256_F_1_3065 + Vector256 z2 = Vector256_.MultiplyAddEstimate(Vector256.Create(0.541196100f), tmp10, z5); // mm256_F_0_5411 + Vector256 z4 = Vector256_.MultiplyAddEstimate(Vector256.Create(1.306562965f), tmp12, z5); // mm256_F_1_3065 Vector256 z3 = tmp11 * mm256_F_0_7071; Vector256 z11 = tmp7 + z3; @@ -122,8 +122,8 @@ internal static partial class FloatingPointDCT z5 = (z10 + z12) * Vector256.Create(1.847759065f); // mm256_F_1_8477 - tmp10 = Vector256_.MultiplyAdd(z5, z12, Vector256.Create(-1.082392200f)); // mm256_F_n1_0823 - tmp12 = Vector256_.MultiplyAdd(z5, z10, Vector256.Create(-2.613125930f)); // mm256_F_n2_6131 + tmp10 = Vector256_.MultiplyAddEstimate(z12, Vector256.Create(-1.082392200f), z5); // mm256_F_n1_0823 + tmp12 = Vector256_.MultiplyAddEstimate(z10, Vector256.Create(-2.613125930f), z5); // mm256_F_n2_6131 tmp6 = tmp12 - tmp7; tmp5 = tmp11 - tmp6; diff --git a/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs b/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs index b8234ff3e..b4d32f709 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs @@ -136,10 +136,10 @@ internal static class ScaledFloatingPointDCT (z4 * FP32_2_562915447); // Save results to the top left 4x4 subregion - block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp2) * 0.5F) + normalizationValue, 0, maxValue)); - block[(ctr * 8) + 3] = MathF.Round(Numerics.Clamp(((tmp10 - tmp2) * 0.5F) + normalizationValue, 0, maxValue)); - block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp12 + tmp0) * 0.5F) + normalizationValue, 0, maxValue)); - block[(ctr * 8) + 2] = MathF.Round(Numerics.Clamp(((tmp12 - tmp0) * 0.5F) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 0] = Numerics.Clamp(((tmp10 + tmp2) * 0.5F) + normalizationValue, 0, maxValue); + block[(ctr * 8) + 3] = Numerics.Clamp(((tmp10 - tmp2) * 0.5F) + normalizationValue, 0, maxValue); + block[(ctr * 8) + 1] = Numerics.Clamp(((tmp12 + tmp0) * 0.5F) + normalizationValue, 0, maxValue); + block[(ctr * 8) + 2] = Numerics.Clamp(((tmp12 - tmp0) * 0.5F) + normalizationValue, 0, maxValue); } } @@ -199,8 +199,8 @@ internal static class ScaledFloatingPointDCT (block[ctr + (8 * 1) + 2] * FP32_3_624509785); // Save results to the top left 2x2 subregion - block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp0) * 0.25F) + normalizationValue, 0, maxValue)); - block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp10 - tmp0) * 0.25F) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 0] = Numerics.Clamp(((tmp10 + tmp0) * 0.25F) + normalizationValue, 0, maxValue); + block[(ctr * 8) + 1] = Numerics.Clamp(((tmp10 - tmp0) * 0.25F) + normalizationValue, 0, maxValue); } } @@ -213,6 +213,6 @@ internal static class ScaledFloatingPointDCT /// Output range normalization value, 1/2 of the . /// Maximum value of the output range. public static float TransformIDCT_1x1(float dc, float dequantizer, float normalizationValue, float maxValue) - => MathF.Round(Numerics.Clamp((dc * dequantizer) + normalizationValue, 0, maxValue)); + => Numerics.Clamp((dc * dequantizer) + normalizationValue, 0, maxValue); } #pragma warning restore IDE0078 diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 7825955e7..ae26914e8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -208,11 +208,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker."); } - this.InitExifProfile(); - this.InitIccProfile(); - this.InitIptcProfile(); - this.InitXmpProfile(); - this.InitDerivedMetadataProperties(); + this.InitializeMetadataProfiles(); _ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile); @@ -232,11 +228,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker."); } - this.InitExifProfile(); - this.InitIccProfile(); - this.InitIptcProfile(); - this.InitXmpProfile(); - this.InitDerivedMetadataProperties(); + this.InitializeMetadataProfiles(); Size pixelSize = this.Frame.PixelSize; return new ImageInfo(new Size(pixelSize.Width, pixelSize.Height), this.Metadata); @@ -385,7 +377,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF2: - this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly); + if (!this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly)) + { + return; + } + break; case JpegConstants.Markers.SOF9: @@ -398,7 +394,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData this.scanDecoder.ResetInterval = this.resetInterval.Value; } - this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly); + if (!this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly)) + { + return; + } + break; case JpegConstants.Markers.SOF5: @@ -429,7 +429,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData } // It's highly unlikely that APPn related data will be found after the SOS marker - // We should have gathered everything we need by now. + // So we can stop parsing here and return the metadata we have parsed so far, instead + // of trying to parse any APPn markers after the SOS marker and risking running out of + // memory or other exceptions. return; case JpegConstants.Markers.DHT: @@ -519,6 +521,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData fileMarker = FindNextFileMarker(stream); } + if (!metadataOnly && this.Frame is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found."); + } + this.Metadata.GetJpegMetadata().Interleaved = this.Frame.Interleaved; } @@ -696,6 +703,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData { this.Metadata.IccProfile = profile; } + else + { + throw new InvalidIccProfileException("Invalid ICC profile."); + } } } @@ -766,6 +777,18 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData return 0; } + /// + /// Initializes decoded metadata profiles using the configured ancillary segment handling policy. + /// + private void InitializeMetadataProfiles() + { + this.ExecuteAncillarySegmentAction(this.InitExifProfile); + this.ExecuteAncillarySegmentAction(this.InitIccProfile); + this.ExecuteAncillarySegmentAction(this.InitIptcProfile); + this.ExecuteAncillarySegmentAction(this.InitXmpProfile); + this.ExecuteAncillarySegmentAction(this.InitDerivedMetadataProperties); + } + /// /// Extends the profile with additional data. /// @@ -789,7 +812,16 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData // We can only decode JFif identifiers. // Some images contain multiple JFIF markers (Issue 1932) so we check to see // if it's already been read. - if (remaining < JFifMarker.Length || (!this.jFif.Equals(default))) + if (remaining < JFifMarker.Length) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length."); + + // Skip the application header length + stream.Skip(remaining); + return; + } + + if (!this.jFif.Equals(default)) { // Skip the application header length stream.Skip(remaining); @@ -799,7 +831,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData Span temp = stackalloc byte[2 * 16 * 4]; stream.Read(temp, 0, JFifMarker.Length); - _ = JFifMarker.TryParse(temp, out this.jFif); + if (!JFifMarker.TryParse(temp, out this.jFif)) + { + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App0 marker."); + } remaining -= JFifMarker.Length; @@ -808,7 +843,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData { if (stream.Position + remaining >= stream.Length) { - JpegThrowHelper.ThrowInvalidImageContentException("Bad App0 Marker length."); + this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length."); + stream.Skip(remaining); + return; } stream.Skip(remaining); @@ -824,7 +861,16 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData { const int exifMarkerLength = 6; const int xmpMarkerLength = 29; - if (remaining < exifMarkerLength || this.skipMetadata) + if (remaining < exifMarkerLength) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); + + // Skip the application header length. + stream.Skip(remaining); + return; + } + + if (this.skipMetadata) { // Skip the application header length. stream.Skip(remaining); @@ -833,7 +879,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData if (stream.Position + remaining >= stream.Length) { - JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); + this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); + stream.Skip(remaining); + return; } Span temp = stackalloc byte[2 * 16 * 4]; @@ -864,8 +912,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData if (ProfileResolver.IsProfile(temp, ProfileResolver.XmpMarker[..exifMarkerLength])) { const int remainingXmpMarkerBytes = xmpMarkerLength - exifMarkerLength; - if (remaining < remainingXmpMarkerBytes || this.skipMetadata) + if (remaining < remainingXmpMarkerBytes) { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); + // Skip the application header length. stream.Skip(remaining); return; @@ -891,6 +941,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData remaining = 0; } + else + { + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App1 marker."); + } } // Skip over any remaining bytes of this header. @@ -906,7 +960,15 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData { // Length is 14 though we only need to check 12. const int icclength = 14; - if (remaining < icclength || this.skipMetadata) + if (remaining < icclength) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App2 Marker length."); + + stream.Skip(remaining); + return; + } + + if (this.skipMetadata) { stream.Skip(remaining); return; @@ -947,7 +1009,15 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData /// The remaining bytes in the segment block. private void ProcessApp13Marker(BufferedReadStream stream, int remaining) { - if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.skipMetadata) + if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length) + { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App13 Marker length."); + + stream.Skip(remaining); + return; + } + + if (this.skipMetadata) { stream.Skip(remaining); return; @@ -965,6 +1035,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData { if (!ProfileResolver.IsProfile(blockDataSpan[..4], ProfileResolver.AdobeImageResourceBlockMarker)) { + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker."); return; } @@ -981,6 +1052,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray(); break; } + + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker."); + return; } else { @@ -990,6 +1064,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData if (blockDataSpan.Length < dataStartIdx + resourceDataSize) { // Not enough data or the resource data size is wrong. + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App13 marker."); break; } @@ -1084,6 +1159,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData const int markerLength = AdobeMarker.Length; if (remaining < markerLength) { + this.ThrowOrIgnoreNonStrictSegmentError("Bad App14 Marker length."); + // Skip the application header length stream.Skip(remaining); return; @@ -1098,6 +1175,10 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData { this.hasAdobeMarker = true; } + else + { + this.ThrowOrIgnoreNonStrictSegmentError("Invalid App14 marker."); + } if (remaining > 0) { @@ -1207,13 +1288,19 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData /// The current frame marker. /// The jpeg decoding component type. /// Whether to parse metadata only. - private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly) + private bool ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly) { if (this.Frame != null) { - if (metadataOnly) + // If we have found the SOS marker, we can stop parsing as we have all + // the information we need to decode the image. + // It's possible that there are APPn related markers after the SOS marker, + // but it's highly unlikely and we would be better off stopping parsing + // and decoding the image instead of trying to parse those APPn markers + // and risking running out of memory or other exceptions. + if (this.hasSOSMarker) { - return; + return false; } JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported."); @@ -1346,6 +1433,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData this.Frame.Init(maxH, maxV); this.scanDecoder.InjectFrameData(this.Frame, this); } + + return true; } /// @@ -1545,7 +1634,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables); } - this.InitIccProfile(); + this.ExecuteAncillarySegmentAction(this.InitIccProfile); _ = this.Options.TryGetIccProfileForColorConversion(this.Metadata.IccProfile, out IccProfile profile); this.scanDecoder.ParseEntropyCodedData(selectorsCount, profile); } diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs index 989eadda7..5451d3d46 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -155,6 +155,11 @@ internal sealed class PbmDecoderCore : ImageDecoderCore ThrowPrematureEof(); } + if (this.maxPixelValue <= 0 || this.maxPixelValue >= 65536) + { + throw new InvalidImageContentException("Invalid max pixel value."); + } + if (this.maxPixelValue > 255) { this.componentType = PbmComponentType.Short; diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index 91f79c815..0038b4134 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -147,7 +147,13 @@ internal readonly struct FrameControl /// The data to parse. /// The parsed fcTL. public static FrameControl Parse(ReadOnlySpan data) - => new( + { + if (data.Length < Size) + { + PngThrowHelper.ThrowInvalidImageContentException("The frame control chunk does not contain enough data!"); + } + + return new( sequenceNumber: BinaryPrimitives.ReadUInt32BigEndian(data[..4]), width: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]), height: BinaryPrimitives.ReadUInt32BigEndian(data[8..12]), @@ -157,4 +163,5 @@ internal readonly struct FrameControl delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]), disposalMode: (FrameDisposalMode)(data[24] + 1), blendMode: (FrameBlendMode)data[25]); + } } diff --git a/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs index 8af0ac8ca..cce4d16b8 100644 --- a/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs +++ b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs @@ -46,6 +46,11 @@ internal readonly struct PngPhysical /// The parsed PhysicalChunkData. public static PngPhysical Parse(ReadOnlySpan data) { + if (data.Length < 9) + { + PngThrowHelper.ThrowInvalidImageContentException("pHYs chunk is too short"); + } + uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]); uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4)); byte unit = data[8]; diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 57c202918..2cd88d6bb 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -140,98 +140,7 @@ internal static class AverageFilter /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, uint bytesPerPixel, out int sum) - { - DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); - DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); - - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); - sum = 0; - - // Average(x) = Raw(x) - floor((Raw(x-bpp)+Prior(x))/2) - resultBaseRef = (byte)FilterType.Average; - - nuint x = 0; - for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) - { - byte scan = Unsafe.Add(ref scanBaseRef, x); - byte above = Unsafe.Add(ref prevBaseRef, x); - ++x; - ref byte res = ref Unsafe.Add(ref resultBaseRef, x); - res = (byte)(scan - (above >> 1)); - sum += Numerics.Abs(unchecked((sbyte)res)); - } - - if (Avx2.IsSupported) - { - Vector256 zero = Vector256.Zero; - Vector256 sumAccumulator = Vector256.Zero; - Vector256 allBitsSet = Avx2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); - - for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector256.Count; xLeft += (uint)Vector256.Count) - { - Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); - Vector256 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); - Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); - - Vector256 avg = Avx2.Xor(Avx2.Average(Avx2.Xor(left, allBitsSet), Avx2.Xor(above, allBitsSet)), allBitsSet); - Vector256 res = Avx2.Subtract(scan, avg); - - Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type - x += (uint)Vector256.Count; - - sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); - } - - sum += Numerics.EvenReduceSum(sumAccumulator); - } - else if (Sse2.IsSupported) - { - Vector128 zero = Vector128.Zero; - Vector128 sumAccumulator = Vector128.Zero; - Vector128 allBitsSet = Sse2.CompareEqual(sumAccumulator, sumAccumulator).AsByte(); - - for (nuint xLeft = x - bytesPerPixel; (int)x <= scanline.Length - Vector128.Count; xLeft += (uint)Vector128.Count) - { - Vector128 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); - Vector128 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); - Vector128 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); - - Vector128 avg = Sse2.Xor(Sse2.Average(Sse2.Xor(left, allBitsSet), Sse2.Xor(above, allBitsSet)), allBitsSet); - Vector128 res = Sse2.Subtract(scan, avg); - - Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type - x += (uint)Vector128.Count; - - Vector128 absRes; - if (Ssse3.IsSupported) - { - absRes = Ssse3.Abs(res.AsSByte()); - } - else - { - Vector128 mask = Sse2.CompareGreaterThan(zero.AsSByte(), res.AsSByte()); - absRes = Sse2.Xor(Sse2.Add(res.AsSByte(), mask), mask).AsByte(); - } - - sumAccumulator = Sse2.Add(sumAccumulator, Sse2.SumAbsoluteDifferences(absRes, zero).AsInt32()); - } - - sum += Numerics.EvenReduceSum(sumAccumulator); - } - - for (nuint xLeft = x - bytesPerPixel; x < (uint)scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) - { - byte scan = Unsafe.Add(ref scanBaseRef, x); - byte left = Unsafe.Add(ref scanBaseRef, xLeft); - byte above = Unsafe.Add(ref prevBaseRef, x); - ++x; - ref byte res = ref Unsafe.Add(ref resultBaseRef, x); - res = (byte)(scan - Average(left, above)); - sum += Numerics.Abs(unchecked((sbyte)res)); - } - } + => PngFilterEncoder.Encode(scanline, previousScanline, result, bytesPerPixel, out sum); /// /// Calculates the average value of two bytes diff --git a/src/ImageSharp/Formats/Png/Filters/IPngFilterOperator.cs b/src/ImageSharp/Formats/Png/Filters/IPngFilterOperator.cs new file mode 100644 index 000000000..bae1ac477 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Filters/IPngFilterOperator.cs @@ -0,0 +1,424 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Png.Filters; + +/// +/// Defines the scalar and SIMD mappings used by the shared PNG filter traversal. +/// +internal interface IPngFilterOperator +{ + /// + /// Gets the filter type written to the leading result byte. + /// + public static abstract FilterType Type { get; } + + /// + /// Gets a value indicating whether the predictor reads the left component. + /// + public static abstract bool UsesLeft { get; } + + /// + /// Gets a value indicating whether the predictor reads the above component. + /// + public static abstract bool UsesAbove { get; } + + /// + /// Gets a value indicating whether the predictor reads the upper-left component. + /// + public static abstract bool UsesUpperLeft { get; } + + /// + /// Filters one byte from its PNG neighborhood. + /// + /// The component being filtered. + /// The corresponding component in the preceding pixel. + /// The corresponding component in the preceding scanline. + /// The preceding component in the preceding scanline. + /// The filtered residual. + public static abstract byte Invoke(byte scan, byte left, byte above, byte upperLeft); + + /// + /// Filters sixteen byte lanes from their PNG neighborhoods. + /// + /// The components being filtered. + /// The corresponding components in the preceding pixels. + /// The corresponding components in the preceding scanline. + /// The preceding components in the preceding scanline. + /// The filtered residuals. + public static abstract Vector128 Invoke(Vector128 scan, Vector128 left, Vector128 above, Vector128 upperLeft); + + /// + /// Filters thirty-two byte lanes from their PNG neighborhoods. + /// + /// The components being filtered. + /// The corresponding components in the preceding pixels. + /// The corresponding components in the preceding scanline. + /// The preceding components in the preceding scanline. + /// The filtered residuals. + public static abstract Vector256 Invoke(Vector256 scan, Vector256 left, Vector256 above, Vector256 upperLeft); + + /// + /// Filters sixty-four byte lanes from their PNG neighborhoods. + /// + /// The components being filtered. + /// The corresponding components in the preceding pixels. + /// The corresponding components in the preceding scanline. + /// The preceding components in the preceding scanline. + /// The filtered residuals. + public static abstract Vector512 Invoke(Vector512 scan, Vector512 left, Vector512 above, Vector512 upperLeft); +} + +/// +/// Maps each component to its difference from the corresponding component in the preceding pixel. +/// +internal readonly struct SubFilterOperator : IPngFilterOperator +{ + /// + public static FilterType Type => FilterType.Sub; + + /// + public static bool UsesLeft => true; + + /// + public static bool UsesAbove => false; + + /// + public static bool UsesUpperLeft => false; + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static byte Invoke(byte scan, byte left, byte above, byte upperLeft) => (byte)(scan - left); + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector128 Invoke(Vector128 scan, Vector128 left, Vector128 above, Vector128 upperLeft) + => scan - left; + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector256 Invoke(Vector256 scan, Vector256 left, Vector256 above, Vector256 upperLeft) + => scan - left; + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector512 Invoke(Vector512 scan, Vector512 left, Vector512 above, Vector512 upperLeft) + => scan - left; +} + +/// +/// Maps each component to its difference from the component directly above it. +/// +internal readonly struct UpFilterOperator : IPngFilterOperator +{ + /// + public static FilterType Type => FilterType.Up; + + /// + public static bool UsesLeft => false; + + /// + public static bool UsesAbove => true; + + /// + public static bool UsesUpperLeft => false; + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static byte Invoke(byte scan, byte left, byte above, byte upperLeft) => (byte)(scan - above); + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector128 Invoke(Vector128 scan, Vector128 left, Vector128 above, Vector128 upperLeft) + => scan - above; + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector256 Invoke(Vector256 scan, Vector256 left, Vector256 above, Vector256 upperLeft) + => scan - above; + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector512 Invoke(Vector512 scan, Vector512 left, Vector512 above, Vector512 upperLeft) + => scan - above; +} + +/// +/// Maps each component to its difference from the truncated average of its left and above neighbors. +/// +internal readonly struct AverageFilterOperator : IPngFilterOperator +{ + /// + public static FilterType Type => FilterType.Average; + + /// + public static bool UsesLeft => true; + + /// + public static bool UsesAbove => true; + + /// + public static bool UsesUpperLeft => false; + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static byte Invoke(byte scan, byte left, byte above, byte upperLeft) + => (byte)(scan - ((left + above) >> 1)); + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector128 Invoke(Vector128 scan, Vector128 left, Vector128 above, Vector128 upperLeft) + { + Vector128 average; + + if (Sse2.IsSupported) + { + // PAVG rounds upward. Complementing both inputs and the result converts + // that rounding into the floor((left + above) / 2) required by PNG. + average = ~Sse2.Average(~left, ~above); + } + else if (AdvSimd.IsSupported) + { + // ARM's halving add truncates directly and therefore needs no correction. + average = AdvSimd.FusedAddHalving(left, above); + } + else + { + // Portable 128-bit backends use the carry-free average identity. Shared + // bits supply the integer part while differing bits supply half the remainder. + average = (left & above) + Vector128.ShiftRightLogical(left ^ above, 1); + } + + return scan - average; + } + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector256 Invoke(Vector256 scan, Vector256 left, Vector256 above, Vector256 upperLeft) + + // VPAVGB rounds (left + above) / 2 upward. Complementing both inputs and + // the result changes that to the truncated average required by PNG. + => scan - ~Avx2.Average(~left, ~above); + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector512 Invoke(Vector512 scan, Vector512 left, Vector512 above, Vector512 upperLeft) + + // AVX-512BW retains VPAVGB's upward rounding, so use the same complement + // identity as AVX2 to obtain floor((left + above) / 2) in every byte lane. + => scan - ~Avx512BW.Average(~left, ~above); +} + +/// +/// Maps each component to its difference from the nearest Paeth neighbor. +/// +internal readonly struct PaethFilterOperator : IPngFilterOperator +{ + /// + public static FilterType Type => FilterType.Paeth; + + /// + public static bool UsesLeft => true; + + /// + public static bool UsesAbove => true; + + /// + public static bool UsesUpperLeft => true; + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static byte Invoke(byte scan, byte left, byte above, byte upperLeft) + { + int p = left + above - upperLeft; + int distanceLeft = Numerics.Abs(p - left); + int distanceAbove = Numerics.Abs(p - above); + int distanceUpperLeft = Numerics.Abs(p - upperLeft); + + // PNG resolves equal distances in left, above, upper-left order. + byte predictor = distanceLeft <= distanceAbove && distanceLeft <= distanceUpperLeft ? left : distanceAbove <= distanceUpperLeft ? above : upperLeft; + + return (byte)(scan - predictor); + } + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector128 Invoke(Vector128 scan, Vector128 left, Vector128 above, Vector128 upperLeft) + { + Vector128 predictor = Predict(left, above, upperLeft); + return scan - predictor; + } + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector256 Invoke(Vector256 scan, Vector256 left, Vector256 above, Vector256 upperLeft) + { + Vector256 predictor = Predict(left, above, upperLeft); + return scan - predictor; + } + + /// + [MethodImpl(InliningOptions.AlwaysInline)] + public static Vector512 Invoke(Vector512 scan, Vector512 left, Vector512 above, Vector512 upperLeft) + { + Vector512 predictor = Predict(left, above, upperLeft); + return scan - predictor; + } + + /// + /// Selects the nearest Paeth neighbor for sixteen independent byte lanes. + /// + /// The reconstructed component immediately before each current component. + /// The reconstructed component immediately above each current component. + /// The reconstructed component diagonally above and before each current component. + /// The selected Paeth predictor for each byte lane. + [MethodImpl(InliningOptions.AlwaysInline)] + private static Vector128 Predict(Vector128 left, Vector128 above, Vector128 upperLeft) + { + // For p = left + above - upperLeft, the Paeth distances simplify to: + // distanceLeft = |above - upperLeft| + // distanceAbove = |left - upperLeft| + // Computing both unsigned subtraction directions and OR-ing them obtains + // each absolute difference without widening the byte lanes. + Vector128 aboveMinusUpper = Vector128_.SubtractSaturate(above, upperLeft); + Vector128 leftMinusUpper = Vector128_.SubtractSaturate(left, upperLeft); + Vector128 distanceLeft = Vector128_.SubtractSaturate(upperLeft, above) | aboveMinusUpper; + Vector128 distanceAbove = Vector128_.SubtractSaturate(upperLeft, left) | leftMinusUpper; + + return SelectPredictor(left, above, upperLeft, aboveMinusUpper, leftMinusUpper, distanceLeft, distanceAbove); + } + + /// + /// Selects the nearest Paeth neighbor for thirty-two independent byte lanes. + /// + /// The reconstructed component immediately before each current component. + /// The reconstructed component immediately above each current component. + /// The reconstructed component diagonally above and before each current component. + /// The selected Paeth predictor for each byte lane. + [MethodImpl(InliningOptions.AlwaysInline)] + private static Vector256 Predict(Vector256 left, Vector256 above, Vector256 upperLeft) + { + // Apply the same Paeth identities as the 128-bit path to thirty-two lanes. + // Saturating subtraction in both directions forms the absolute differences + // without widening, preserving one predictor result per source byte. + Vector256 aboveMinusUpper = Vector256_.SubtractSaturate(above, upperLeft); + Vector256 leftMinusUpper = Vector256_.SubtractSaturate(left, upperLeft); + Vector256 distanceLeft = Vector256_.SubtractSaturate(upperLeft, above) | aboveMinusUpper; + Vector256 distanceAbove = Vector256_.SubtractSaturate(upperLeft, left) | leftMinusUpper; + + return SelectPredictor(left, above, upperLeft, aboveMinusUpper, leftMinusUpper, distanceLeft, distanceAbove); + } + + /// + /// Selects the nearest Paeth neighbor for sixty-four independent byte lanes. + /// + /// The reconstructed component immediately before each current component. + /// The reconstructed component immediately above each current component. + /// The reconstructed component diagonally above and before each current component. + /// The selected Paeth predictor for each byte lane. + [MethodImpl(InliningOptions.AlwaysInline)] + private static Vector512 Predict(Vector512 left, Vector512 above, Vector512 upperLeft) + { + // Apply the same byte-lane Paeth identities to sixty-four AVX-512BW lanes. + // No cross-lane operation is required because every component has its own + // left, above, and upper-left inputs at the matching vector index. + Vector512 aboveMinusUpper = Vector512_.SubtractSaturate(above, upperLeft); + Vector512 leftMinusUpper = Vector512_.SubtractSaturate(left, upperLeft); + Vector512 distanceLeft = Vector512_.SubtractSaturate(upperLeft, above) | aboveMinusUpper; + Vector512 distanceAbove = Vector512_.SubtractSaturate(upperLeft, left) | leftMinusUpper; + + return SelectPredictor(left, above, upperLeft, aboveMinusUpper, leftMinusUpper, distanceLeft, distanceAbove); + } + + /// + /// Applies Paeth distance and tie-breaking rules to sixteen lanes. + /// + /// The left-neighbor candidates. + /// The above-neighbor candidates. + /// The upper-left-neighbor candidates. + /// The saturated differences from above to upper-left. + /// The saturated differences from left to upper-left. + /// The Paeth distances for the left candidates. + /// The Paeth distances for the above candidates. + /// The selected Paeth predictor for each byte lane. + [MethodImpl(InliningOptions.AlwaysInline)] + private static Vector128 SelectPredictor(Vector128 left, Vector128 above, Vector128 upperLeft, Vector128 aboveMinusUpper, Vector128 leftMinusUpper, Vector128 distanceLeft, Vector128 distanceAbove) + { + Vector128 sameDirection = Vector128.Equals(Vector128.Equals(aboveMinusUpper, Vector128.Zero), Vector128.Equals(leftMinusUpper, Vector128.Zero)); + + // If left and above lie on the same side of upper-left, distanceUpper is + // their summed distance and cannot beat either neighbor; the all-bits mask + // excludes upper-left. On opposite sides, that distance is the absolute + // difference between distanceLeft and distanceAbove. + Vector128 distanceUpper = sameDirection | Vector128_.SubtractSaturate(distanceAbove, distanceLeft) | Vector128_.SubtractSaturate(distanceLeft, distanceAbove); + + // Equality selects above before upper-left, implementing PNG's second tie rule. + Vector128 minimumAboveUpper = Vector128.Min(distanceUpper, distanceAbove); + Vector128 aboveOrUpper = Vector128.ConditionalSelect(Vector128.Equals(minimumAboveUpper, distanceAbove), above, upperLeft); + + // Applying the left comparison last preserves PNG's left-first tie rule. + return Vector128.ConditionalSelect(Vector128.Equals(Vector128.Min(minimumAboveUpper, distanceLeft), distanceLeft), left, aboveOrUpper); + } + + /// + /// Applies Paeth distance and tie-breaking rules to thirty-two lanes. + /// + /// The left-neighbor candidates. + /// The above-neighbor candidates. + /// The upper-left-neighbor candidates. + /// The saturated differences from above to upper-left. + /// The saturated differences from left to upper-left. + /// The Paeth distances for the left candidates. + /// The Paeth distances for the above candidates. + /// The selected Paeth predictor for each byte lane. + [MethodImpl(InliningOptions.AlwaysInline)] + private static Vector256 SelectPredictor(Vector256 left, Vector256 above, Vector256 upperLeft, Vector256 aboveMinusUpper, Vector256 leftMinusUpper, Vector256 distanceLeft, Vector256 distanceAbove) + { + Vector256 sameDirection = Vector256.Equals(Vector256.Equals(aboveMinusUpper, Vector256.Zero), Vector256.Equals(leftMinusUpper, Vector256.Zero)); + + // Exclude upper-left when its distance is the non-minimal sum; otherwise + // compute its distance as the absolute difference of the two known distances. + Vector256 distanceUpper = sameDirection | Vector256_.SubtractSaturate(distanceAbove, distanceLeft) | Vector256_.SubtractSaturate(distanceLeft, distanceAbove); + + // Select above on equality, then select left on equality to preserve PNG's + // required left, above, upper-left tie order in every byte lane. + Vector256 minimumAboveUpper = Vector256.Min(distanceUpper, distanceAbove); + Vector256 aboveOrUpper = Vector256.ConditionalSelect(Vector256.Equals(minimumAboveUpper, distanceAbove), above, upperLeft); + + return Vector256.ConditionalSelect(Vector256.Equals(Vector256.Min(minimumAboveUpper, distanceLeft), distanceLeft), left, aboveOrUpper); + } + + /// + /// Applies Paeth distance and tie-breaking rules to sixty-four lanes. + /// + /// The left-neighbor candidates. + /// The above-neighbor candidates. + /// The upper-left-neighbor candidates. + /// The saturated differences from above to upper-left. + /// The saturated differences from left to upper-left. + /// The Paeth distances for the left candidates. + /// The Paeth distances for the above candidates. + /// The selected Paeth predictor for each byte lane. + [MethodImpl(InliningOptions.AlwaysInline)] + private static Vector512 SelectPredictor(Vector512 left, Vector512 above, Vector512 upperLeft, Vector512 aboveMinusUpper, Vector512 leftMinusUpper, Vector512 distanceLeft, Vector512 distanceAbove) + { + Vector512 sameDirection = Vector512.Equals(Vector512.Equals(aboveMinusUpper, Vector512.Zero), Vector512.Equals(leftMinusUpper, Vector512.Zero)); + + // Exclude upper-left when its distance is the non-minimal sum; otherwise + // compute its distance as the absolute difference of the two known distances. + Vector512 distanceUpper = sameDirection | Vector512_.SubtractSaturate(distanceAbove, distanceLeft) | Vector512_.SubtractSaturate(distanceLeft, distanceAbove); + + // Select above on equality, then select left on equality to preserve PNG's + // required left, above, upper-left tie order in every byte lane. + Vector512 minimumAboveUpper = Vector512.Min(distanceUpper, distanceAbove); + Vector512 aboveOrUpper = Vector512.ConditionalSelect(Vector512.Equals(minimumAboveUpper, distanceAbove), above, upperLeft); + + return Vector512.ConditionalSelect(Vector512.Equals(Vector512.Min(minimumAboveUpper, distanceLeft), distanceLeft), left, aboveOrUpper); + } +} diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 59c903c1d..eaeaaf173 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -193,86 +192,7 @@ internal static class PaethFilter /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum) - { - DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); - DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); - - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); - sum = 0; - - // Paeth(x) = Raw(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x - bpp)) - resultBaseRef = (byte)FilterType.Paeth; - - nuint x = 0; - for (; x < (uint)bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) - { - byte scan = Unsafe.Add(ref scanBaseRef, x); - byte above = Unsafe.Add(ref prevBaseRef, x); - ++x; - ref byte res = ref Unsafe.Add(ref resultBaseRef, x); - res = (byte)(scan - PaethPredictor(0, above, 0)); - sum += Numerics.Abs(unchecked((sbyte)res)); - } - - if (Avx2.IsSupported) - { - Vector256 zero = Vector256.Zero; - Vector256 sumAccumulator = Vector256.Zero; - - for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector256.Count; xLeft += (uint)Vector256.Count) - { - Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); - Vector256 left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); - Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); - Vector256 upperLeft = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, xLeft)); - - Vector256 res = Avx2.Subtract(scan, PaethPredictor(left, above, upperLeft)); - Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type - x += (uint)Vector256.Count; - - sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); - } - - sum += Numerics.EvenReduceSum(sumAccumulator); - } - else if (Vector.IsHardwareAccelerated) - { - Vector sumAccumulator = Vector.Zero; - - for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= scanline.Length - Vector.Count; xLeft += (uint)Vector.Count) - { - Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); - Vector left = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); - Vector above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); - Vector upperLeft = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, xLeft)); - - Vector res = scan - PaethPredictor(left, above, upperLeft); - Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type - x += (uint)Vector.Count; - - Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); - } - - for (int i = 0; i < Vector.Count; i++) - { - sum += (int)sumAccumulator[i]; - } - } - - for (nuint xLeft = x - (uint)bytesPerPixel; (int)x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) - { - byte scan = Unsafe.Add(ref scanBaseRef, x); - byte left = Unsafe.Add(ref scanBaseRef, xLeft); - byte above = Unsafe.Add(ref prevBaseRef, x); - byte upperLeft = Unsafe.Add(ref prevBaseRef, xLeft); - ++x; - ref byte res = ref Unsafe.Add(ref resultBaseRef, x); - res = (byte)(scan - PaethPredictor(left, above, upperLeft)); - sum += Numerics.Abs(unchecked((sbyte)res)); - } - } + => PngFilterEncoder.Encode(scanline, previousScanline, result, (uint)bytesPerPixel, out sum); /// /// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses @@ -304,70 +224,4 @@ internal static class PaethFilter return upperLeft; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 PaethPredictor(Vector256 left, Vector256 above, Vector256 upleft) - { - Vector256 zero = Vector256.Zero; - - // Here, we refactor pa = abs(p - left) = abs(left + above - upleft - left) - // to pa = abs(above - upleft). Same deal for pb. - // Using saturated subtraction, if the result is negative, the output is zero. - // If we subtract in both directions and `or` the results, only one can be - // non-zero, so we end up with the absolute value. - Vector256 sac = Avx2.SubtractSaturate(above, upleft); - Vector256 sbc = Avx2.SubtractSaturate(left, upleft); - Vector256 pa = Avx2.Or(Avx2.SubtractSaturate(upleft, above), sac); - Vector256 pb = Avx2.Or(Avx2.SubtractSaturate(upleft, left), sbc); - - // pc = abs(left + above - upleft - upleft), or abs(left - upleft + above - upleft). - // We've already calculated left - upleft and above - upleft in `sac` and `sbc`. - // If they are both negative or both positive, the absolute value of their - // sum can't possibly be less than `pa` or `pb`, so we'll never use the value. - // We make a mask that sets the value to 255 if they either both got - // saturated to zero or both didn't. Then we calculate the absolute value - // of their difference using saturated subtract and `or`, same as before, - // keeping the value only where the mask isn't set. - Vector256 pm = Avx2.CompareEqual(Avx2.CompareEqual(sac, zero), Avx2.CompareEqual(sbc, zero)); - Vector256 pc = Avx2.Or(pm, Avx2.Or(Avx2.SubtractSaturate(pb, pa), Avx2.SubtractSaturate(pa, pb))); - - // Finally, blend the values together. We start with `upleft` and overwrite on - // tied values so that the `left`, `above`, `upleft` precedence is preserved. - Vector256 minbc = Avx2.Min(pc, pb); - Vector256 resbc = Avx2.BlendVariable(upleft, above, Avx2.CompareEqual(minbc, pb)); - return Avx2.BlendVariable(resbc, left, Avx2.CompareEqual(Avx2.Min(minbc, pa), pa)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector PaethPredictor(Vector left, Vector above, Vector upperLeft) - { - Vector.Widen(left, out Vector a1, out Vector a2); - Vector.Widen(above, out Vector b1, out Vector b2); - Vector.Widen(upperLeft, out Vector c1, out Vector c2); - - Vector p1 = PaethPredictor(Vector.AsVectorInt16(a1), Vector.AsVectorInt16(b1), Vector.AsVectorInt16(c1)); - Vector p2 = PaethPredictor(Vector.AsVectorInt16(a2), Vector.AsVectorInt16(b2), Vector.AsVectorInt16(c2)); - return Vector.AsVectorByte(Vector.Narrow(p1, p2)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector PaethPredictor(Vector left, Vector above, Vector upperLeft) - { - Vector p = left + above - upperLeft; - Vector pa = Vector.Abs(p - left); - Vector pb = Vector.Abs(p - above); - Vector pc = Vector.Abs(p - upperLeft); - - Vector pa_pb = Vector.LessThanOrEqual(pa, pb); - Vector pa_pc = Vector.LessThanOrEqual(pa, pc); - Vector pb_pc = Vector.LessThanOrEqual(pb, pc); - - return Vector.ConditionalSelect( - condition: Vector.BitwiseAnd(pa_pb, pa_pc), - left: left, - right: Vector.ConditionalSelect( - condition: pb_pc, - left: above, - right: upperLeft)); - } } diff --git a/src/ImageSharp/Formats/Png/Filters/PngFilterEncoder.cs b/src/ImageSharp/Formats/Png/Filters/PngFilterEncoder.cs new file mode 100644 index 000000000..3e4386d93 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Filters/PngFilterEncoder.cs @@ -0,0 +1,187 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Png.Filters; + +/// +/// Applies PNG filter operators while accumulating the absolute signed residuals used for filter selection. +/// +internal static class PngFilterEncoder +{ + /// + /// Maps a scanline through a filter operator, writes the residuals, and reduces their total variance. + /// + /// The PNG predictor selected for this closed traversal. + /// The scanline to encode. + /// The preceding scanline. + /// The destination including its leading filter byte. + /// The distance to the corresponding component in the preceding pixel. + /// The sum of the absolute signed residuals. + // Inlining closes every static interface call over TOperator. The JIT can then remove + // source loads ignored by simpler predictors and specialize the active register widths. + [MethodImpl(InliningOptions.AlwaysInline)] + public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, uint bytesPerPixel, out int sum) + where TOperator : struct, IPngFilterOperator + { + DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); + + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte previousBaseRef = ref MemoryMarshal.GetReference(previousScanline); + ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); + + resultBaseRef = (byte)TOperator.Type; + sum = 0; + + nuint x = 0; + + // Components in the first pixel have no left or upper-left neighbor. Supplying + // zeroes expresses the PNG boundary rule directly through the same predictor. + for (; x < bytesPerPixel; x++) + { + byte above = TOperator.UsesAbove ? Unsafe.Add(ref previousBaseRef, x) : (byte)0; + + byte filtered = TOperator.Invoke(Unsafe.Add(ref scanBaseRef, x), 0, above, 0); + + Unsafe.Add(ref resultBaseRef, x + 1) = filtered; + sum += Numerics.Abs(unchecked((sbyte)filtered)); + } + + Vector128 sum128 = Vector128.Zero; + + // A single 512-bit register does not amortize folding its SAD accumulator. + // Leave short rows to the narrower paths, which have lower fixed reduction cost. + if (Avx512BW.IsSupported && scanline.Length - (int)x >= Vector512.Count * 2) + { + Vector512 sum512 = Vector512.Zero; + int oneRegisterFromEnd = scanline.Length - Vector512.Count; + + for (nuint xLeft = x - bytesPerPixel; (int)x <= oneRegisterFromEnd; xLeft += (uint)Vector512.Count) + { + // Each byte lane represents one independently filtered component. The + // four input vectors retain the scan/left/above/upper-left PNG layout. + // Operator usage flags are constants after generic specialization, so + // unused predictors do not retain even fault-preserving probe loads. + Vector512 left = TOperator.UsesLeft ? Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)) : default; + Vector512 above = TOperator.UsesAbove ? Unsafe.As>(ref Unsafe.Add(ref previousBaseRef, x)) : default; + Vector512 upperLeft = TOperator.UsesUpperLeft ? Unsafe.As>(ref Unsafe.Add(ref previousBaseRef, xLeft)) : default; + + Vector512 filtered = TOperator.Invoke(Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)), left, above, upperLeft); + + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = filtered; + x += (uint)Vector512.Count; + + // Vector512.Abs lowers to VPABSB under the surrounding AVX-512BW guard. + // Reinterpreting the signed result preserves -128's 0x80 bit pattern as + // the unsigned magnitude 128 consumed by VPSADBW. + Vector512 absolute = Vector512.Abs(filtered.AsSByte()).AsByte(); + sum512 += Avx512BW.SumAbsoluteDifferences(absolute, Vector512.Zero).AsUInt32(); + } + + // Fold only widths that processed data. Short rows therefore avoid both + // wide accumulator initialization and an otherwise empty reduction. + Vector256 folded512 = sum512.GetLower() + sum512.GetUpper(); + sum128 += folded512.GetLower() + folded512.GetUpper(); + } + + if (Avx2.IsSupported) + { + Vector256 sum256 = Vector256.Zero; + int oneRegisterFromEnd = scanline.Length - Vector256.Count; + + for (nuint xLeft = x - bytesPerPixel; (int)x <= oneRegisterFromEnd; xLeft += (uint)Vector256.Count) + { + // Thirty-two byte lanes preserve the same scan/left/above/upper-left + // correspondence as the 512-bit path. Closed operator flags remove + // unused loads when the predictor does not consume that neighbor. + Vector256 left = TOperator.UsesLeft ? Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)) : default; + Vector256 above = TOperator.UsesAbove ? Unsafe.As>(ref Unsafe.Add(ref previousBaseRef, x)) : default; + Vector256 upperLeft = TOperator.UsesUpperLeft ? Unsafe.As>(ref Unsafe.Add(ref previousBaseRef, xLeft)) : default; + + Vector256 filtered = TOperator.Invoke(Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)), left, above, upperLeft); + + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = filtered; + x += (uint)Vector256.Count; + + // Vector256.Abs lowers to VPABSB under the surrounding AVX2 guard. + // Reinterpreting the signed result preserves -128's 0x80 bit pattern as + // the unsigned magnitude 128 consumed by VPSADBW. + Vector256 absolute = Vector256.Abs(filtered.AsSByte()).AsByte(); + sum256 += Avx2.SumAbsoluteDifferences(absolute, Vector256.Zero).AsUInt32(); + } + + // Fold both 128-bit halves into the shared four-lane accumulator. + sum128 += sum256.GetLower() + sum256.GetUpper(); + } + + if (Vector128.IsHardwareAccelerated) + { + int oneRegisterFromEnd = scanline.Length - Vector128.Count; + + for (nuint xLeft = x - bytesPerPixel; (int)x <= oneRegisterFromEnd; xLeft += (uint)Vector128.Count) + { + // The final vector width handles sixteen more components with the + // same lane-wise neighborhood layout before the scalar remainder. + Vector128 left = TOperator.UsesLeft ? Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)) : default; + Vector128 above = TOperator.UsesAbove ? Unsafe.As>(ref Unsafe.Add(ref previousBaseRef, x)) : default; + Vector128 upperLeft = TOperator.UsesUpperLeft ? Unsafe.As>(ref Unsafe.Add(ref previousBaseRef, xLeft)) : default; + + Vector128 filtered = TOperator.Invoke(Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)), left, above, upperLeft); + + Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = filtered; + x += (uint)Vector128.Count; + + // AccumulateAbsolute selects the available x86 or portable widening + // reduction while preserving the same unsigned 32-bit partial sums. + sum128 = AccumulateAbsolute(sum128, filtered); + } + } + + // Reduce the four partial lanes before adding individually scored tail bytes. + sum += unchecked((int)Vector128.Sum(sum128)); + + for (nuint xLeft = x - bytesPerPixel; x < (uint)scanline.Length; xLeft++, x++) + { + byte left = TOperator.UsesLeft ? Unsafe.Add(ref scanBaseRef, xLeft) : (byte)0; + byte above = TOperator.UsesAbove ? Unsafe.Add(ref previousBaseRef, x) : (byte)0; + byte upperLeft = TOperator.UsesUpperLeft ? Unsafe.Add(ref previousBaseRef, xLeft) : (byte)0; + + byte filtered = TOperator.Invoke(Unsafe.Add(ref scanBaseRef, x), left, above, upperLeft); + + Unsafe.Add(ref resultBaseRef, x + 1) = filtered; + sum += Numerics.Abs(unchecked((sbyte)filtered)); + } + } + + /// + /// Accumulates the absolute signed values in a 128-bit residual vector. + /// + /// The four-lane unsigned accumulator. + /// The sixteen filtered byte residuals. + /// The updated accumulator. + [MethodImpl(InliningOptions.AlwaysInline)] + private static Vector128 AccumulateAbsolute(Vector128 accumulator, Vector128 residuals) + { + // The generic absolute-value intrinsic selects PABSB where available and + // preserves -128's 0x80 bit pattern as the unsigned magnitude 128. + Vector128 absolute = Vector128.Abs(residuals.AsSByte()).AsByte(); + + if (Sse2.IsSupported) + { + return accumulator + Sse2.SumAbsoluteDifferences(absolute, Vector128.Zero).AsUInt32(); + } + + (Vector128 lower16, Vector128 upper16) = Vector128.Widen(absolute); + (Vector128 lower0, Vector128 lower1) = Vector128.Widen(lower16); + (Vector128 upper0, Vector128 upper1) = Vector128.Widen(upper16); + + // Four widening additions keep every byte contribution in a 32-bit lane, + // matching the x86 accumulator's overflow behavior without scalar reduction. + return accumulator + lower0 + lower1 + upper0 + upper1; + } +} diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index 1af4a3b72..946b59022 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -103,84 +102,16 @@ internal static class SubFilter } /// - /// Encodes a scanline with the sup filter applied. + /// Encodes a scanline with the sub filter applied. /// /// The scanline to encode. /// The filtered scanline result. /// The bytes per pixel. /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Encode(ReadOnlySpan scanline, ReadOnlySpan result, int bytesPerPixel, out int sum) - { - DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); - - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); - sum = 0; - - // Sub(x) = Raw(x) - Raw(x-bpp) - resultBaseRef = (byte)FilterType.Sub; - - nuint x = 0; - for (; x < (uint)bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) - { - byte scan = Unsafe.Add(ref scanBaseRef, x); - ++x; - ref byte res = ref Unsafe.Add(ref resultBaseRef, x); - res = scan; - sum += Numerics.Abs(unchecked((sbyte)res)); - } - - if (Avx2.IsSupported) - { - Vector256 zero = Vector256.Zero; - Vector256 sumAccumulator = Vector256.Zero; - - for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= (scanline.Length - Vector256.Count); xLeft += (uint)Vector256.Count) - { - Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); - Vector256 prev = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); - - Vector256 res = Avx2.Subtract(scan, prev); - Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type - x += (uint)Vector256.Count; - - sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); - } + public static void Encode(ReadOnlySpan scanline, Span result, int bytesPerPixel, out int sum) - sum += Numerics.EvenReduceSum(sumAccumulator); - } - else - if (Vector.IsHardwareAccelerated) - { - Vector sumAccumulator = Vector.Zero; - - for (nuint xLeft = x - (uint)bytesPerPixel; (int)x <= (scanline.Length - Vector.Count); xLeft += (uint)Vector.Count) - { - Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); - Vector prev = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, xLeft)); - - Vector res = scan - prev; - Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type - x += (uint)Vector.Count; - - Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); - } - - for (int i = 0; i < Vector.Count; i++) - { - sum += (int)sumAccumulator[i]; - } - } - - for (nuint xLeft = x - (uint)bytesPerPixel; x < (uint)scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) - { - byte scan = Unsafe.Add(ref scanBaseRef, x); - byte prev = Unsafe.Add(ref scanBaseRef, xLeft); - ++x; - ref byte res = ref Unsafe.Add(ref resultBaseRef, x); - res = (byte)(scan - prev); - sum += Numerics.Abs(unchecked((sbyte)res)); - } - } + // Sub does not consume an above neighbor, so the shared traversal may alias + // the unused previous-row argument to the current row without an extra buffer. + => PngFilterEncoder.Encode(scanline, scanline, result, (uint)bytesPerPixel, out sum); } diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index 405d89e6c..d64a1ea51 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -1,12 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Png.Filters; @@ -27,128 +23,8 @@ internal static class UpFilter { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); - if (Avx2.IsSupported) - { - DecodeAvx2(scanline, previousScanline); - } - else if (Sse2.IsSupported) - { - DecodeSse2(scanline, previousScanline); - } - else if (AdvSimd.IsSupported) - { - DecodeArm(scanline, previousScanline); - } - else - { - DecodeScalar(scanline, previousScanline); - } - } - - private static void DecodeAvx2(Span scanline, Span previousScanline) - { - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - - // Up(x) + Prior(x) - int rb = scanline.Length; - nuint offset = 1; - while (rb >= Vector256.Count) - { - ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); - Vector256 prior = Unsafe.As>(ref scanRef); - Vector256 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); - - Unsafe.As>(ref scanRef) = Avx2.Add(up, prior); - - offset += (uint)Vector256.Count; - rb -= Vector256.Count; - } - - // Handle left over. - for (nuint i = offset; i < (uint)scanline.Length; i++) - { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); - byte above = Unsafe.Add(ref prevBaseRef, offset); - scan = (byte)(scan + above); - offset++; - } - } - - private static void DecodeSse2(Span scanline, Span previousScanline) - { - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - - // Up(x) + Prior(x) - int rb = scanline.Length; - nuint offset = 1; - while (rb >= Vector128.Count) - { - ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); - Vector128 prior = Unsafe.As>(ref scanRef); - Vector128 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); - - Unsafe.As>(ref scanRef) = Sse2.Add(up, prior); - - offset += (uint)Vector128.Count; - rb -= Vector128.Count; - } - - // Handle left over. - for (nuint i = offset; i < (uint)scanline.Length; i++) - { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); - byte above = Unsafe.Add(ref prevBaseRef, offset); - scan = (byte)(scan + above); - offset++; - } - } - - private static void DecodeArm(Span scanline, Span previousScanline) - { - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - - // Up(x) + Prior(x) - int rb = scanline.Length; - nuint offset = 1; - const int bytesPerBatch = 16; - while (rb >= bytesPerBatch) - { - ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); - Vector128 prior = Unsafe.As>(ref scanRef); - Vector128 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); - - Unsafe.As>(ref scanRef) = AdvSimd.Add(prior, up); - - offset += bytesPerBatch; - rb -= bytesPerBatch; - } - - // Handle left over. - for (nuint i = offset; i < (uint)scanline.Length; i++) - { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); - byte above = Unsafe.Add(ref prevBaseRef, offset); - scan = (byte)(scan + above); - offset++; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void DecodeScalar(Span scanline, Span previousScanline) - { - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - - // Up(x) + Prior(x) - for (nuint x = 1; x < (uint)scanline.Length; x++) - { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + above); - } + // The leading filter byte is metadata; every remaining byte is the modulo-256 sum of Raw(x) and Prior(x). + TensorPrimitives_.Add(scanline[1..], previousScanline[1..], scanline[1..]); } /// @@ -160,69 +36,8 @@ internal static class UpFilter /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, out int sum) - { - DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); - DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result)); - - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - ref byte resultBaseRef = ref MemoryMarshal.GetReference(result); - sum = 0; - - // Up(x) = Raw(x) - Prior(x) - resultBaseRef = (byte)FilterType.Up; - - nuint x = 0; - if (Avx2.IsSupported) - { - Vector256 zero = Vector256.Zero; - Vector256 sumAccumulator = Vector256.Zero; - - for (; (int)x <= scanline.Length - Vector256.Count;) - { - Vector256 scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); - Vector256 above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); - - Vector256 res = Avx2.Subtract(scan, above); - Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type - x += (uint)Vector256.Count; - - sumAccumulator = Avx2.Add(sumAccumulator, Avx2.SumAbsoluteDifferences(Avx2.Abs(res.AsSByte()), zero).AsInt32()); - } - - sum += Numerics.EvenReduceSum(sumAccumulator); - } - else if (Vector.IsHardwareAccelerated) - { - Vector sumAccumulator = Vector.Zero; - - for (; (int)x <= scanline.Length - Vector.Count;) - { - Vector scan = Unsafe.As>(ref Unsafe.Add(ref scanBaseRef, x)); - Vector above = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, x)); - - Vector res = scan - above; - Unsafe.As>(ref Unsafe.Add(ref resultBaseRef, x + 1)) = res; // +1 to skip filter type - x += (uint)Vector.Count; - - Numerics.Accumulate(ref sumAccumulator, Vector.AsVectorByte(Vector.Abs(Vector.AsVectorSByte(res)))); - } - - for (int i = 0; i < Vector.Count; i++) - { - sum += (int)sumAccumulator[i]; - } - } - - for (; x < (uint)scanline.Length; /* Note: ++x happens in the body to avoid one add operation */) - { - byte scan = Unsafe.Add(ref scanBaseRef, x); - byte above = Unsafe.Add(ref prevBaseRef, x); - ++x; - ref byte res = ref Unsafe.Add(ref resultBaseRef, x); - res = (byte)(scan - above); - sum += Numerics.Abs(unchecked((sbyte)res)); - } - } + // Up never reads a left neighbor, so bytesPerPixel is deliberately zero in + // the shared traversal and no unsigned left offset is evaluated. + => PngFilterEncoder.Encode(scanline, previousScanline, result, 0, out sum); } diff --git a/src/ImageSharp/Formats/Png/PngCgbiProcessor.cs b/src/ImageSharp/Formats/Png/PngCgbiProcessor.cs new file mode 100644 index 000000000..3addbb3c0 --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngCgbiProcessor.cs @@ -0,0 +1,319 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.PixelFormats; +using static SixLabors.ImageSharp.SimdUtils; + +namespace SixLabors.ImageSharp.Formats.Png; + +/// +/// Reverses the pixel mangling applied by Apple's CgBI PNG variant. CgBI files +/// (emitted by pngcrush -iphone) swap channel order from RGB(A) to BGR(A) +/// and premultiply RGB samples by alpha. This converts a defiltered scanline back +/// to standard PNG semantics in place so the existing scanline processors can +/// consume it unchanged. CgBI is only emitted for 8-bit truecolor (with or +/// without alpha); other color types are left alone. +/// +/// +/// See https://theapplewiki.com/wiki/PNG_CgBI_Format +/// +internal static class PngCgbiProcessor +{ + // Per-pixel byte indices that swap CgBI's BGRA layout to Rgba32's RGBA. + // MMShuffle3012 expands to [2, 1, 0, 3] per 4-byte pixel; the same 64-byte + // sequence seeds all three shuffle masks (Vector128/256 take a leading slice). + private static readonly byte[] BgraToRgbaShuffleBytes = BuildShuffleBytes(); + + private static readonly Vector128 BgraToRgbaShuffle128 = Vector128.Create(new ReadOnlySpan(BgraToRgbaShuffleBytes, 0, Vector128.Count)); + + private static readonly Vector256 BgraToRgbaShuffle256 = Vector256.Create(new ReadOnlySpan(BgraToRgbaShuffleBytes, 0, Vector256.Count)); + + private static readonly Vector512 BgraToRgbaShuffle512 = Vector512.Create(BgraToRgbaShuffleBytes); + + /// + /// Applies the inverse of Apple's CgBI pixel mangling to a defiltered scanline in place. + /// + /// The configuration used by the Rgb24 R/B swap. + /// The defiltered pixel bytes (without the leading filter byte). + /// The PNG color type from IHDR. + public static void ApplyTransform(Configuration configuration, Span scanline, PngColorType colorType) + { + if (colorType == PngColorType.RgbWithAlpha) + { + Span pixels = MemoryMarshal.Cast(scanline); + int i = 0; + + if (Vector512.IsHardwareAccelerated && pixels.Length >= Vector512.Count) + { + i = ApplyTransformVector512(scanline, pixels.Length); + } + + if (Vector256.IsHardwareAccelerated && Avx2.IsSupported && (pixels.Length - i) >= Vector256.Count) + { + i = ApplyTransformVector256(scanline, i, pixels.Length); + } + + if (Vector128.IsHardwareAccelerated && (pixels.Length - i) >= Vector128.Count) + { + i = ApplyTransformVector128(scanline, i, pixels.Length); + } + + for (; i < pixels.Length; i++) + { + ref Rgba32 pixel = ref pixels[i]; + pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A); + UndoPremultiplicationScalar(ref pixel); + } + } + else if (colorType == PngColorType.Rgb) + { + // No alpha channel, so just swap R and B using built in SIMD-optimized pixel operations. + Span target = MemoryMarshal.Cast(scanline); + PixelOperations.Instance.FromBgr24Bytes(configuration, scanline, target, target.Length); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UndoPremultiplicationScalar(ref Rgba32 pixel) + { + byte a = pixel.A; + if (a is 0 or byte.MaxValue) + { + return; + } + + // Reverse: c' = c * a / 255 => c = round(c' * 255 / a) + int half = a >> 1; + byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a); + byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a); + byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a); + pixel = new Rgba32(r, g, b, a); + } + + private static int ApplyTransformVector512(Span scanline, int pixelCount) + { + ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline); + int i = 0; + + // Indices stay within their own 4-byte pixel, so the per-pixel pattern + // is also valid under the per-128-bit-lane vpshufb that ShuffleNative + // selects on AVX-512BW hosts. + Vector512 shuffleMask = BgraToRgbaShuffle512; + + Vector512 zero = Vector512.Zero; + Vector512 one = Vector512.One; + Vector512 byteMax = Vector512.Create((int)byte.MaxValue); + + for (; i <= pixelCount - Vector512.Count; i += Vector512.Count) + { + ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf()); + Vector512 bgra = Unsafe.ReadUnaligned>(ref blockRef); + Vector512 rgba = Vector512_.ShuffleNative(bgra, shuffleMask); + Vector512 packed = rgba.AsInt32(); + Vector512 alpha = Vector512.ShiftRightLogical(packed, 24); + + // Fully transparent and fully opaque pixels are identity cases for + // unpremultiplication. Masking them keeps the scalar behavior and lets + // safeAlpha avoid dividing by zero for alpha == 0. + Vector512 partialMask = ~(Vector512.Equals(alpha, zero) | Vector512.Equals(alpha, byteMax)); + + Vector512 r = packed & byteMax; + Vector512 g = Vector512.ShiftRightLogical(packed, 8) & byteMax; + Vector512 b = Vector512.ShiftRightLogical(packed, 16) & byteMax; + + Vector512 safeAlpha = Vector512.ConditionalSelect(partialMask, alpha, one); + Vector512 halfAlpha = Vector512.ShiftRightLogical(safeAlpha, 1); + Vector512 safeAlphaF = Vector512.ConvertToSingle(safeAlpha); + + // ConvertToInt32 truncates toward zero (cvttps2dq / fcvtzs); since + // every quotient here is non-negative, that matches the scalar + // ((c * 255) + (a >> 1)) / a integer-division floor. + Vector512 unpremultipliedR = Vector512.Min( + byteMax, + Vector512.ConvertToInt32(Vector512.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)); + + Vector512 unpremultipliedG = Vector512.Min( + byteMax, + Vector512.ConvertToInt32(Vector512.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)); + + Vector512 unpremultipliedB = Vector512.Min( + byteMax, + Vector512.ConvertToInt32(Vector512.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)); + + // ConditionalSelect applies the expensive unpremultiply only to pixels + // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the + // shuffled channel values exactly as the scalar path does. + Vector512 finalR = Vector512.ConditionalSelect(partialMask, unpremultipliedR, r); + Vector512 finalG = Vector512.ConditionalSelect(partialMask, unpremultipliedG, g); + Vector512 finalB = Vector512.ConditionalSelect(partialMask, unpremultipliedB, b); + + // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so + // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3 + // recreates the in-memory RGBA bytes for the unaligned store. + Vector512 result = + finalR | + Vector512.ShiftLeft(finalG, 8) | + Vector512.ShiftLeft(finalB, 16) | + Vector512.ShiftLeft(alpha, 24); + + Unsafe.WriteUnaligned(ref blockRef, result.AsByte()); + } + + return i; + } + + private static int ApplyTransformVector256(Span scanline, int startPixel, int pixelCount) + { + ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline); + int i = startPixel; + + // vpshufb is 128-bit lane-local and uses only the low 4 bits of each + // index, so the same per-pixel [2,1,0,3] pattern in both lanes keeps + // every byte inside its own lane. + Vector256 shuffleMask = BgraToRgbaShuffle256; + + Vector256 zero = Vector256.Zero; + Vector256 one = Vector256.One; + Vector256 byteMax = Vector256.Create((int)byte.MaxValue); + + for (; i <= pixelCount - Vector256.Count; i += Vector256.Count) + { + ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf()); + Vector256 bgra = Unsafe.ReadUnaligned>(ref blockRef); + Vector256 rgba = Vector256_.ShufflePerLane(bgra, shuffleMask); + Vector256 packed = rgba.AsInt32(); + Vector256 alpha = Vector256.ShiftRightLogical(packed, 24); + + // Fully transparent and fully opaque pixels are identity cases for + // unpremultiplication. Masking them keeps the scalar behavior and lets + // safeAlpha avoid dividing by zero for alpha == 0. + Vector256 partialMask = ~(Vector256.Equals(alpha, zero) | Vector256.Equals(alpha, byteMax)); + + Vector256 r = packed & byteMax; + Vector256 g = Vector256.ShiftRightLogical(packed, 8) & byteMax; + Vector256 b = Vector256.ShiftRightLogical(packed, 16) & byteMax; + + Vector256 safeAlpha = Vector256.ConditionalSelect(partialMask, alpha, one); + Vector256 halfAlpha = Vector256.ShiftRightLogical(safeAlpha, 1); + Vector256 safeAlphaF = Vector256.ConvertToSingle(safeAlpha); + + // ConvertToInt32 truncates toward zero (cvttps2dq / fcvtzs); since + // every quotient here is non-negative, that matches the scalar + // ((c * 255) + (a >> 1)) / a integer-division floor. + Vector256 unpremultipliedR = Vector256.Min( + byteMax, + Vector256.ConvertToInt32(Vector256.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)); + + Vector256 unpremultipliedG = Vector256.Min( + byteMax, + Vector256.ConvertToInt32(Vector256.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)); + + Vector256 unpremultipliedB = Vector256.Min( + byteMax, + Vector256.ConvertToInt32(Vector256.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)); + + // ConditionalSelect applies the expensive unpremultiply only to pixels + // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the + // shuffled channel values exactly as the scalar path does. + Vector256 finalR = Vector256.ConditionalSelect(partialMask, unpremultipliedR, r); + Vector256 finalG = Vector256.ConditionalSelect(partialMask, unpremultipliedG, g); + Vector256 finalB = Vector256.ConditionalSelect(partialMask, unpremultipliedB, b); + + // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so + // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3 + // recreates the in-memory RGBA bytes for the unaligned store. + Vector256 result = + finalR | + Vector256.ShiftLeft(finalG, 8) | + Vector256.ShiftLeft(finalB, 16) | + Vector256.ShiftLeft(alpha, 24); + + Unsafe.WriteUnaligned(ref blockRef, result.AsByte()); + } + + return i; + } + + private static int ApplyTransformVector128(Span scanline, int startPixel, int pixelCount) + { + ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline); + int i = startPixel; + + Vector128 shuffleMask = BgraToRgbaShuffle128; + + Vector128 zero = Vector128.Zero; + Vector128 one = Vector128.One; + Vector128 byteMax = Vector128.Create((int)byte.MaxValue); + + for (; i <= pixelCount - Vector128.Count; i += Vector128.Count) + { + ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf()); + Vector128 bgra = Unsafe.ReadUnaligned>(ref blockRef); + Vector128 rgba = Vector128_.ShuffleNative(bgra, shuffleMask); + Vector128 packed = rgba.AsInt32(); + Vector128 alpha = Vector128.ShiftRightLogical(packed, 24); + + // Fully transparent and fully opaque pixels are identity cases for + // unpremultiplication. Masking them keeps the scalar behavior and lets + // safeAlpha avoid dividing by zero for alpha == 0. + Vector128 partialMask = ~(Vector128.Equals(alpha, zero) | Vector128.Equals(alpha, byteMax)); + + Vector128 r = packed & byteMax; + Vector128 g = Vector128.ShiftRightLogical(packed, 8) & byteMax; + Vector128 b = Vector128.ShiftRightLogical(packed, 16) & byteMax; + + Vector128 safeAlpha = Vector128.ConditionalSelect(partialMask, alpha, one); + Vector128 halfAlpha = Vector128.ShiftRightLogical(safeAlpha, 1); + Vector128 safeAlphaF = Vector128.ConvertToSingle(safeAlpha); + + // ConvertToInt32 truncates toward zero (cvttps2dq / fcvtzs); since + // every quotient here is non-negative, that matches the scalar + // ((c * 255) + (a >> 1)) / a integer-division floor. + Vector128 unpremultipliedR = Vector128.Min( + byteMax, + Vector128.ConvertToInt32(Vector128.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)); + + Vector128 unpremultipliedG = Vector128.Min( + byteMax, + Vector128.ConvertToInt32(Vector128.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)); + + Vector128 unpremultipliedB = Vector128.Min( + byteMax, + Vector128.ConvertToInt32(Vector128.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)); + + // ConditionalSelect applies the expensive unpremultiply only to pixels + // where alpha is between 1 and 254; alpha 0 and 255 lanes keep the + // shuffled channel values exactly as the scalar path does. + Vector128 finalR = Vector128.ConditionalSelect(partialMask, unpremultipliedR, r); + Vector128 finalG = Vector128.ConditionalSelect(partialMask, unpremultipliedG, g); + Vector128 finalB = Vector128.ConditionalSelect(partialMask, unpremultipliedB, b); + + // Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so + // shifting the unpacked channels back to byte offsets 0, 1, 2, and 3 + // recreates the in-memory RGBA bytes for the unaligned store. + Vector128 result = + finalR | + Vector128.ShiftLeft(finalG, 8) | + Vector128.ShiftLeft(finalB, 16) | + Vector128.ShiftLeft(alpha, 24); + + Unsafe.WriteUnaligned(ref blockRef, result.AsByte()); + } + + return i; + } + + private static byte[] BuildShuffleBytes() + { + byte[] bytes = new byte[Vector512.Count]; + Span span = bytes; + Shuffle.MMShuffleSpan(ref span, Shuffle.MMShuffle3012); + + return bytes; + } +} diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs index 3883986d0..bbf1a88bf 100644 --- a/src/ImageSharp/Formats/Png/PngChunk.cs +++ b/src/ImageSharp/Formats/Png/PngChunk.cs @@ -43,11 +43,11 @@ internal readonly struct PngChunk /// /// The segment handling behavior. public bool IsCritical(SegmentIntegrityHandling handling) - => handling switch + => this.Type switch { - SegmentIntegrityHandling.IgnoreNone => true, - SegmentIntegrityHandling.IgnoreNonCritical => this.Type is PngChunkType.Header or PngChunkType.Palette or PngChunkType.Data or PngChunkType.FrameData, - SegmentIntegrityHandling.IgnoreData => this.Type is PngChunkType.Header or PngChunkType.Palette, - _ => false, + PngChunkType.Header => true, + PngChunkType.Palette => true, + PngChunkType.Data or PngChunkType.FrameData => handling < SegmentIntegrityHandling.IgnoreImageData, + _ => handling < SegmentIntegrityHandling.IgnoreAncillary, }; } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 271474a7e..28f9a989b 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -137,6 +137,13 @@ internal sealed class PngDecoderCore : ImageDecoderCore /// private bool hasImageData; + /// + /// Whether this is an Apple CgBI PNG. CgBI files store IDATs as raw DEFLATE + /// (no zlib header/Adler-32) and pixels as premultiplied BGRA, so they need + /// extra inversion steps to round-trip back to standard PNG semantics. + /// + private bool isCgbi; + /// /// Initializes a new instance of the class. /// @@ -214,7 +221,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; case PngChunkType.FrameData: { - if (frameCount >= this.maxFrames) + if (frameCount > this.maxFrames) { goto EOF; } @@ -275,7 +282,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore previousFrameControl = currentFrameControl; } - if (frameCount >= this.maxFrames) + if (frameCount > this.maxFrames) { goto EOF; } @@ -314,7 +321,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore case PngChunkType.End: goto EOF; case PngChunkType.ProprietaryApple: - PngThrowHelper.ThrowInvalidChunkType("Proprietary Apple PNG detected! This PNG file is not conform to the specification and cannot be decoded."); + this.ReadCgbiChunk(); break; } } @@ -402,7 +409,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; case PngChunkType.FrameControl: ++frameCount; - if (frameCount >= this.maxFrames) + if (frameCount > this.maxFrames) { break; } @@ -411,8 +418,12 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; case PngChunkType.FrameData: - if (frameCount >= this.maxFrames) + if (frameCount > this.maxFrames) { + // Must skip the chunk data even when we've hit maxFrames, because TryReadChunk + // restores the stream position to the start of the fdAT data after CRC validation. + this.SkipChunkDataAndCrc(chunk); + this.SkipRemainingFrameDataChunks(buffer); break; } @@ -428,9 +439,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore InitializeFrameMetadata(framesMetadata, currentFrameControl.Value); - // Skip sequence number - this.currentStream.Skip(4); + // Skip data for this and all remaining FrameData chunks belonging to the same frame + // (comparable to how Decode consumes them via ReadScanlines + ReadNextFrameDataChunk). this.SkipChunkDataAndCrc(chunk); + this.SkipRemainingFrameDataChunks(buffer); break; case PngChunkType.Data: @@ -512,6 +524,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore case PngChunkType.End: goto EOF; + case PngChunkType.ProprietaryApple: + this.ReadCgbiChunk(); + break; + default: if (this.colorMetadataOnly) { @@ -749,7 +765,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore /// The length of the chunk that containing the compressed scanline data. /// The pixel data. /// The png metadata - /// A delegate to get more data from the inner stream for . + /// A delegate to get more data from the inner stream when chunk boundaries are crossed. /// The frame control /// The cancellation token. private void ReadScanlines( @@ -761,14 +777,34 @@ internal sealed class PngDecoderCore : ImageDecoderCore CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - using ZlibInflateStream inflateStream = new(this.currentStream, getData); + // CgBI IDATs wrap a raw DEFLATE payload directly (no zlib CMF/FLG header + // and no Adler-32 trailer); skip the zlib header parser entirely. + if (this.isCgbi) + { + using ChunkedReadStream segmentStream = new(this.currentStream, getData); + segmentStream.SetCurrentSegmentLength(chunkLength); + using DeflateStream cgbiDataStream = new(segmentStream, CompressionMode.Decompress, leaveOpen: true); + this.DecodeFromDeflate(cgbiDataStream, image, pngMetadata, frameControl, cancellationToken); + return; + } + + using ZlibInflateReader inflateStream = new(this.currentStream, getData); if (!inflateStream.AllocateNewBytes(chunkLength, !this.hasImageData)) { return; } - DeflateStream dataStream = inflateStream.CompressedStream!; + this.DecodeFromDeflate(inflateStream.CompressedStream!, image, pngMetadata, frameControl, cancellationToken); + } + private void DecodeFromDeflate( + DeflateStream dataStream, + ImageFrame image, + PngMetadata pngMetadata, + in FrameControl frameControl, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) { this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); @@ -795,18 +831,46 @@ internal sealed class PngDecoderCore : ImageDecoderCore PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel + { + using IMemoryOwner? blendMemory = frameControl.BlendMode == FrameBlendMode.Over + ? this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean) + : null; + + this.ExecuteImageDataSegmentAction(() => this.DecodePixelDataCore( + frameControl, + compressedStream, + imageFrame, + pngMetadata, + blendMemory, + cancellationToken)); + + this.hasImageData = true; + } + + /// + /// Decodes the raw pixel data row by row. + /// + /// The pixel format. + /// The frame control. + /// The compressed pixel data stream. + /// The image frame to decode to. + /// The png metadata. + /// The optional row blending buffer. + /// The cancellation token. + private void DecodePixelDataCore( + FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + IMemoryOwner? blendMemory, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { int currentRow = (int)frameControl.YOffset; int currentRowBytesRead = 0; int height = (int)frameControl.YMax; - IMemoryOwner? blendMemory = null; - Span blendRowBuffer = []; - if (frameControl.BlendMode == FrameBlendMode.Over) - { - blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean); - blendRowBuffer = blendMemory.Memory.Span; - } + Span blendRowBuffer = blendMemory is null ? [] : blendMemory.Memory.Span; while (currentRow < height) { @@ -850,23 +914,22 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; default: - if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll) - { - goto EXIT; - } - PngThrowHelper.ThrowUnknownFilter(); break; } + if (this.isCgbi) + { + PngCgbiProcessor.ApplyTransform(this.configuration, scanSpan[1..], this.pngColorType); + } + this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer); this.SwapScanlineBuffers(); currentRow++; } EXIT: - this.hasImageData = true; - blendMemory?.Dispose(); + return; } /// @@ -885,6 +948,41 @@ internal sealed class PngDecoderCore : ImageDecoderCore PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel + { + using IMemoryOwner? blendMemory = frameControl.BlendMode == FrameBlendMode.Over + ? this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean) + : null; + + FrameControl frameControlCopy = frameControl; + this.ExecuteImageDataSegmentAction(() => this.DecodeInterlacedPixelDataCore( + frameControlCopy, + compressedStream, + imageFrame, + pngMetadata, + blendMemory, + cancellationToken)); + + this.hasImageData = true; + } + + /// + /// Decodes the raw interlaced pixel data row by row. + /// + /// The pixel format. + /// The frame control. + /// The compressed pixel data stream. + /// The current image frame. + /// The png metadata. + /// The optional row blending buffer. + /// The cancellation token. + private void DecodeInterlacedPixelDataCore( + FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + IMemoryOwner? blendMemory, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset; int currentRowBytesRead = 0; @@ -894,13 +992,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore Buffer2D imageBuffer = imageFrame.PixelBuffer; - IMemoryOwner? blendMemory = null; - Span blendRowBuffer = []; - if (frameControl.BlendMode == FrameBlendMode.Over) - { - blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean); - blendRowBuffer = blendMemory.Memory.Span; - } + Span blendRowBuffer = blendMemory is null ? [] : blendMemory.Memory.Span; while (true) { @@ -957,15 +1049,15 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; default: - if (this.segmentIntegrityHandling is SegmentIntegrityHandling.IgnoreData or SegmentIntegrityHandling.IgnoreAll) - { - goto EXIT; - } - PngThrowHelper.ThrowUnknownFilter(); break; } + if (this.isCgbi) + { + PngCgbiProcessor.ApplyTransform(this.configuration, scanSpan[1..], this.pngColorType); + } + Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow); this.ProcessInterlacedDefilteredScanline( frameControl, @@ -997,8 +1089,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore } EXIT: - this.hasImageData = true; - blendMemory?.Dispose(); + return; } /// @@ -1253,6 +1344,12 @@ internal sealed class PngDecoderCore : ImageDecoderCore ReadOnlySpan rgbTable = MemoryMarshal.Cast(palette); Color.FromPixel(rgbTable, colorTable); + // The tRNS chunk must not contain more alpha values than there are palette entries. + if (alpha.Length > colorTable.Length) + { + alpha = alpha.Slice(0, colorTable.Length); + } + if (alpha.Length > 0) { // The alpha chunk may contain as many transparency entries as there are palette entries @@ -1352,6 +1449,37 @@ internal sealed class PngDecoderCore : ImageDecoderCore this.pngColorType = this.header.ColorType; this.Dimensions = new Size(this.header.Width, this.header.Height); + + // Apple's pngcrush emits the CgBI chunk before IHDR, so the header + // compatibility check is deferred until both chunks have been seen. + if (this.isCgbi) + { + ThrowIfInvalidCgbiContent(this.header); + } + } + + /// + /// Marks the image as CgBI and validates a header that has already been read. + /// + private void ReadCgbiChunk() + { + this.isCgbi = true; + + // Although Apple's pngcrush normally writes CgBI before IHDR, accepting the + // reverse order must apply the same compatibility validation. + if (!Equals(this.header, default(PngHeader))) + { + ThrowIfInvalidCgbiContent(this.header); + } + } + + private static void ThrowIfInvalidCgbiContent(in PngHeader header) + { + if (header.BitDepth != 8 || (header.ColorType is not PngColorType.Rgb and not PngColorType.RgbWithAlpha)) + { + PngThrowHelper.ThrowInvalidImageContentException( + $"CgBI is only supported for 8-bit truecolor images. Was bit depth '{header.BitDepth}', color type '{header.ColorType}'."); + } } /// @@ -1402,26 +1530,31 @@ internal sealed class PngDecoderCore : ImageDecoderCore return; } - int zeroIndex = data.IndexOf((byte)0); - if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) + int keywordEnd = data.IndexOf((byte)0); + if (keywordEnd is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) { return; } - byte compressionMethod = data[zeroIndex + 1]; + if (keywordEnd < 0 || keywordEnd + 2 > data.Length) + { + return; // Not enough data for keyword + null + compression method. + } + + byte compressionMethod = data[keywordEnd + 1]; if (compressionMethod != 0) { // Only compression method 0 is supported (zlib datastream with deflate compression). return; } - ReadOnlySpan keywordBytes = data[..zeroIndex]; + ReadOnlySpan keywordBytes = data[..keywordEnd]; if (!TryReadTextKeyword(keywordBytes, out string name)) { return; } - ReadOnlySpan compressedData = data[(zeroIndex + 2)..]; + ReadOnlySpan compressedData = data[(keywordEnd + 2)..]; if (this.TryDecompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed) && !TryReadTextChunkMetadata(baseMetadata, name, uncompressed)) @@ -1834,7 +1967,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore using MemoryStream memoryStreamOutput = new(compressedData.Length); using UnmanagedMemoryStream memoryStreamInput = new(compressedDataBase, compressedData.Length); using BufferedReadStream bufferedStream = new(this.configuration, memoryStreamInput); - using ZlibInflateStream inflateStream = new(bufferedStream); + using ZlibInflateReader inflateStream = new(bufferedStream); Span destUncompressedData = destBuffer.GetSpan(); if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) @@ -1932,6 +2065,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore return; } + if (zeroIndexKeyword < 0 || zeroIndexKeyword + 4 > data.Length) + { + return; // Not enough data for keyword + null + flag + method + language. + } + byte compressionFlag = data[zeroIndexKeyword + 1]; if (compressionFlag is not (0 or 1)) { @@ -1956,6 +2094,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore int translatedKeywordStartIdx = langStartIdx + languageLength + 1; int translatedKeywordLength = data[translatedKeywordStartIdx..].IndexOf((byte)0); + if (translatedKeywordLength < 0) + { + return; + } + string translatedKeyword = PngConstants.TranslatedEncoding.GetString(data.Slice(translatedKeywordStartIdx, translatedKeywordLength)); ReadOnlySpan keywordBytes = data[..zeroIndexKeyword]; @@ -2072,6 +2215,31 @@ internal sealed class PngDecoderCore : ImageDecoderCore return 0; } + /// + /// Skips any remaining chunks belonging to the current frame. + /// This mirrors how is used during decoding: + /// consecutive fdAT chunks are consumed until a non-fdAT chunk is encountered, + /// which is stored in for the next iteration. + /// + /// Temporary buffer. + private void SkipRemainingFrameDataChunks(Span buffer) + { + while (this.TryReadChunk(buffer, out PngChunk chunk)) + { + if (chunk.Type is PngChunkType.FrameData) + { + chunk.Data?.Dispose(); + this.SkipChunkDataAndCrc(chunk); + } + else + { + // Not a FrameData chunk; store it so the next TryReadChunk call returns it. + this.nextChunk = chunk; + return; + } + } + } + /// /// Reads a chunk from the stream. /// diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 8dc70e1d9..80c51ef6e 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -9,8 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Png; internal static class PngThrowHelper { [DoesNotReturn] - public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) - => throw new InvalidImageContentException(errorMessage, innerException); + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); [DoesNotReturn] public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks."); diff --git a/src/ImageSharp/Formats/SegmentIntegrityHandling.cs b/src/ImageSharp/Formats/SegmentIntegrityHandling.cs index 977aee4ad..f16c1bb26 100644 --- a/src/ImageSharp/Formats/SegmentIntegrityHandling.cs +++ b/src/ImageSharp/Formats/SegmentIntegrityHandling.cs @@ -1,30 +1,26 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats; /// -/// Specifies how to handle validation of errors in different segments of encoded image files. +/// Specifies how to handle validation of recoverable errors in ancillary and image data segments. +/// Structural errors that prevent safe decoding remain fatal regardless of the selected mode. /// public enum SegmentIntegrityHandling { /// - /// Do not ignore any errors. + /// Do not ignore any recoverable ancillary or image data segment errors. /// - IgnoreNone, + Strict = 0, /// - /// Ignore errors in non-critical segments of the encoded image. + /// Ignore recoverable errors in ancillary segments, such as optional metadata. /// - IgnoreNonCritical, + IgnoreAncillary = 1, /// - /// Ignore errors in data segments (e.g., image data, metadata). + /// Ignore recoverable errors in image data segments in addition to ancillary segments. /// - IgnoreData, - - /// - /// Ignore errors in all segments. - /// - IgnoreAll + IgnoreImageData = 2, } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs index 4e176f28d..d3b65c537 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -54,7 +54,7 @@ internal sealed class DeflateTiffCompression : TiffBaseDecompressor protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) { long pos = stream.Position; - using (ZlibInflateStream deframeStream = new( + using (ZlibInflateReader deframeStream = new( stream, () => { diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index d818aef1b..16c3cf116 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -34,7 +34,7 @@ internal class BlackIsZero16TiffColor : TiffBaseColorDecoder public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { L16 l16 = TiffUtilities.L16Default; - TPixel color = TPixel.FromScaledVector4(Vector4.Zero); + TPixel color = TPixel.FromUnassociatedScaledVector4(Vector4.Zero); int offset = 0; for (int y = top; y < top + height; y++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs index ac316459d..730091b6a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs @@ -40,7 +40,7 @@ internal class BlackIsZero32FloatTiffColor : TiffBaseColorDecoder : TiffBaseColorDecoder : TiffBaseColorDecoder { int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = value / this.factor; - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs index b422e65ad..0fb7057aa 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs @@ -109,7 +109,7 @@ internal class CieLab16PlanarTiffColor : TiffBasePlanarColorDecoder Rgb -> Vector4 -> TPixel this.colorProfileConverter.Convert(cieLabRow, rgbRow); Rgb.ToScaledVector4(rgbRow, vectorRow); - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } return; @@ -138,7 +138,7 @@ internal class CieLab16PlanarTiffColor : TiffBasePlanarColorDecoder Rgb -> Vector4 -> TPixel this.colorProfileConverter.Convert(cieLabRow, rgbRow); Rgb.ToScaledVector4(rgbRow, vectorRow); - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs index 4455753bf..500914109 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs @@ -105,7 +105,7 @@ internal class CieLab16TiffColor : TiffBaseColorDecoder // Convert CIE Lab -> Rgb -> Vector4 -> TPixel this.colorProfileConverter.Convert(cieLabRow, rgbRow); Rgb.ToScaledVector4(rgbRow, vectorRow); - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } return; @@ -134,7 +134,7 @@ internal class CieLab16TiffColor : TiffBaseColorDecoder // Convert CIE Lab -> Rgb -> Vector4 -> TPixel this.colorProfileConverter.Convert(cieLabRow, rgbRow); Rgb.ToScaledVector4(rgbRow, vectorRow); - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs index 1252f1d3d..b7ad55cb1 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs @@ -35,7 +35,7 @@ internal class CieLab8PlanarTiffColor : TiffBasePlanarColorDecoder(in lab); - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f)); offset++; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs index ce5bed53c..7e71cab82 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs @@ -39,7 +39,7 @@ internal class CieLab8TiffColor : TiffBaseColorDecoder float l = (data[offset] & 0xFF) * 100f * Inv255; CieLab lab = new(l, (sbyte)data[offset + 1], (sbyte)data[offset + 2]); Rgb rgb = ColorProfileConverter.Convert(in lab); - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1f)); offset += 3; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs index 509056267..d8e45c25c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs @@ -66,7 +66,7 @@ internal class CmykTiffColor : TiffBaseColorDecoder Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { - pixelRow[x] = TPixel.FromVector4(new Vector4(data[offset] * Inv255, data[offset + 1] * Inv255, data[offset + 2] * Inv255, 1.0f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(data[offset] * Inv255, data[offset + 1] * Inv255, data[offset + 2] * Inv255, 1.0f)); offset += 3; } @@ -99,7 +99,7 @@ internal class CmykTiffColor : TiffBaseColorDecoder // Convert CMYK -> RGB -> Vector4 -> TPixel this.colorProfileConverter.Convert(cmykRow, rgbRow); Rgb.ToScaledVector4(rgbRow, vectorRow); - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs index d1b519bdd..52315a02b 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs @@ -97,7 +97,7 @@ internal class PaletteTiffColor : TiffBaseColorDecoder Vector4 color = this.vectorPallete[index]; color.W = alpha; - pixelRow[x] = TPixel.FromScaledVector4(color); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(color); // Best-effort palette update for downstream conversions. // This is intentionally "last writer wins" with no per-pixel branch. @@ -163,7 +163,7 @@ internal class PaletteTiffColor : TiffBaseColorDecoder float r = colorMap[rOffset + i] * InvMax; float g = colorMap[gOffset + i] * InvMax; float b = colorMap[bOffset + i] * InvMax; - palette[i] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + palette[i] = TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f)); } return palette; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs index 3a90e8174..f84a77939 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs @@ -30,7 +30,7 @@ internal class Rgb444TiffColor : TiffBaseColorDecoder byte b = (byte)((data[offset] & 0xF0) >> 4); Bgra4444 bgra = new() { PackedValue = ToBgraPackedValue(b, g, r) }; - pixelRow[x] = TPixel.FromScaledVector4(bgra.ToScaledVector4()); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(bgra.ToUnassociatedScaledVector4()); if (x + 1 >= pixelRow.Length) { offset++; @@ -44,7 +44,7 @@ internal class Rgb444TiffColor : TiffBaseColorDecoder offset++; bgra.PackedValue = ToBgraPackedValue(b, g, r); - pixelRow[x + 1] = TPixel.FromScaledVector4(bgra.ToScaledVector4()); + pixelRow[x + 1] = TPixel.FromUnassociatedScaledVector4(bgra.ToUnassociatedScaledVector4()); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs index f9c17eb2f..f48879500 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs @@ -51,7 +51,7 @@ internal class RgbFloat323232TiffColor : TiffBaseColorDecoder float b = BitConverter.ToSingle(buffer); offset += 4; - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f)); } } else @@ -67,7 +67,7 @@ internal class RgbFloat323232TiffColor : TiffBaseColorDecoder float b = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f)); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs index a013abfbd..33f1703ae 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -63,7 +63,7 @@ internal class RgbPlanarTiffColor : TiffBasePlanarColorDecoder float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f)); } rBitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs index 3c205d147..0578cbc58 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs @@ -52,7 +52,7 @@ internal class RgbTiffColor : TiffBaseColorDecoder float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, 1f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f)); } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs index 086aa3b4e..015b31383 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Numerics; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -96,16 +97,21 @@ internal class Rgba16161616TiffColor : TiffBaseColorDecoder Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); int byteCount = pixelRow.Length * 8; - PixelOperations.Instance.FromRgba64Bytes( - this.configuration, - data.Slice(offset, byteCount), - pixelRow, - pixelRow.Length); - if (hasAssociatedAlpha) { - PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorsSpan); - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorsSpan, pixelRow, PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale); + // This little-endian branch can view the interleaved samples as Rgba64. The values are already associated, so + // the Rgba64 operation performs only unpacking and normalization before the destination consumes that representation. + ReadOnlySpan associatedSamples = MemoryMarshal.Cast(data.Slice(offset, byteCount)); + PixelOperations.Instance.ToVector4(this.configuration, associatedSamples, vectorsSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorsSpan, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + } + else + { + PixelOperations.Instance.FromRgba64Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); } offset += byteCount; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs index 26ffbbab9..65bfe7431 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Numerics; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -41,16 +42,22 @@ internal class Rgba8888TiffColor : TiffBaseColorDecoder { Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); int byteCount = pixelRow.Length * 4; - PixelOperations.Instance.FromRgba32Bytes( - this.configuration, - data.Slice(offset, byteCount), - pixelRow, - pixelRow.Length); if (hasAssociatedAlpha) { - PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorsSpan); - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorsSpan, pixelRow, PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale); + // The TIFF samples already contain associated RGB. Reinterpret them as Rgba32 only to normalize and unpack the + // channels; asking that unassociated storage type to associate them would multiply RGB a second time. + ReadOnlySpan associatedSamples = MemoryMarshal.Cast(data.Slice(offset, byteCount)); + PixelOperations.Instance.ToVector4(this.configuration, associatedSamples, vectorsSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorsSpan, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + } + else + { + PixelOperations.Instance.FromRgba32Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); } offset += byteCount; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs index 87a019641..2784caa59 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs @@ -16,16 +16,24 @@ internal class RgbaFloat32323232TiffColor : TiffBaseColorDecoder { private readonly bool isBigEndian; + private readonly TiffExtraSampleType? extraSamplesType; + /// /// Initializes a new instance of the class. /// /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public RgbaFloat32323232TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + /// The alpha representation declared by the TIFF extra samples field. + public RgbaFloat32323232TiffColor(bool isBigEndian, TiffExtraSampleType? extraSamplesType) + { + this.isBigEndian = isBigEndian; + this.extraSamplesType = extraSamplesType; + } /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { int offset = 0; + bool hasAssociatedAlpha = this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; Span buffer = stackalloc byte[4]; for (int y = top; y < top + height; y++) @@ -56,7 +64,10 @@ internal class RgbaFloat32323232TiffColor : TiffBaseColorDecoder float a = BitConverter.ToSingle(buffer); offset += 4; - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, a)); + Vector4 vector = new(r, g, b, a); + pixelRow[x] = hasAssociatedAlpha + ? TPixel.FromAssociatedScaledVector4(vector) + : TPixel.FromUnassociatedScaledVector4(vector); } } else @@ -75,7 +86,10 @@ internal class RgbaFloat32323232TiffColor : TiffBaseColorDecoder float a = BitConverter.ToSingle(data.Slice(offset, 4)); offset += 4; - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(r, g, b, a)); + Vector4 vector = new(r, g, b, a); + pixelRow[x] = hasAssociatedAlpha + ? TPixel.FromAssociatedScaledVector4(vector) + : TPixel.FromUnassociatedScaledVector4(vector); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs index 7a599a06a..c505b58d3 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs @@ -80,11 +80,11 @@ internal class RgbaPlanarTiffColor : TiffBasePlanarColorDecoder Vector4 vector = new(r, g, b, a); if (hasAssociatedAlpha) { - pixelRow[x] = TiffUtilities.UnPremultiply(ref vector); + pixelRow[x] = TPixel.FromAssociatedScaledVector4(vector); } else { - pixelRow[x] = TPixel.FromScaledVector4(vector); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(vector); } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs index 68b59c95a..2c934036a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs @@ -63,16 +63,16 @@ internal class RgbaTiffColor : TiffBaseColorDecoder float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; - float a = bitReader.ReadBits(this.bitsPerSampleB) / this.aFactor; + float a = bitReader.ReadBits(this.bitsPerSampleA) / this.aFactor; Vector4 vector = new(r, g, b, a); if (hasAssociatedAlpha) { - pixelRow[x] = TiffUtilities.UnPremultiply(ref vector); + pixelRow[x] = TPixel.FromAssociatedScaledVector4(vector); } else { - pixelRow[x] = TPixel.FromScaledVector4(vector); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(vector); } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index dd36e912c..bb109de77 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -383,7 +383,7 @@ internal static class TiffColorDecoderFactory && bitsPerSample.Channel0 == 32, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new RgbaFloat32323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + return new RgbaFloat32323232TiffColor(byteOrder == ByteOrder.BigEndian, extraSampleType); case TiffColorType.PaletteColor: DebugGuard.NotNull(colorMap, "colorMap"); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs index 6986f25eb..7fe142d74 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs @@ -40,7 +40,7 @@ internal class WhiteIsZero32FloatTiffColor : TiffBaseColorDecoder : TiffBaseColorDecoder : TiffBaseColorDecoder { int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = 1f - (value / this.factor); - pixelRow[x] = TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); + pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(intensity, intensity, intensity, 1f)); } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 1822cb8d1..e232045be 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -289,7 +289,19 @@ internal class TiffDecoderCore : ImageDecoderCore // We resolve the ICC profile early so that we can use it for color conversion if needed. if (tags.TryGetValue(ExifTag.IccProfile, out IExifValue iccProfileBytes)) { - imageFrameMetaData.IccProfile = new IccProfile(iccProfileBytes.Value); + this.ExecuteAncillarySegmentAction( + () => + { + IccProfile profile = new(iccProfileBytes.Value); + if (profile.CheckIsValid()) + { + imageFrameMetaData.IccProfile = profile; + } + else + { + throw new InvalidIccProfileException("Invalid TIFF ICC profile."); + } + }); } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index ebf407f9b..f7cdd1bfc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -22,8 +22,6 @@ internal static class TiffDecoderMetadataCreator TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); } - ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0]); - if (!ignoreMetadata) { for (int i = 0; i < frames.Count; i++) @@ -43,12 +41,24 @@ internal static class TiffDecoderMetadataCreator } } - return imageMetaData; + // IPTC and XMP are materialized from IFD tags above, so image-level metadata + // must be created after all frame profiles are available. + return Create(byteOrder, isBigTiff, frames[0]); } private static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ImageFrameMetadata rootFrameMetadata) { - ImageMetadata imageMetaData = new(); + // TIFF stores metadata per IFD, while ImageMetadata is the source used by single-frame encoders. + // Mirror the root IFD profiles at image scope so cross-format encoding preserves them. Each profile + // is cloned because image-level and frame-level metadata are independently mutable public APIs. + ImageMetadata imageMetaData = new() + { + ExifProfile = rootFrameMetadata.ExifProfile?.DeepClone(), + IccProfile = rootFrameMetadata.IccProfile?.DeepClone(), + IptcProfile = rootFrameMetadata.IptcProfile?.DeepClone(), + XmpProfile = rootFrameMetadata.XmpProfile?.DeepClone() + }; + SetResolution(imageMetaData, rootFrameMetadata.ExifProfile); TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index a068613bf..7859b2c90 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -15,7 +15,7 @@ public class TiffEncoder : QuantizingImageEncoder /// /// Initializes a new instance of the class. /// - public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree; + public TiffEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree; /// /// Gets the number of bits per pixel. diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index d7508b02e..e5e47166e 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -71,7 +71,7 @@ internal sealed class TiffEncoderCore this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; this.PhotometricInterpretation = encoder.PhotometricInterpretation; - this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree; + this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.BitsPerPixel = encoder.BitsPerPixel; this.HorizontalPredictor = encoder.HorizontalPredictor; diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs index b7d412f3c..3bc4500fd 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs @@ -40,72 +40,67 @@ internal static class TiffUtilities public static TPixel ColorFromRgba64Premultiplied(ushort r, ushort g, ushort b, ushort a) where TPixel : unmanaged, IPixel { + // TIFF 6.0 requires associated RGB to be interpreted as zero when alpha is zero. Enforce that codec contract here + // while leaving the general associated-alpha conversion contract free to preserve hidden RGB for formats such as EXR. if (a == 0) { - return TPixel.FromRgba64(default); + return TPixel.FromAssociatedScaledVector4(Vector4.Zero); } - float scale = 65535f / a; - ushort ur = (ushort)Math.Min(r * scale, 65535); - ushort ug = (ushort)Math.Min(g * scale, 65535); - ushort ub = (ushort)Math.Min(b * scale, 65535); - - return TPixel.FromRgba64(new Rgba64(ur, ug, ub, a)); + // TIFF marks these samples as already associated. Passing that representation directly avoids an unassociate/reassociate + // round trip for associated destinations while unassociated formats perform the required conversion in their pixel contract. + return TPixel.FromAssociatedScaledVector4(new Vector4(r, g, b, a) / ushort.MaxValue); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo24Bit(uint r, uint g, uint b) where TPixel : unmanaged, IPixel - => TPixel.FromScaledVector4(new Vector4(r, g, b, 1f) * Scale24BitVector); + => TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f) * Scale24BitVector); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo24Bit(uint r, uint g, uint b, uint a) where TPixel : unmanaged, IPixel - => TPixel.FromScaledVector4(new Vector4(r, g, b, a) * Scale24Bit); + => TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, a) * Scale24Bit); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo24BitPremultiplied(uint r, uint g, uint b, uint a) where TPixel : unmanaged, IPixel { - Vector4 colorVector = new Vector4(r, g, b, a) * Scale24Bit; - return UnPremultiply(ref colorVector); + // TIFF 6.0 assigns no color to a fully transparent associated sample, even when malformed input stores nonzero RGB. + return a == 0 + ? TPixel.FromAssociatedScaledVector4(Vector4.Zero) + : TPixel.FromAssociatedScaledVector4(new Vector4(r, g, b, a) * Scale24Bit); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo32Bit(uint r, uint g, uint b) where TPixel : unmanaged, IPixel - => TPixel.FromScaledVector4(new Vector4(r, g, b, 1f) * Scale32BitVector); + => TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f) * Scale32BitVector); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo32Bit(uint r, uint g, uint b, uint a) where TPixel : unmanaged, IPixel - => TPixel.FromScaledVector4(new Vector4(r, g, b, a) * Scale32Bit); + => TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, a) * Scale32Bit); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo32BitPremultiplied(uint r, uint g, uint b, uint a) where TPixel : unmanaged, IPixel { - Vector4 vector = new Vector4(r, g, b, a) * Scale32Bit; - return UnPremultiply(ref vector); + // TIFF 6.0 assigns no color to a fully transparent associated sample, even when malformed input stores nonzero RGB. + return a == 0 + ? TPixel.FromAssociatedScaledVector4(Vector4.Zero) + : TPixel.FromAssociatedScaledVector4(new Vector4(r, g, b, a) * Scale32Bit); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo24Bit(uint intensity) where TPixel : unmanaged, IPixel - => TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f) * Scale24BitVector); + => TPixel.FromUnassociatedScaledVector4(new Vector4(intensity, intensity, intensity, 1f) * Scale24BitVector); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo32Bit(uint intensity) where TPixel : unmanaged, IPixel - => TPixel.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1f) * Scale32BitVector); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TPixel UnPremultiply(ref Vector4 vector) - where TPixel : unmanaged, IPixel - { - Numerics.UnPremultiply(ref vector); - return TPixel.FromScaledVector4(vector); - } + => TPixel.FromUnassociatedScaledVector4(new Vector4(intensity, intensity, intensity, 1f) * Scale32BitVector); /// /// Finds the padding needed to round 'valueToRoundUp' to the next integer multiple of subSampling value. diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs index accfea948..bbe9748fc 100644 --- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -361,34 +361,10 @@ internal class AlphaDecoder : IDisposable { HorizontalUnfilter(null, input, dst, width); } - else if (Vector256.IsHardwareAccelerated) - { - ref byte inputRef = ref MemoryMarshal.GetReference(input); - ref byte prevRef = ref MemoryMarshal.GetReference(prev); - ref byte dstRef = ref MemoryMarshal.GetReference(dst); - - nuint i; - int maxPos = width & ~31; - for (i = 0; i < (uint)maxPos; i += 32) - { - Vector256 a0 = Unsafe.As>(ref Unsafe.Add(ref inputRef, i)); - Vector256 b0 = Unsafe.As>(ref Unsafe.Add(ref prevRef, i)); - Vector256 c0 = a0.AsByte() + b0.AsByte(); - ref byte outputRef = ref Unsafe.Add(ref dstRef, i); - Unsafe.As>(ref outputRef) = c0; - } - - for (; i < (uint)width; i++) - { - Unsafe.Add(ref dstRef, i) = (byte)(Unsafe.Add(ref prevRef, i) + Unsafe.Add(ref inputRef, i)); - } - } else { - for (int i = 0; i < width; i++) - { - dst[i] = (byte)(prev[i] + input[i]); - } + // Byte addition intentionally wraps modulo 256, matching the WebP alpha predictor. + TensorPrimitives_.Add(input[..width], prev[..width], dst[..width]); } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index 03bedfe67..eca60dd3f 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -3,9 +3,7 @@ using System.Buffers; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; @@ -333,7 +331,7 @@ internal abstract unsafe class Vp8LHistogram { if (b.IsUsed(0)) { - AddVector(this.Literal, b.Literal, output.Literal, literalSize); + TensorPrimitives_.Add(this.Literal[..literalSize], b.Literal[..literalSize], output.Literal[..literalSize]); } else { @@ -356,7 +354,7 @@ internal abstract unsafe class Vp8LHistogram { if (b.IsUsed(1)) { - AddVector(this.Red, b.Red, output.Red, size); + TensorPrimitives_.Add(this.Red[..size], b.Red[..size], output.Red[..size]); } else { @@ -379,7 +377,7 @@ internal abstract unsafe class Vp8LHistogram { if (b.IsUsed(2)) { - AddVector(this.Blue, b.Blue, output.Blue, size); + TensorPrimitives_.Add(this.Blue[..size], b.Blue[..size], output.Blue[..size]); } else { @@ -402,7 +400,7 @@ internal abstract unsafe class Vp8LHistogram { if (b.IsUsed(3)) { - AddVector(this.Alpha, b.Alpha, output.Alpha, size); + TensorPrimitives_.Add(this.Alpha[..size], b.Alpha[..size], output.Alpha[..size]); } else { @@ -425,7 +423,7 @@ internal abstract unsafe class Vp8LHistogram { if (b.IsUsed(4)) { - AddVector(this.Distance, b.Distance, output.Distance, size); + TensorPrimitives_.Add(this.Distance[..size], b.Distance[..size], output.Distance[..size]); } else { @@ -535,56 +533,6 @@ internal abstract unsafe class Vp8LHistogram return cost; } - - private static void AddVector(Span a, Span b, Span output, int count) - { - DebugGuard.MustBeGreaterThanOrEqualTo(a.Length, count, nameof(a.Length)); - DebugGuard.MustBeGreaterThanOrEqualTo(b.Length, count, nameof(b.Length)); - DebugGuard.MustBeGreaterThanOrEqualTo(output.Length, count, nameof(output.Length)); - - if (Avx2.IsSupported && count >= 32) - { - ref uint aRef = ref MemoryMarshal.GetReference(a); - ref uint bRef = ref MemoryMarshal.GetReference(b); - ref uint outputRef = ref MemoryMarshal.GetReference(output); - - nuint idx = 0; - do - { - // Load values. - Vector256 a0 = Unsafe.As>(ref Unsafe.Add(ref aRef, idx + 0)); - Vector256 a1 = Unsafe.As>(ref Unsafe.Add(ref aRef, idx + 8)); - Vector256 a2 = Unsafe.As>(ref Unsafe.Add(ref aRef, idx + 16)); - Vector256 a3 = Unsafe.As>(ref Unsafe.Add(ref aRef, idx + 24)); - Vector256 b0 = Unsafe.As>(ref Unsafe.Add(ref bRef, idx + 0)); - Vector256 b1 = Unsafe.As>(ref Unsafe.Add(ref bRef, idx + 8)); - Vector256 b2 = Unsafe.As>(ref Unsafe.Add(ref bRef, idx + 16)); - Vector256 b3 = Unsafe.As>(ref Unsafe.Add(ref bRef, idx + 24)); - - // Note we are adding uint32_t's as *signed* int32's (using _mm_add_epi32). But - // that's ok since the histogram values are less than 1<<28 (max picture count). - Unsafe.As>(ref Unsafe.Add(ref outputRef, idx + 0)) = Avx2.Add(a0, b0); - Unsafe.As>(ref Unsafe.Add(ref outputRef, idx + 8)) = Avx2.Add(a1, b1); - Unsafe.As>(ref Unsafe.Add(ref outputRef, idx + 16)) = Avx2.Add(a2, b2); - Unsafe.As>(ref Unsafe.Add(ref outputRef, idx + 24)) = Avx2.Add(a3, b3); - idx += 32; - } - while (idx <= (uint)count - 32); - - int i = (int)idx; - for (; i < count; i++) - { - output[i] = a[i] + b[i]; - } - } - else - { - for (int i = 0; i < count; i++) - { - output[i] = a[i] + b[i]; - } - } - } } internal sealed unsafe class OwnedVp8LHistogram : Vp8LHistogram, IDisposable diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index a23705413..8a8ad823d 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics; using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; @@ -63,9 +64,9 @@ internal class WebpAnimationDecoder : IDisposable private readonly BackgroundColorHandling backgroundColorHandling; /// - /// How to handle validation of errors in different segments of encoded image files. + /// Executes a known ancillary segment parsing action using the configured integrity policy. /// - private readonly SegmentIntegrityHandling segmentIntegrityHandling; + private readonly Action executeAncillarySegmentAction; /// /// Initializes a new instance of the class. @@ -75,21 +76,21 @@ internal class WebpAnimationDecoder : IDisposable /// The maximum number of frames to decode. Inclusive. /// Whether to skip metadata. /// The flag to decide how to handle the background color in the Animation Chunk. - /// How to handle validation of errors in different segments of encoded image files. + /// Executes a known ancillary segment parsing action using the configured integrity policy. public WebpAnimationDecoder( MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, bool skipMetadata, BackgroundColorHandling backgroundColorHandling, - SegmentIntegrityHandling segmentIntegrityHandling) + Action executeAncillarySegmentAction) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; this.maxFrames = maxFrames; this.skipMetadata = skipMetadata; this.backgroundColorHandling = backgroundColorHandling; - this.segmentIntegrityHandling = segmentIntegrityHandling; + this.executeAncillarySegmentAction = executeAncillarySegmentAction; } /// @@ -117,7 +118,6 @@ internal class WebpAnimationDecoder : IDisposable : features.AnimationBackgroundColor!.Value; bool ignoreMetadata = this.skipMetadata; - SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling; Span buffer = stackalloc byte[4]; uint frameCount = 0; int remainingBytes = (int)completeDataSize; @@ -138,13 +138,7 @@ internal class WebpAnimationDecoder : IDisposable case WebpChunkType.Iccp: case WebpChunkType.Xmp: case WebpChunkType.Exif: - WebpChunkParsingUtils.ParseOptionalChunks( - stream, - chunkType, - this.metadata, - ignoreMetadata, - segmentIntegrityHandling, - buffer); + this.ReadOptionalChunk(stream, chunkType, this.metadata, ignoreMetadata); break; default: @@ -195,7 +189,6 @@ internal class WebpAnimationDecoder : IDisposable TPixel backgroundPixel = backgroundColor.ToPixel(); bool ignoreMetadata = this.skipMetadata; - SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling; Span buffer = stackalloc byte[4]; uint frameCount = 0; int remainingBytes = (int)completeDataSize; @@ -222,7 +215,7 @@ internal class WebpAnimationDecoder : IDisposable case WebpChunkType.Iccp: case WebpChunkType.Xmp: case WebpChunkType.Exif: - WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, ignoreMetadata, segmentIntegrityHandling, buffer); + this.ReadOptionalChunk(stream, chunkType, image!.Metadata, ignoreMetadata); break; default: @@ -379,6 +372,29 @@ internal class WebpAnimationDecoder : IDisposable frameMetadata.DisposalMode = frameData.DisposalMethod; } + private void ReadOptionalChunk( + BufferedReadStream stream, + WebpChunkType chunkType, + ImageMetadata imageMetadata, + bool ignoreMetadata) + { + switch (chunkType) + { + case WebpChunkType.Iccp: + + // While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must + // precede the frame data, and we cannot safely skip it without successfully reading its size. + WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata); + break; + case WebpChunkType.Exif: + this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata)); + break; + case WebpChunkType.Xmp: + this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, imageMetadata, ignoreMetadata)); + break; + } + } + /// /// Reads the ALPH chunk data. /// @@ -456,15 +472,21 @@ internal class WebpAnimationDecoder : IDisposable // The destination frame has already been prepopulated with the pixel data from the previous frame // so blending will leave the desired result which takes into consideration restoration to the // background color within the restore area. - PixelBlender blender = - PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + PixelBlender blender = PixelOperations.Instance.GetPixelBlender( + PixelColorBlendingMode.Normal, + PixelAlphaCompositionMode.SrcOver); + + // By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend + // We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending. + using IMemoryOwner workingBufferOwner = imageFrame.Configuration.MemoryAllocator.Allocate(restoreArea.Width * 3); + Span workingBuffer = workingBufferOwner.GetSpan(); for (int y = 0; y < restoreArea.Height; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; - blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f); + blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f, workingBuffer); } return; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 26ae28fd4..119d53fe9 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -343,69 +343,18 @@ internal static class WebpChunkParsingUtils throw new ImageFormatException("Invalid Webp data, could not read chunk type."); } - /// - /// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP '). - /// If there are more such chunks, readers MAY ignore all except the first one. - /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks. - /// - /// The stream to read the data from. - /// The chunk type to parse. - /// The image metadata to write to. - /// If true, metadata will be ignored. - /// Indicates how to handle segment integrity issues. - /// Buffer to store the data read from the stream. - public static void ParseOptionalChunks( - BufferedReadStream stream, - WebpChunkType chunkType, - ImageMetadata metadata, - bool ignoreMetadata, - SegmentIntegrityHandling segmentIntegrityHandling, - Span buffer) - { - long streamLength = stream.Length; - while (stream.Position < streamLength) - { - switch (chunkType) - { - case WebpChunkType.Iccp: - ReadIccProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer); - break; - - case WebpChunkType.Exif: - ReadExifProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer); - break; - case WebpChunkType.Xmp: - ReadXmpProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer); - break; - default: - - // Ignore unknown chunks. - // These must always fall after the image data so we are safe to always skip them. - uint chunkLength = ReadChunkSize(stream, buffer, false); - stream.Skip((int)chunkLength); - break; - } - } - } - /// /// Reads the ICCP chunk from the stream. /// /// The stream to decode from. /// The image metadata. /// If true, metadata will be ignored. - /// Indicates how to handle segment integrity issues. - /// Temporary buffer. public static void ReadIccProfile( BufferedReadStream stream, ImageMetadata metadata, - bool ignoreMetadata, - SegmentIntegrityHandling segmentIntegrityHandling, - Span buffer) + bool ignoreMetadata) { - // While ICC profiles are optional, an invalid ICC profile cannot be ignored as it must precede the image data - // and since we canot determine its size to allow skipping without reading the chunk size, we have to throw if it's invalid. - // Hence we do not consider segment integrity handling here. + Span buffer = stackalloc byte[4]; uint iccpChunkSize = ReadChunkSize(stream, buffer); if (ignoreMetadata || metadata.IccProfile != null) { @@ -413,22 +362,20 @@ internal static class WebpChunkParsingUtils } else { - bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone; byte[] iccpData = new byte[iccpChunkSize]; int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize); - - // We have the size but the profile is invalid if we cannot read enough data. - // Use the segment integrity handling to determine if we throw. - if (bytesRead != iccpChunkSize && ignoreNone) + if (bytesRead != iccpChunkSize) { WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); } IccProfile profile = new(iccpData); - if (profile.CheckIsValid()) + if (!profile.CheckIsValid()) { - metadata.IccProfile = profile; + throw new InvalidIccProfileException("Invalid ICC profile."); } + + metadata.IccProfile = profile; } } @@ -438,17 +385,13 @@ internal static class WebpChunkParsingUtils /// The stream to decode from. /// The image metadata. /// If true, metadata will be ignored. - /// Indicates how to handle segment integrity issues. - /// Temporary buffer. public static void ReadExifProfile( BufferedReadStream stream, ImageMetadata metadata, - bool ignoreMetadata, - SegmentIntegrityHandling segmentIntegrityHandling, - Span buffer) + bool ignoreMetadata) { - bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone; - uint exifChunkSize = ReadChunkSize(stream, buffer, ignoreNone); + Span buffer = stackalloc byte[4]; + uint exifChunkSize = ReadChunkSize(stream, buffer); if (ignoreMetadata || metadata.ExifProfile != null) { stream.Skip((int)exifChunkSize); @@ -459,12 +402,7 @@ internal static class WebpChunkParsingUtils int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize); if (bytesRead != exifChunkSize) { - if (ignoreNone) - { - WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile"); - } - - return; + WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the EXIF profile"); } ExifProfile exifProfile = new(exifData); @@ -490,18 +428,13 @@ internal static class WebpChunkParsingUtils /// The stream to decode from. /// The image metadata. /// If true, metadata will be ignored. - /// Indicates how to handle segment integrity issues. - /// Temporary buffer. public static void ReadXmpProfile( BufferedReadStream stream, ImageMetadata metadata, - bool ignoreMetadata, - SegmentIntegrityHandling segmentIntegrityHandling, - Span buffer) + bool ignoreMetadata) { - bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone; - - uint xmpChunkSize = ReadChunkSize(stream, buffer, ignoreNone); + Span buffer = stackalloc byte[4]; + uint xmpChunkSize = ReadChunkSize(stream, buffer); if (ignoreMetadata || metadata.XmpProfile != null) { stream.Skip((int)xmpChunkSize); @@ -512,12 +445,7 @@ internal static class WebpChunkParsingUtils int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize); if (bytesRead != xmpChunkSize) { - if (ignoreNone) - { - WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile"); - } - - return; + WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the XMP profile"); } metadata.XmpProfile = new XmpProfile(xmpData); diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index d6a07eeca..55bdca69c 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -52,8 +52,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable /// private readonly BackgroundColorHandling backgroundColorHandling; - private readonly SegmentIntegrityHandling segmentIntegrityHandling; - /// /// Initializes a new instance of the class. /// @@ -62,7 +60,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable : base(options.GeneralOptions) { this.backgroundColorHandling = options.BackgroundColorHandling; - this.segmentIntegrityHandling = options.GeneralOptions.SegmentIntegrityHandling; this.configuration = options.GeneralOptions.Configuration; this.skipMetadata = options.GeneralOptions.SkipMetadata; this.maxFrames = options.GeneralOptions.MaxFrames; @@ -90,7 +87,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable this.maxFrames, this.skipMetadata, this.backgroundColorHandling, - this.segmentIntegrityHandling); + this.ExecuteAncillarySegmentAction); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -149,7 +146,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable this.maxFrames, this.skipMetadata, this.backgroundColorHandling, - this.segmentIntegrityHandling); + this.ExecuteAncillarySegmentAction); return animationDecoder.Identify( stream, @@ -278,19 +275,21 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable Span buffer) { bool ignoreMetadata = this.skipMetadata; - SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling; switch (chunkType) { case WebpChunkType.Iccp: - WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); + + // While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must + // precede the image data, and we cannot safely skip it without successfully reading its size. + WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata); break; case WebpChunkType.Exif: - WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); break; case WebpChunkType.Xmp: - WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata)); break; case WebpChunkType.AnimationParameter: @@ -320,7 +319,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span buffer) { bool ignoreMetadata = this.skipMetadata; - SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling; if (ignoreMetadata || (!features.ExifProfile && !features.XmpMetaData)) { @@ -334,11 +332,11 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) { - WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); } else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null) { - WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer); + this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata)); } else { diff --git a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs index 73d114588..fa6eaa722 100644 --- a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs @@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Formats.Exr; namespace SixLabors.ImageSharp; @@ -1143,4 +1144,106 @@ public static partial class ImageExtensions encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), cancellationToken); + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsExr(this Image source, string path) => SaveAsExr(source, path, default); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path) => SaveAsExrAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsExrAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsExr(this Image source, string path, ExrEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance)); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path, ExrEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsExr(this Image source, Stream stream) + => SaveAsExr(source, stream, default); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsExrAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsExr(this Image source, Stream stream, ExrEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance)); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, Stream stream, ExrEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance), + cancellationToken); + } diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index 2dc9e8158..5f31daebc 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -15,6 +15,7 @@ using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Ani; +using SixLabors.ImageSharp.Formats.Exr; namespace SixLabors.ImageSharp; @@ -263,6 +264,26 @@ public static class ImageMetadataExtensions /// The new public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static ExrMetadata GetExrMetadata(this ImageMetadata source) => source.GetFormatMetadata(ExrFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static ExrMetadata CloneExrMetadata(this ImageMetadata source) => source.CloneFormatMetadata(ExrFormat.Instance); + /// /// Gets the from .
diff --git a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude index 2d6129c4c..c1c69c5b5 100644 --- a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude +++ b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude @@ -14,7 +14,8 @@ "Qoi", "Tga", "Tiff", - "Webp" + "Webp", + "Exr" ]; private static readonly string[] frameFormats = [ diff --git a/src/ImageSharp/GraphicsOptions.cs b/src/ImageSharp/GraphicsOptions.cs index dc3d17902..e05659651 100644 --- a/src/ImageSharp/GraphicsOptions.cs +++ b/src/ImageSharp/GraphicsOptions.cs @@ -6,11 +6,12 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; /// -/// Options for influencing the drawing functions. +/// Provides configuration for controlling how graphics operations are rendered, +/// including antialiasing, pixel blending, alpha composition, and coverage thresholding. /// public class GraphicsOptions : IDeepCloneable { - private int antialiasSubpixelDepth = 16; + private float antialiasThreshold = .5F; private float blendPercentage = 1F; /// @@ -24,61 +25,62 @@ public class GraphicsOptions : IDeepCloneable { this.AlphaCompositionMode = source.AlphaCompositionMode; this.Antialias = source.Antialias; - this.AntialiasSubpixelDepth = source.AntialiasSubpixelDepth; + this.AntialiasThreshold = source.AntialiasThreshold; this.BlendPercentage = source.BlendPercentage; this.ColorBlendingMode = source.ColorBlendingMode; } /// /// Gets or sets a value indicating whether antialiasing should be applied. - /// Defaults to true. + /// When , edges are rendered with smooth sub-pixel coverage. + /// When , coverage is snapped to binary (fully opaque or fully transparent) + /// using as the cutoff. + /// Defaults to . /// public bool Antialias { get; set; } = true; /// - /// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled. - /// Defaults to 16. + /// Gets or sets the coverage threshold used when is . + /// Pixels with antialiased coverage above this value are rendered as fully opaque; + /// pixels below are discarded. Valid range is 0 to 1. Lower values preserve more + /// thin features at small sizes. Defaults to 0.5F. /// - public int AntialiasSubpixelDepth + public float AntialiasThreshold { - get - { - return this.antialiasSubpixelDepth; - } + get => this.antialiasThreshold; set { - Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.AntialiasSubpixelDepth)); - this.antialiasSubpixelDepth = value; + Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.AntialiasThreshold)); + this.antialiasThreshold = value; } } /// - /// Gets or sets a value between indicating the blending percentage to apply to the drawing operation. - /// Range 0..1; Defaults to 1. + /// Gets or sets the blending percentage applied to the drawing operation. + /// A value of 1.0 applies the operation at full strength; 0.0 makes it invisible. + /// Valid range is 0 to 1. Defaults to 1.0F. /// public float BlendPercentage { - get - { - return this.blendPercentage; - } + get => this.blendPercentage; set { - Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage)); + Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.BlendPercentage)); this.blendPercentage = value; } } /// - /// Gets or sets a value indicating the color blending mode to apply to the drawing operation. + /// Gets or sets the color blending mode used to combine source and destination pixel colors. /// Defaults to . /// public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal; /// - /// Gets or sets a value indicating the alpha composition mode to apply to the drawing operation + /// Gets or sets the alpha composition mode that determines how source and destination alpha + /// channels are combined using Porter-Duff operators. /// Defaults to . /// public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; diff --git a/src/ImageSharp/Image.LoadPixelData.cs b/src/ImageSharp/Image.LoadPixelData.cs index efe1b6e2f..2847f0d57 100644 --- a/src/ImageSharp/Image.LoadPixelData.cs +++ b/src/ImageSharp/Image.LoadPixelData.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; @@ -25,6 +24,27 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => LoadPixelData(Configuration.Default, data, width, height); + /// + /// Create a new instance of the class from raw data + /// using pixels between source row starts. + /// + /// The readonly span containing image data. + /// The width of the final image. + /// The height of the final image. + /// The number of pixels between row starts in . + /// The pixel format. + /// + /// or is not positive, + /// or is less than . + /// + /// + /// is smaller than ((height - 1) * rowStride) + width. + /// + /// A new . + public static Image LoadPixelData(ReadOnlySpan data, int width, int height, int rowStride) + where TPixel : unmanaged, IPixel + => LoadPixelData(Configuration.Default, data, width, height, rowStride); + /// /// Create a new instance of the class from the given readonly span of bytes in format. /// @@ -38,6 +58,28 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => LoadPixelData(Configuration.Default, data, width, height); + /// + /// Create a new instance of the class from a readonly span of bytes in + /// format using bytes between source row starts. + /// + /// The readonly span containing image data. + /// The width of the final image. + /// The height of the final image. + /// The number of bytes between row starts in . + /// The pixel format. + /// + /// or is not positive, + /// or resolves to fewer than pixels. + /// + /// + /// is not divisible by the pixel size, + /// or is smaller than the required strided image length. + /// + /// A new . + public static Image LoadPixelData(ReadOnlySpan data, int width, int height, int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => LoadPixelData(Configuration.Default, data, width, height, rowStrideInBytes); + /// /// Create a new instance of the class from the given readonly span of bytes in format. /// @@ -53,6 +95,40 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height); + /// + /// Create a new instance of the class from a readonly span of bytes in + /// format using bytes between source row starts. + /// + /// The configuration for the decoder. + /// The readonly span containing image data. + /// The width of the final image. + /// The height of the final image. + /// The number of bytes between row starts in . + /// The pixel format. + /// The configuration is null. + /// + /// or is not positive, + /// or resolves to fewer than pixels. + /// + /// + /// is not divisible by the pixel size, + /// or is smaller than the required strided image length. + /// + /// A new . + public static Image LoadPixelData( + Configuration configuration, + ReadOnlySpan data, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + + int rowStride = GetPixelRowStrideFromByteStride(width, rowStrideInBytes, nameof(rowStrideInBytes)); + return LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height, rowStride); + } + /// /// Create a new instance of the class from the raw data. /// @@ -66,20 +142,42 @@ public abstract partial class Image /// A new . public static Image LoadPixelData(Configuration configuration, ReadOnlySpan data, int width, int height) where TPixel : unmanaged, IPixel + => LoadPixelData(configuration, data, width, height, width); + + /// + /// Create a new instance of the class from raw data + /// using pixels between source row starts. + /// + /// The configuration for the decoder. + /// The readonly span containing the image pixel data. + /// The width of the final image. + /// The height of the final image. + /// The number of pixels between row starts in . + /// The configuration is null. + /// + /// or is not positive, + /// or is less than . + /// + /// + /// is smaller than ((height - 1) * rowStride) + width. + /// + /// The pixel format. + /// A new . + public static Image LoadPixelData( + Configuration configuration, + ReadOnlySpan data, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); - - if (data.IsEmpty) - { - throw new ArgumentException("Pixel data cannot be empty.", nameof(data)); - } - - int count = width * height; - Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data)); + ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride)); + long requiredLength = GetRequiredLength(width, height, rowStride); + Guard.MustBeGreaterThanOrEqualTo(data.Length, requiredLength, nameof(data)); Image image = new(configuration, width, height); - data = data[..count]; - data.CopyTo(image.Frames.RootFrame.PixelBuffer.FastMemoryGroup); + image.Frames.RootFrame.PixelBuffer.CopyFrom(data, rowStride); return image; } diff --git a/src/ImageSharp/Image.WrapMemory.cs b/src/ImageSharp/Image.WrapMemory.cs index 03bec8bc6..eac202a41 100644 --- a/src/ImageSharp/Image.WrapMemory.cs +++ b/src/ImageSharp/Image.WrapMemory.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -58,29 +59,55 @@ public abstract partial class Image /// /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as - /// an instance. - /// - /// - /// Please note: using this method does not transfer the ownership of the underlying buffer of the input - /// to the new instance. This means that consumers of this method must ensure that the input buffer - /// is either self-contained, (for example, a instance wrapping a new array that was - /// created), or that the owning object is not disposed until the returned is disposed. + /// Wraps an existing memory area allowing viewing/manipulation as an + /// with pixels between row starts. /// /// - /// If the input instance is one retrieved from an instance - /// rented from a memory pool (such as ), and that owning instance is disposed while the image is still - /// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other - /// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately. + /// Please note: using this method does not transfer the ownership of the underlying buffer of the input + /// to the new instance. Consumers must ensure that + /// the input buffer remains valid for the full lifetime of the returned image. /// /// - /// The pixel type - /// The - /// The pixel memory. - /// The width of the memory image. - /// The height of the memory image. + /// The pixel type. + /// The . + /// The source pixel memory. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of pixels between row starts in . + /// The . /// The configuration is null. + /// The metadata is null. + /// + /// or is not positive, + /// or is less than . + /// + /// + /// The length of is less than + /// ((height - 1) * rowStride) + width. + /// /// An instance. + public static Image WrapMemory( + Configuration configuration, + Memory pixelMemory, + int width, + int height, + int rowStride, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride)); + + long requiredLength = GetRequiredLength(width, height, rowStride); + Guard.IsTrue(pixelMemory.Length >= requiredLength, nameof(pixelMemory), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemory); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// public static Image WrapMemory( Configuration configuration, Memory pixelMemory, @@ -89,29 +116,17 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(configuration, pixelMemory, width, height, new ImageMetadata()); - /// - /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as - /// an instance. - /// - /// - /// Please note: using this method does not transfer the ownership of the underlying buffer of the input - /// to the new instance. This means that consumers of this method must ensure that the input buffer - /// is either self-contained, (for example, a instance wrapping a new array that was - /// created), or that the owning object is not disposed until the returned is disposed. - /// - /// - /// If the input instance is one retrieved from an instance - /// rented from a memory pool (such as ), and that owning instance is disposed while the image is still - /// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other - /// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately. - /// - /// - /// The pixel type. - /// The pixel memory. - /// The width of the memory image. - /// The height of the memory image. - /// An instance. + /// + public static Image WrapMemory( + Configuration configuration, + Memory pixelMemory, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pixelMemory, width, height, rowStride, new ImageMetadata()); + + /// public static Image WrapMemory( Memory pixelMemory, int width, @@ -119,6 +134,15 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(Configuration.Default, pixelMemory, width, height); + /// + public static Image WrapMemory( + Memory pixelMemory, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pixelMemory, width, height, rowStride); + /// /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, /// allowing to view/manipulate it as an instance. @@ -152,19 +176,55 @@ public abstract partial class Image } /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, - /// allowing to view/manipulate it as an instance. - /// The ownership of the is being transferred to the new instance, - /// meaning that the caller is not allowed to dispose . - /// It will be disposed together with the result image. + /// + /// Wraps an existing memory owner allowing viewing/manipulation as an + /// with pixels between row starts. + /// + /// + /// Ownership of is transferred to the returned image. The caller + /// must not dispose the owner manually. + /// /// /// The pixel type. - /// The - /// The that is being transferred to the image. - /// The width of the memory image. - /// The height of the memory image. + /// The . + /// The pixel memory owner transferred to the image. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of pixels between row starts in the source memory. + /// The . /// The configuration is null. - /// An instance + /// The metadata is null. + /// + /// or is not positive, + /// or is less than . + /// + /// + /// The length of is less than + /// ((height - 1) * rowStride) + width. + /// + /// An instance. + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner pixelMemoryOwner, + int width, + int height, + int rowStride, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + ValidateWrapMemoryStride(width, height, rowStride, nameof(rowStride)); + + long requiredLength = GetRequiredLength(width, height, rowStride); + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= requiredLength, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemoryOwner); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// public static Image WrapMemory( Configuration configuration, IMemoryOwner pixelMemoryOwner, @@ -173,18 +233,17 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(configuration, pixelMemoryOwner, width, height, new ImageMetadata()); - /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, - /// allowing to view/manipulate it as an instance. - /// The ownership of the is being transferred to the new instance, - /// meaning that the caller is not allowed to dispose . - /// It will be disposed together with the result image. - /// - /// The pixel type - /// The that is being transferred to the image. - /// The width of the memory image. - /// The height of the memory image. - /// An instance. + /// + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner pixelMemoryOwner, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pixelMemoryOwner, width, height, rowStride, new ImageMetadata()); + + /// public static Image WrapMemory( IMemoryOwner pixelMemoryOwner, int width, @@ -192,6 +251,15 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(Configuration.Default, pixelMemoryOwner, width, height); + /// + public static Image WrapMemory( + IMemoryOwner pixelMemoryOwner, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pixelMemoryOwner, width, height, rowStride); + /// /// /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as @@ -240,29 +308,56 @@ public abstract partial class Image /// /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as - /// an instance. + /// Wraps an existing byte memory area allowing viewing/manipulation as an + /// with bytes between row starts. /// /// - /// Please note: using this method does not transfer the ownership of the underlying buffer of the input - /// to the new instance. This means that consumers of this method must ensure that the input buffer - /// is either self-contained, (for example, a instance wrapping a new array that was - /// created), or that the owning object is not disposed until the returned is disposed. - /// - /// - /// If the input instance is one retrieved from an instance - /// rented from a memory pool (such as ), and that owning instance is disposed while the image is still - /// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other - /// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately. + /// Please note: using this method does not transfer the ownership of the underlying buffer of the input + /// to the new instance. Consumers must ensure that + /// the input buffer remains valid for the full lifetime of the returned image. /// /// - /// The pixel type - /// The - /// The byte memory representing the pixel data. - /// The width of the memory image. - /// The height of the memory image. + /// The pixel type. + /// The . + /// The source byte memory. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of bytes between row starts in . + /// The . /// The configuration is null. + /// The metadata is null. + /// + /// or is not positive, + /// or resolves to less than pixels. + /// + /// + /// is not divisible by the size of , + /// or is smaller than the required strided image length. + /// /// An instance. + public static Image WrapMemory( + Configuration configuration, + Memory byteMemory, + int width, + int height, + int rowStrideInBytes, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + int rowStride = GetPixelRowStrideFromByteStride(width, rowStrideInBytes, nameof(rowStrideInBytes)); + long requiredLength = GetRequiredLength(width, height, rowStride); + + ByteMemoryManager memoryManager = new(byteMemory); + Guard.IsTrue(memoryManager.Memory.Length >= requiredLength, nameof(byteMemory), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// public static Image WrapMemory( Configuration configuration, Memory byteMemory, @@ -271,29 +366,17 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(configuration, byteMemory, width, height, new ImageMetadata()); - /// - /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as - /// an instance. - /// - /// - /// Please note: using this method does not transfer the ownership of the underlying buffer of the input - /// to the new instance. This means that consumers of this method must ensure that the input buffer - /// is either self-contained, (for example, a instance wrapping a new array that was - /// created), or that the owning object is not disposed until the returned is disposed. - /// - /// - /// If the input instance is one retrieved from an instance - /// rented from a memory pool (such as ), and that owning instance is disposed while the image is still - /// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other - /// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately. - /// - /// - /// The pixel type. - /// The byte memory representing the pixel data. - /// The width of the memory image. - /// The height of the memory image. - /// An instance. + /// + public static Image WrapMemory( + Configuration configuration, + Memory byteMemory, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, byteMemory, width, height, rowStrideInBytes, new ImageMetadata()); + + /// public static Image WrapMemory( Memory byteMemory, int width, @@ -301,6 +384,15 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(Configuration.Default, byteMemory, width, height); + /// + public static Image WrapMemory( + Memory byteMemory, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, byteMemory, width, height, rowStrideInBytes); + /// /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, /// allowing to view/manipulate it as an instance. @@ -337,19 +429,56 @@ public abstract partial class Image } /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, - /// allowing to view/manipulate it as an instance. - /// The ownership of the is being transferred to the new instance, - /// meaning that the caller is not allowed to dispose . - /// It will be disposed together with the result image. + /// + /// Wraps an existing byte memory owner allowing viewing/manipulation as an + /// with bytes between row starts. + /// + /// + /// Ownership of is transferred to the returned image. The caller + /// must not dispose the owner manually. + /// /// /// The pixel type. - /// The - /// The that is being transferred to the image. - /// The width of the memory image. - /// The height of the memory image. + /// The . + /// The byte memory owner transferred to the image. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of bytes between row starts in the source memory. + /// The . /// The configuration is null. - /// An instance + /// The metadata is null. + /// + /// or is not positive, + /// or resolves to less than pixels. + /// + /// + /// is not divisible by the size of , + /// or is smaller than the required strided image length. + /// + /// An instance. + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner byteMemoryOwner, + int width, + int height, + int rowStrideInBytes, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + int rowStride = GetPixelRowStrideFromByteStride(width, rowStrideInBytes, nameof(rowStrideInBytes)); + + ByteMemoryOwner pixelMemoryOwner = new(byteMemoryOwner); + long requiredLength = GetRequiredLength(width, height, rowStride); + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= requiredLength, nameof(byteMemoryOwner), "The length of the input memory is less than the specified image size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemoryOwner); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// public static Image WrapMemory( Configuration configuration, IMemoryOwner byteMemoryOwner, @@ -358,18 +487,17 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(configuration, byteMemoryOwner, width, height, new ImageMetadata()); - /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels, - /// allowing to view/manipulate it as an instance. - /// The ownership of the is being transferred to the new instance, - /// meaning that the caller is not allowed to dispose . - /// It will be disposed together with the result image. - /// - /// The pixel type - /// The that is being transferred to the image. - /// The width of the memory image. - /// The height of the memory image. - /// An instance. + /// + public static Image WrapMemory( + Configuration configuration, + IMemoryOwner byteMemoryOwner, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, byteMemoryOwner, width, height, rowStrideInBytes, new ImageMetadata()); + + /// public static Image WrapMemory( IMemoryOwner byteMemoryOwner, int width, @@ -377,6 +505,15 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(Configuration.Default, byteMemoryOwner, width, height); + /// + public static Image WrapMemory( + IMemoryOwner byteMemoryOwner, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, byteMemoryOwner, width, height, rowStrideInBytes); + /// /// /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as @@ -434,35 +571,60 @@ public abstract partial class Image /// /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as - /// an instance. + /// Wraps an unmanaged memory area allowing viewing/manipulation as an + /// with bytes between row starts. /// /// - /// Please note: this method relies on callers to carefully manage the target memory area being referenced by the - /// pointer and that the lifetime of such a memory area is at least equal to that of the returned - /// instance. For example, if the input pointer references an unmanaged memory area, - /// callers must ensure that the memory area is not freed as long as the returned is - /// in use and not disposed. The same applies if the input memory area points to a pinned managed object, as callers - /// must ensure that objects will remain pinned as long as the instance is in use. - /// Failing to do so constitutes undefined behavior and will likely lead to memory corruption and runtime crashes. - /// - /// - /// Note also that if you have a or an array (which can be cast to ) of - /// either or values, it is highly recommended to use one of the other - /// available overloads of this method instead (such as - /// or , to make the resulting code less error - /// prone and avoid having to pin the underlying memory buffer in use. This method is primarily meant to be used when - /// doing interop or working with buffers that are located in unmanaged memory. + /// Callers must ensure the memory referenced by remains valid for the full + /// lifetime of the returned image. /// /// - /// The pixel type - /// The - /// The pointer to the target memory buffer to wrap. - /// The byte length of the memory allocated. - /// The width of the memory image. - /// The height of the memory image. + /// The pixel type. + /// The . + /// The pointer to the source memory. + /// The byte length of the source memory. + /// The width of the memory image in pixels. + /// The height of the memory image in pixels. + /// The number of bytes between row starts in the source memory. + /// The . /// The configuration is null. + /// The metadata is null. + /// + /// is null, + /// is not divisible by the size of , + /// or is smaller than the required strided image length. + /// + /// + /// or is not positive, + /// or resolves to less than pixels. + /// /// An instance. + public static unsafe Image WrapMemory( + Configuration configuration, + void* pointer, + int bufferSizeInBytes, + int width, + int height, + int rowStrideInBytes, + ImageMetadata metadata) + where TPixel : unmanaged, IPixel + { + Guard.IsFalse(pointer == null, nameof(pointer), "Pointer must not be null"); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(metadata, nameof(metadata)); + + int rowStride = GetPixelRowStrideFromByteStride(width, rowStrideInBytes, nameof(rowStrideInBytes)); + long requiredLength = GetRequiredLength(width, height, rowStride); + + Guard.MustBeLessThanOrEqualTo(requiredLength, int.MaxValue, nameof(requiredLength)); + Guard.MustBeGreaterThanOrEqualTo(bufferSizeInBytes / Unsafe.SizeOf(), requiredLength, nameof(bufferSizeInBytes)); + + UnmanagedMemoryManager memoryManager = new(pointer, (int)requiredLength); + MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); + return new Image(configuration, memorySource, width, height, rowStride, metadata); + } + + /// public static unsafe Image WrapMemory( Configuration configuration, void* pointer, @@ -472,35 +634,18 @@ public abstract partial class Image where TPixel : unmanaged, IPixel => WrapMemory(configuration, pointer, bufferSizeInBytes, width, height, new ImageMetadata()); - /// - /// - /// Wraps an existing contiguous memory area of at least 'width' x 'height' pixels allowing viewing/manipulation as - /// an instance. - /// - /// - /// Please note: this method relies on callers to carefully manage the target memory area being referenced by the - /// pointer and that the lifetime of such a memory area is at least equal to that of the returned - /// instance. For example, if the input pointer references an unmanaged memory area, - /// callers must ensure that the memory area is not freed as long as the returned is - /// in use and not disposed. The same applies if the input memory area points to a pinned managed object, as callers - /// must ensure that objects will remain pinned as long as the instance is in use. - /// Failing to do so constitutes undefined behavior and will likely lead to memory corruption and runtime crashes. - /// - /// - /// Note also that if you have a or an array (which can be cast to ) of - /// either or values, it is highly recommended to use one of the other - /// available overloads of this method instead (such as - /// or , to make the resulting code less error - /// prone and avoid having to pin the underlying memory buffer in use. This method is primarily meant to be used when - /// doing interop or working with buffers that are located in unmanaged memory. - /// - /// - /// The pixel type. - /// The pointer to the target memory buffer to wrap. - /// The byte length of the memory allocated. - /// The width of the memory image. - /// The height of the memory image. - /// An instance. + /// + public static unsafe Image WrapMemory( + Configuration configuration, + void* pointer, + int bufferSizeInBytes, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(configuration, pointer, bufferSizeInBytes, width, height, rowStrideInBytes, new ImageMetadata()); + + /// public static unsafe Image WrapMemory( void* pointer, int bufferSizeInBytes, @@ -508,4 +653,41 @@ public abstract partial class Image int height) where TPixel : unmanaged, IPixel => WrapMemory(Configuration.Default, pointer, bufferSizeInBytes, width, height); + + /// + public static unsafe Image WrapMemory( + void* pointer, + int bufferSizeInBytes, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + => WrapMemory(Configuration.Default, pointer, bufferSizeInBytes, width, height, rowStrideInBytes); + + private static void ValidateWrapMemoryStride(int width, int height, int rowStride, string rowStrideParamName) + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(rowStride, width, rowStrideParamName); + } + + private static int GetPixelRowStrideFromByteStride(int width, int rowStrideInBytes, string rowStrideParamName) + where TPixel : unmanaged, IPixel + { + int pixelSizeInBytes = Unsafe.SizeOf(); + + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(rowStrideInBytes, 0, rowStrideParamName); + Guard.IsTrue( + rowStrideInBytes % pixelSizeInBytes == 0, + rowStrideParamName, + "The row stride in bytes must be divisible by the pixel size."); + + int rowStride = rowStrideInBytes / pixelSizeInBytes; + Guard.MustBeGreaterThanOrEqualTo(rowStride, width, rowStrideParamName); + return rowStride; + } + + private static long GetRequiredLength(int width, int height, int rowStride) + => checked(((long)(height - 1) * rowStride) + width); } diff --git a/src/ImageSharp/ImageFrame.LoadPixelData.cs b/src/ImageSharp/ImageFrame.LoadPixelData.cs index 003a0349a..6e2658b6e 100644 --- a/src/ImageSharp/ImageFrame.LoadPixelData.cs +++ b/src/ImageSharp/ImageFrame.LoadPixelData.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -25,6 +26,36 @@ public partial class ImageFrame where TPixel : unmanaged, IPixel => LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height); + /// + /// Create a new instance of the class from the given byte array in format. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The byte array containing image data. + /// The width of the final image. + /// The height of the final image. + /// The number of bytes between row starts in . + /// The pixel format. + /// A new . + internal static ImageFrame LoadPixelData( + Configuration configuration, + ReadOnlySpan data, + int width, + int height, + int rowStrideInBytes) + where TPixel : unmanaged, IPixel + { + int pixelSizeInBytes = Unsafe.SizeOf(); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(rowStrideInBytes, 0, nameof(rowStrideInBytes)); + Guard.IsTrue( + rowStrideInBytes % pixelSizeInBytes == 0, + nameof(rowStrideInBytes), + "The row stride in bytes must be divisible by the pixel size."); + + int rowStride = rowStrideInBytes / pixelSizeInBytes; + return LoadPixelData(configuration, MemoryMarshal.Cast(data), width, height, rowStride); + } + /// /// Create a new instance of the class from the raw data. /// @@ -36,14 +67,36 @@ public partial class ImageFrame /// A new . internal static ImageFrame LoadPixelData(Configuration configuration, ReadOnlySpan data, int width, int height) where TPixel : unmanaged, IPixel + => LoadPixelData(configuration, data, width, height, width); + + /// + /// Create a new instance of the class from raw data + /// using pixels between source row starts. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The span containing the image pixel data. + /// The width of the final image. + /// The height of the final image. + /// The number of pixels between row starts in . + /// The pixel format. + /// A new . + internal static ImageFrame LoadPixelData( + Configuration configuration, + ReadOnlySpan data, + int width, + int height, + int rowStride) + where TPixel : unmanaged, IPixel { - int count = width * height; - Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data)); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride)); - ImageFrame image = new(configuration, width, height); + long requiredLength = checked(((long)(height - 1) * rowStride) + width); + Guard.MustBeGreaterThanOrEqualTo(data.Length, requiredLength, nameof(data)); - data = data[..count]; - data.CopyTo(image.PixelBuffer.FastMemoryGroup); + ImageFrame image = new(configuration, width, height); + image.PixelBuffer.CopyFrom(data, rowStride); return image; } diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index 3d4b63fad..2d726dc4a 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -71,7 +71,20 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// Whether to dispose of managed and unmanaged objects. protected abstract void Dispose(bool disposing); - internal abstract void CopyPixelsTo(MemoryGroup destination) + /// + /// Accepts a . + /// Implemented by invoking + /// with the pixel type of the image. + /// + /// The visitor. + internal abstract void Accept(IImageFrameVisitor visitor); + + /// + /// Copies the pixel data of the image frame to a of a specific pixel type. + /// + /// The pixel type of the destination buffer. + /// The buffer to copy the pixel data to. + internal abstract void CopyPixelsTo(Buffer2D destination) where TDestinationPixel : unmanaged, IPixel; /// diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs index ad7d71974..892adb7aa 100644 --- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs +++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -27,11 +27,16 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer } internal ImageFrameCollection(Image parent, int width, int height, MemoryGroup memorySource) + : this(parent, width, height, width, memorySource) + { + } + + internal ImageFrameCollection(Image parent, int width, int height, int rowStride, MemoryGroup memorySource) { this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); // Frames are already cloned within the caller - this.frames.Add(new ImageFrame(parent.Configuration, width, height, memorySource)); + this.frames.Add(new ImageFrame(parent.Configuration, width, height, rowStride, memorySource)); } internal ImageFrameCollection(Image parent, IEnumerable> frames) @@ -416,7 +421,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.parent.Configuration, source.Size, source.Metadata.DeepClone()); - source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); + source.CopyPixelsTo(result.PixelBuffer); return result; } } diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index de71e77ca..9029d3eec 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -114,7 +114,20 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// The height of the image in pixels. /// The memory source. internal ImageFrame(Configuration configuration, int width, int height, MemoryGroup memorySource) - : this(configuration, width, height, memorySource, new ImageFrameMetadata()) + : this(configuration, width, height, width, memorySource, new ImageFrameMetadata()) + { + } + + /// + /// Initializes a new instance of the class wrapping an existing buffer. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The number of elements between row starts. + /// The memory source. + internal ImageFrame(Configuration configuration, int width, int height, int rowStride, MemoryGroup memorySource) + : this(configuration, width, height, rowStride, memorySource, new ImageFrameMetadata()) { } @@ -127,12 +140,26 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// The memory source. /// The metadata. internal ImageFrame(Configuration configuration, int width, int height, MemoryGroup memorySource, ImageFrameMetadata metadata) + : this(configuration, width, height, width, memorySource, metadata) + { + } + + /// + /// Initializes a new instance of the class wrapping an existing buffer. + /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The number of elements between row starts. + /// The memory source. + /// The metadata. + internal ImageFrame(Configuration configuration, int width, int height, int rowStride, MemoryGroup memorySource, ImageFrameMetadata metadata) : base(configuration, width, height, metadata) { Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - this.PixelBuffer = new Buffer2D(memorySource, width, height); + this.PixelBuffer = new Buffer2D(memorySource, width, height, rowStride); } /// @@ -150,7 +177,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource source.PixelBuffer.Width, source.PixelBuffer.Height, configuration.PreferContiguousImageBuffers); - source.PixelBuffer.FastMemoryGroup.CopyTo(this.PixelBuffer.FastMemoryGroup); + source.PixelBuffer.CopyTo(this.PixelBuffer); } /// @@ -270,16 +297,23 @@ public sealed class ImageFrame : ImageFrame, IPixelSource } /// - /// Copy image pixels to . + /// Copy image pixels to using the backing row layout. /// + /// + /// Destination length must be at least ((Height - 1) * PixelBuffer.RowStride) + Width. + /// /// The to copy image pixels to. - public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(destination); + public void CopyPixelDataTo(Span destination) => this.PixelBuffer.CopyTo(destination); /// - /// Copy image pixels to . + /// Copy image pixels to using the backing row layout. /// + /// + /// Destination length must be at least + /// (((Height - 1) * PixelBuffer.RowStride) + Width) * sizeof(TPixel) bytes. + /// /// The of to copy image pixels to. - public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(MemoryMarshal.Cast(destination)); + public void CopyPixelDataTo(Span destination) => this.PixelBuffer.CopyTo(MemoryMarshal.Cast(destination)); /// /// Gets the representation of the pixels as a in the source image's pixel format @@ -294,17 +328,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// The referencing the image buffer. /// The indicating the success. public bool DangerousTryGetSinglePixelMemory(out Memory memory) - { - IMemoryGroup mg = this.GetPixelMemoryGroup(); - if (mg.Count > 1) - { - memory = default; - return false; - } - - memory = mg.Single(); - return true; - } + => this.PixelBuffer.DangerousTryGetSingleMemory(out memory); /// /// Gets a reference to the pixel at the specified position. @@ -315,6 +339,10 @@ public sealed class ImageFrame : ImageFrame, IPixelSource [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ref TPixel GetPixelReference(int x, int y) => ref this.PixelBuffer[x, y]; + /// + internal override void Accept(IImageFrameVisitor visitor) + => visitor.Visit(this); + /// /// Copies the pixels to a of the same size. /// @@ -322,12 +350,12 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// ImageFrame{TPixel}.CopyTo(): target must be of the same size! internal void CopyTo(Buffer2D target) { - if (this.Size != target.Size()) + if (this.Size != target.Size) { throw new ArgumentException("ImageFrame.CopyTo(): target must be of the same size!", nameof(target)); } - this.PixelBuffer.FastMemoryGroup.CopyTo(target.FastMemoryGroup); + this.PixelBuffer.CopyTo(target); } /// @@ -339,8 +367,8 @@ public sealed class ImageFrame : ImageFrame, IPixelSource { Guard.NotNull(source, nameof(source)); - Buffer2D.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer); - this.UpdateSize(this.PixelBuffer.Size()); + _ = Buffer2D.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer); + this.UpdateSize(this.PixelBuffer.Size); } /// @@ -370,20 +398,32 @@ public sealed class ImageFrame : ImageFrame, IPixelSource this.isDisposed = true; } - internal override void CopyPixelsTo(MemoryGroup destination) + internal override void CopyPixelsTo(Buffer2D destination) { + Guard.NotNull(destination, nameof(destination)); + Guard.IsTrue( + destination.Width == this.Width && destination.Height == this.Height, + nameof(destination), + "Destination buffer must have the same dimensions as the source frame."); + if (typeof(TPixel) == typeof(TDestinationPixel)) { - this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) => + for (int y = 0; y < this.Height; y++) { - Span d1 = MemoryMarshal.Cast(d); - s.CopyTo(d1); - }); + Span sourceRow = this.PixelBuffer.DangerousGetRowSpan(y); + Span destinationRow = destination.DangerousGetRowSpan(y); + sourceRow.CopyTo(MemoryMarshal.Cast(destinationRow)); + } + return; } - this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) - => PixelOperations.Instance.To(this.Configuration, s, d)); + for (int y = 0; y < this.Height; y++) + { + Span sourceRow = this.PixelBuffer.DangerousGetRowSpan(y); + Span destinationRow = destination.DangerousGetRowSpan(y); + PixelOperations.Instance.To(this.Configuration, sourceRow, destinationRow); + } } /// @@ -439,19 +479,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// Clears the bitmap. /// /// The value to initialize the bitmap with. - internal void Clear(TPixel value) - { - MemoryGroup group = this.PixelBuffer.FastMemoryGroup; - - if (value.Equals(default)) - { - group.Clear(); - } - else - { - group.Fill(value); - } - } + internal void Clear(TPixel value) => this.PixelBuffer.Clear(value); [MethodImpl(InliningOptions.ShortMethod)] private void VerifyCoords(int x, int y) diff --git a/src/ImageSharp/ImageInfo.cs b/src/ImageSharp/ImageInfo.cs index 0bbd73b63..d27c4b933 100644 --- a/src/ImageSharp/ImageInfo.cs +++ b/src/ImageSharp/ImageInfo.cs @@ -63,8 +63,12 @@ public class ImageInfo public int Height => this.Size.Height; /// - /// Gets the number of frames in the image. + /// Gets the number of frame metadata entries available for the image. /// + /// + /// This value is the same as count and may be 0 when frame + /// metadata was not populated by the decoder. + /// public int FrameCount => this.FrameMetadataCollection.Count; /// @@ -73,8 +77,12 @@ public class ImageInfo public ImageMetadata Metadata { get; } /// - /// Gets the collection of metadata associated with individual image frames. + /// Gets the metadata associated with the decoded image frames, if available. /// + /// + /// For multi-frame formats, decoders populate one entry per decoded frame. For single-frame formats, this + /// collection is typically empty. + /// public IReadOnlyList FrameMetadataCollection { get; } /// @@ -86,4 +94,24 @@ public class ImageInfo /// Gets the bounds of the image. /// public Rectangle Bounds => new(Point.Empty, this.Size); + + /// + /// Gets the total number of bytes required to store the image pixels in memory. + /// + /// + /// This reports the in-memory size of the pixel data represented by this , not the + /// encoded size of the image file. The value is computed from the image dimensions and + /// . When contains decoded frame metadata, the + /// per-frame size is multiplied by that count. Otherwise, the value is the in-memory size of the single + /// image frame represented by this . + /// + /// The total number of bytes required to store the image pixels in memory. + public long GetPixelMemorySize() + { + int count = this.FrameMetadataCollection.Count > 0 + ? this.FrameMetadataCollection.Count + : 1; + + return (long)this.Size.Width * this.Size.Height * (this.PixelType.BitsPerPixel / 8) * count; + } } diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 2c7172387..9f545fc2a 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -56,6 +56,11 @@ True InlineArray.tt + + True + True + ImageExtensions.Save.tt + True True @@ -136,15 +141,20 @@ True DefaultPixelBlenders.Generated.tt - + True True - PorterDuffFunctions.Generated.tt + AssociatedAlphaPixelBlenders.Generated.tt - + + True True + AssociatedAlphaPorterDuffFunctions.Generated.tt + + True - ImageExtensions.Save.tt + True + PorterDuffFunctions.Generated.tt @@ -221,6 +231,14 @@ DefaultPixelBlenders.Generated.cs TextTemplatingFileGenerator + + AssociatedAlphaPixelBlenders.Generated.cs + TextTemplatingFileGenerator + + + AssociatedAlphaPorterDuffFunctions.Generated.cs + TextTemplatingFileGenerator + TextTemplatingFileGenerator ImageExtensions.Save.cs diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index dff8f577f..4881518c9 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -91,7 +90,7 @@ public sealed class Image : Image Configuration configuration, Buffer2D pixelBuffer, ImageMetadata metadata) - : this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, metadata) + : this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, pixelBuffer.RowStride, metadata) { } @@ -110,8 +109,29 @@ public sealed class Image : Image int width, int height, ImageMetadata metadata) + : this(configuration, memoryGroup, width, height, width, metadata) + { + } + + /// + /// Initializes a new instance of the class + /// wrapping an external . + /// + /// The configuration providing initialization code which allows extending the library. + /// The memory source. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The number of elements between row starts. + /// The images metadata. + internal Image( + Configuration configuration, + MemoryGroup memoryGroup, + int width, + int height, + int rowStride, + ImageMetadata metadata) : base(configuration, TPixel.GetPixelTypeInfo(), metadata, width, height) - => this.frames = new ImageFrameCollection(this, width, height, memoryGroup); + => this.frames = new ImageFrameCollection(this, width, height, rowStride, memoryGroup); /// /// Initializes a new instance of the class @@ -287,16 +307,24 @@ public sealed class Image : Image } /// - /// Copy image pixels to . + /// Copy image pixels to using the root frame backing row layout. /// + /// + /// Destination length must be at least + /// ((Height - 1) * Frames.RootFrame.PixelBuffer.RowStride) + Width. + /// /// The to copy image pixels to. - public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(destination); + public void CopyPixelDataTo(Span destination) => this.Frames.RootFrame.CopyPixelDataTo(destination); /// - /// Copy image pixels to . + /// Copy image pixels to using the root frame backing row layout. /// + /// + /// Destination length must be at least + /// (((Height - 1) * Frames.RootFrame.PixelBuffer.RowStride) + Width) * sizeof(TPixel) bytes. + /// /// The of to copy image pixels to. - public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(MemoryMarshal.Cast(destination)); + public void CopyPixelDataTo(Span destination) => this.Frames.RootFrame.CopyPixelDataTo(destination); /// /// Gets the representation of the pixels as a in the source image's pixel format @@ -311,17 +339,7 @@ public sealed class Image : Image /// The referencing the image buffer. /// The indicating the success. public bool DangerousTryGetSinglePixelMemory(out Memory memory) - { - IMemoryGroup mg = this.GetPixelMemoryGroup(); - if (mg.Count > 1) - { - memory = default; - return false; - } - - memory = mg.Single(); - return true; - } + => this.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out memory); /// /// Clones the current image. diff --git a/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs new file mode 100644 index 000000000..764bb37e8 --- /dev/null +++ b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Memory; + +/// +/// Provides the tracked memory-owner contract required by . +/// +/// The element type. +/// +/// Custom allocators implement +/// and return a derived type. The base allocator attaches allocation tracking after the owner has been +/// created so custom implementations cannot forget, duplicate, or mismatch the reservation lifecycle. +/// +public abstract class AllocationTrackedMemoryManager : MemoryManager + where T : struct +{ + private AllocationTrackingState allocationTracking; + + /// + /// Releases resources held by the concrete tracked owner. + /// + /// + /// when the owner is being disposed deterministically; + /// otherwise, . + /// + /// + /// Implementations release their own resources here. Allocation tracking is released by the sealed base + /// dispose path after this method returns. + /// + protected abstract void DisposeCore(bool disposing); + + /// + protected sealed override void Dispose(bool disposing) + { + try + { + this.DisposeCore(disposing); + } + finally + { + this.ReleaseAllocationTracking(); + } + } + + /// + /// Attaches allocation tracking to this owner after allocation has succeeded. + /// + /// The allocator that owns the reservation for this instance. + /// The reserved allocation size, in bytes. + /// + /// calls this exactly once after AllocateCore returns. + /// Derived allocators should not call it themselves; they only construct the concrete owner. + /// + protected internal virtual void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + => this.allocationTracking.Attach(allocator, lengthInBytes); + + /// + /// Releases any tracked allocation bytes associated with this instance. + /// + /// + /// Calling this more than once is safe; only the first call after tracking has been attached releases bytes. + /// + private void ReleaseAllocationTracking() => this.allocationTracking.Release(); +} diff --git a/src/ImageSharp/Memory/AllocationTrackingState.cs b/src/ImageSharp/Memory/AllocationTrackingState.cs new file mode 100644 index 000000000..1e9a632ed --- /dev/null +++ b/src/ImageSharp/Memory/AllocationTrackingState.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Memory; + +/// +/// Tracks a single allocator reservation and releases it exactly once. +/// +/// +/// This type is intended to live as a mutable field on the owning object. It should not be copied +/// after tracking has been attached, because the owner relies on a single shared release state. +/// +internal struct AllocationTrackingState +{ + private MemoryAllocator? allocator; + private long lengthInBytes; + private int released; + + /// + /// Attaches allocator reservation tracking to the current owner. + /// + /// The allocator that owns the reservation. + /// The reserved allocation size, in bytes. + /// + /// Must complete-before the owning object's reference is observable to any other thread. + /// guarantees this by attaching synchronously on the allocating + /// thread before returning the owner; reference publication then provides the release fence + /// that makes these field writes visible to a subsequent on another thread. + /// + internal void Attach(MemoryAllocator allocator, long lengthInBytes) + { + this.allocator = allocator; + this.lengthInBytes = lengthInBytes; + } + + /// + /// Releases the attached allocator reservation once. + /// + internal void Release() + { + if (Interlocked.Exchange(ref this.released, 1) == 0 && this.allocator != null) + { + this.allocator.ReleaseAccumulatedBytes(this.lengthInBytes); + this.allocator = null; + } + } +} diff --git a/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs b/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs index 3ead1c5df..986ed7f7c 100644 --- a/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs +++ b/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs @@ -1,9 +1,19 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Memory; +/// +/// Provides helper methods for working with . +/// internal static class AllocationOptionsExtensions { - public static bool Has(this AllocationOptions options, AllocationOptions flag) => (options & flag) == flag; + /// + /// Returns a value indicating whether the specified flag is set on the allocation options. + /// + /// The allocation options to inspect. + /// The flag to test for. + /// if is set; otherwise, . + public static bool Has(this AllocationOptions options, AllocationOptions flag) + => (options & flag) == flag; } diff --git a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs index 9f34602fb..c22e827a8 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs @@ -47,7 +47,7 @@ internal class BasicArrayBuffer : ManagedBufferBase public override Span GetSpan() => this.Array.AsSpan(0, this.Length); /// - protected override void Dispose(bool disposing) + protected override void DisposeCore(bool disposing) { } diff --git a/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs b/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs index a6ed797d6..84dd065f5 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals; /// Provides a base class for implementations by implementing pinning logic for adaption. /// /// The element type. -internal abstract class ManagedBufferBase : MemoryManager +internal abstract class ManagedBufferBase : AllocationTrackedMemoryManager where T : struct { private GCHandle pinHandle; diff --git a/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs b/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs index 4a202a96c..b0fe0b649 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals; /// internal abstract class RefCountedMemoryLifetimeGuard : IDisposable { + private AllocationTrackingState allocationTracking; private int refCount = 1; private int disposed; private int released; @@ -38,6 +39,14 @@ internal abstract class RefCountedMemoryLifetimeGuard : IDisposable public void ReleaseRef() => this.ReleaseRef(false); + /// + /// Attaches allocator reservation tracking to this lifetime guard. + /// + /// The allocator that owns the reservation. + /// The reserved allocation size, in bytes. + public void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + => this.allocationTracking.Attach(allocator, lengthInBytes); + public void Dispose() { int wasDisposed = Interlocked.Exchange(ref this.disposed, 1); @@ -69,6 +78,10 @@ internal abstract class RefCountedMemoryLifetimeGuard : IDisposable } this.Release(); + + // Guard-backed resources can be recovered by finalization, so their allocator + // reservation must follow the guard's actual release point instead of the owner object. + this.allocationTracking.Release(); } } } diff --git a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs index 02bdf0f48..97fab1b68 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs @@ -13,7 +13,7 @@ internal class SharedArrayPoolBuffer : ManagedBufferBase, IRefCounted where T : struct { private readonly int lengthInBytes; - private LifetimeGuard lifetimeGuard; + private readonly LifetimeGuard lifetimeGuard; public SharedArrayPoolBuffer(int lengthInElements) { @@ -24,7 +24,10 @@ internal class SharedArrayPoolBuffer : ManagedBufferBase, IRefCounted public byte[]? Array { get; private set; } - protected override void Dispose(bool disposing) + protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + => this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes); + + protected override void DisposeCore(bool disposing) { if (this.Array == null) { diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs index 854b40e0c..3a729cdf2 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Memory.Internals; /// access to unmanaged buffers allocated by . /// /// The element type. -internal sealed unsafe class UnmanagedBuffer : MemoryManager, IRefCounted +internal sealed unsafe class UnmanagedBuffer : AllocationTrackedMemoryManager, IRefCounted where T : struct { private readonly int lengthInElements; @@ -31,6 +31,9 @@ internal sealed unsafe class UnmanagedBuffer : MemoryManager, IRefCounted public void* Pointer => this.lifetimeGuard.Handle.Pointer; + protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + => this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes); + public override Span GetSpan() { DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name); @@ -52,7 +55,7 @@ internal sealed unsafe class UnmanagedBuffer : MemoryManager, IRefCounted } /// - protected override void Dispose(bool disposing) + protected override void DisposeCore(bool disposing) { DebugGuard.IsTrue(disposing, nameof(disposing), "Unmanaged buffers should not have finalizer!"); diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs index 8eaf0b6d6..1ca0df1c9 100644 --- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs @@ -12,6 +12,7 @@ namespace SixLabors.ImageSharp.Memory; public abstract class MemoryAllocator { private const int OneGigabyte = 1 << 30; + private long accumulativeAllocatedBytes; /// /// Gets the default platform-specific global instance that @@ -23,9 +24,34 @@ public abstract class MemoryAllocator /// public static MemoryAllocator Default { get; } = Create(); - internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte; + /// + /// Gets the maximum number of bytes that can be allocated by a memory group. + /// + /// + /// The allocation limit is determined by the process architecture: 4 GB for 64-bit processes and + /// 1 GB for 32-bit processes. + /// + internal long MemoryGroupAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte; - internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte; + /// + /// Gets the maximum accumulative size, in bytes, of all active allocations made through this allocator instance. + /// + /// + /// Defaults to , effectively imposing no limit on the accumulative total. + /// When set, this provides a safeguard against excessive memory consumption by capping the combined size of + /// outstanding allocations issued by this instance.
+ /// When the accumulative size of active allocations exceeds this limit, an will be thrown to + /// prevent further allocations and signal that the limit has been breached. + ///
+ internal long AccumulativeAllocationLimitBytes { get; private protected set; } = long.MaxValue; + + /// + /// Gets the maximum size, in bytes, that can be allocated for a single buffer. + /// + /// + /// The single buffer allocation limit is set to 1 GB by default. + /// + internal int SingleBufferAllocationLimitBytes { get; private protected set; } = OneGigabyte; /// /// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes. @@ -47,13 +73,26 @@ public abstract class MemoryAllocator public static MemoryAllocator Create(MemoryAllocatorOptions options) { UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes); + allocator.ApplyOptions(options); + return allocator; + } + + /// + /// Applies the supplied to this instance. + /// + /// The options to apply. Properties left as are ignored. + private protected void ApplyOptions(MemoryAllocatorOptions options) + { if (options.AllocationLimitMegabytes.HasValue) { - allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L; - allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes); + this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L; + this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes); } - return allocator; + if (options.AccumulativeAllocationLimitMegabytes.HasValue) + { + this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L; + } } /// @@ -63,15 +102,60 @@ public abstract class MemoryAllocator /// Size of the buffer to allocate. /// The allocation options. /// A buffer of values of type . - /// When length is zero or negative. - /// When length is over the capacity of the allocator. - public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + /// When length is negative or over the capacity of the allocator. + public IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + where T : struct + { + long lengthInBytes = this.GetValidatedAllocationLengthInBytes(length); + bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && lengthInBytes != 0; + if (shouldTrack) + { + this.ReserveAllocation(lengthInBytes); + } + + try + { + AllocationTrackedMemoryManager owner = this.AllocateCore(length, options); + if (shouldTrack) + { + owner.AttachAllocationTracking(this, lengthInBytes); + } + + return owner; + } + catch + { + if (shouldTrack) + { + this.ReleaseAccumulatedBytes(lengthInBytes); + } + + throw; + } + } + + /// + /// Allocates a tracked memory owner for . + /// + /// Type of the data stored in the buffer. + /// Size of the buffer to allocate. + /// The allocation options. + /// A tracked memory owner of values of type . + /// + /// Implementations should only allocate and initialize the concrete owner. The base allocator + /// reserves bytes, attaches tracking to the returned owner, and releases the reservation if allocation fails. + /// + protected abstract AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) where T : struct; /// /// Releases all retained resources not being in use. /// Eg: by resetting array pools and letting GC to free the arrays. /// + /// + /// This does not dispose active allocations; callers are responsible for disposing all + /// instances to release memory. + /// public virtual void ReleaseRetainedResources() { } @@ -102,11 +186,109 @@ public abstract class MemoryAllocator InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes); } - // Cast to long is safe because we already checked that the total length is within the limit. - return this.AllocateGroupCore(totalLength, (long)totalLengthInBytes, bufferAlignment, options); + long totalLengthInBytesLong = (long)totalLengthInBytes; + bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && totalLengthInBytesLong != 0; + if (shouldTrack) + { + this.ReserveAllocation(totalLengthInBytesLong); + } + + try + { + MemoryGroup group = this.AllocateGroupCore(totalLength, totalLengthInBytesLong, bufferAlignment, options); + if (shouldTrack) + { + group.AttachAllocationTracking(this, totalLengthInBytesLong); + } + + return group; + } + catch + { + if (shouldTrack) + { + this.ReleaseAccumulatedBytes(totalLengthInBytesLong); + } + + throw; + } } internal virtual MemoryGroup AllocateGroupCore(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options) where T : struct => MemoryGroup.Allocate(this, totalLengthInElements, bufferAlignment, options); + + /// + /// Allocates a single segment for construction. + /// + /// Type of the data stored in the buffer. + /// Size of the segment to allocate. + /// The allocation options. + /// A segment owner for the requested buffer length. + /// + /// The default implementation validates the segment size then calls + /// directly so group construction can reserve and release the total allocation once. + /// + internal virtual IMemoryOwner AllocateGroupBuffer(int length, AllocationOptions options = AllocationOptions.None) + where T : struct + { + _ = this.GetValidatedAllocationLengthInBytes(length); + return this.AllocateCore(length, options); + } + + /// + /// Returns the validated allocation length in bytes. + /// + /// Type of the data stored in the buffer. + /// Size of the buffer to allocate. + /// The allocation length in bytes. + private long GetValidatedAllocationLengthInBytes(int length) + where T : struct + { + if (length < 0) + { + InvalidMemoryOperationException.ThrowNegativeAllocationException(length); + } + + ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf(); + if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes) + { + InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes); + } + + return (long)lengthInBytes; + } + + /// + /// Reserves accumulative allocation bytes before creating the underlying buffer. + /// + /// The number of bytes to reserve. + private void ReserveAllocation(long lengthInBytes) + { + if (lengthInBytes <= 0) + { + return; + } + + long total = Interlocked.Add(ref this.accumulativeAllocatedBytes, lengthInBytes); + if (total > this.AccumulativeAllocationLimitBytes) + { + _ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes); + InvalidMemoryOperationException.ThrowAccumulativeAllocationOverLimitException(lengthInBytes, total, this.AccumulativeAllocationLimitBytes); + } + } + + /// + /// Releases accumulative allocation bytes previously tracked by this allocator. + /// + /// The number of bytes to release. + internal void ReleaseAccumulatedBytes(long lengthInBytes) + { + if (lengthInBytes <= 0) + { + return; + } + + _ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes); + } } diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs index d9ba62c1e..41a5ea5ee 100644 --- a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs +++ b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs @@ -10,6 +10,7 @@ public struct MemoryAllocatorOptions { private int? maximumPoolSizeMegabytes; private int? allocationLimitMegabytes; + private int? accumulativeAllocationLimitMegabytes; /// /// Gets or sets a value defining the maximum size of the 's internal memory pool @@ -17,7 +18,7 @@ public struct MemoryAllocatorOptions /// public int? MaximumPoolSizeMegabytes { - get => this.maximumPoolSizeMegabytes; + readonly get => this.maximumPoolSizeMegabytes; set { if (value.HasValue) @@ -35,15 +36,48 @@ public struct MemoryAllocatorOptions /// public int? AllocationLimitMegabytes { - get => this.allocationLimitMegabytes; + readonly get => this.allocationLimitMegabytes; set { if (value.HasValue) { Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AllocationLimitMegabytes)); + if (this.AccumulativeAllocationLimitMegabytes.HasValue) + { + Guard.MustBeLessThanOrEqualTo( + value.Value, + this.AccumulativeAllocationLimitMegabytes.Value, + nameof(this.AllocationLimitMegabytes)); + } } this.allocationLimitMegabytes = value; } } + + /// + /// Gets or sets a value defining the maximum accumulative size, in Megabytes, of all active allocations made + /// through the created instance. + /// (the default) imposes no limit on the accumulative total. + /// + public int? AccumulativeAllocationLimitMegabytes + { + readonly get => this.accumulativeAllocationLimitMegabytes; + set + { + if (value.HasValue) + { + Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AccumulativeAllocationLimitMegabytes)); + if (this.AllocationLimitMegabytes.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo( + value.Value, + this.AllocationLimitMegabytes.Value, + nameof(this.AccumulativeAllocationLimitMegabytes)); + } + } + + this.accumulativeAllocationLimitMegabytes = value; + } + } } diff --git a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs index 675afe8b9..5d183fa44 100644 --- a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory; @@ -12,23 +10,24 @@ namespace SixLabors.ImageSharp.Memory; /// public sealed class SimpleGcMemoryAllocator : MemoryAllocator { + /// + /// Initializes a new instance of the class with default limits. + /// + public SimpleGcMemoryAllocator() + : this(default) + { + } + + /// + /// Initializes a new instance of the class with custom limits. + /// + /// The to apply. + public SimpleGcMemoryAllocator(MemoryAllocatorOptions options) => this.ApplyOptions(options); + /// protected internal override int GetBufferCapacityInBytes() => int.MaxValue; /// - public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) - { - if (length < 0) - { - InvalidMemoryOperationException.ThrowNegativeAllocationException(length); - } - - ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf(); - if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes) - { - InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes); - } - - return new BasicArrayBuffer(new T[length]); - } + protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) + => new BasicArrayBuffer(new T[length]); } diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs index 10defe6cd..cfffc679a 100644 --- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory.Internals; @@ -71,30 +70,25 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes); } - // This delegate allows overriding the method returning the available system memory, - // so we can test our workaround for https://github.com/dotnet/runtime/issues/65466 - internal static Func GetTotalAvailableMemoryBytes { get; set; } = () => GC.GetGCMemoryInfo().TotalAvailableMemoryBytes; + internal UniformUnmanagedMemoryPoolMemoryAllocator( + int sharedArrayPoolThresholdInBytes, + int poolBufferSizeInBytes, + long maxPoolSizeInBytes, + int unmanagedBufferSizeInBytes, + MemoryAllocatorOptions options) + : this(sharedArrayPoolThresholdInBytes, poolBufferSizeInBytes, maxPoolSizeInBytes, unmanagedBufferSizeInBytes) + => this.ApplyOptions(options); /// protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes; /// - public override IMemoryOwner Allocate( + protected override AllocationTrackedMemoryManager AllocateCore( int length, AllocationOptions options = AllocationOptions.None) { - if (length < 0) - { - InvalidMemoryOperationException.ThrowNegativeAllocationException(length); - } - - ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf(); - if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes) - { - InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes); - } - - if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes) + int lengthInBytes = length * Unsafe.SizeOf(); + if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes) { SharedArrayPoolBuffer buffer = new(length); if (options.Has(AllocationOptions.Clean)) @@ -105,17 +99,16 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato return buffer; } - if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes) + if (lengthInBytes <= this.poolBufferSizeInBytes) { UnmanagedMemoryHandle mem = this.pool.Rent(); if (mem.IsValid) { - UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, length, options.Has(AllocationOptions.Clean)); - return buffer; + return this.pool.CreateGuardedBuffer(mem, length, options.Has(AllocationOptions.Clean)); } } - return this.nonPoolAllocator.Allocate(length, options); + return UnmanagedMemoryAllocator.AllocateBuffer(length, options); } /// @@ -155,20 +148,14 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato private static long GetDefaultMaxPoolSizeBytes() { - // On 64 bit set the pool size to a portion of the total available memory. - // https://github.com/dotnet/runtime/issues/55126#issuecomment-876779327 if (Environment.Is64BitProcess) { - long total = GetTotalAvailableMemoryBytes(); - - // Workaround for https://github.com/dotnet/runtime/issues/65466 - if (total > 0) - { - return (long)((ulong)total / 8); - } + // On 64 bit set the pool size to a portion of the total available memory. + GCMemoryInfo info = GC.GetGCMemoryInfo(); + return info.TotalAvailableMemoryBytes / 8; } - // Stick to a conservative value of 128 Megabytes on other platforms and 32 bit .NET 5.0: + // Stick to a conservative value of 128 Megabytes on 32 bit. return 128 * OneMegabyte; } } diff --git a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs index daf1a7992..eb52da7c0 100644 --- a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs @@ -18,7 +18,14 @@ internal class UnmanagedMemoryAllocator : MemoryAllocator protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes; - public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) + where T : struct + => AllocateBuffer(length, options); + + // The pooled allocator uses this internal entry point when it needs a raw unmanaged owner without + // nesting another allocator-level reservation cycle around the fallback allocation. + internal static UnmanagedBuffer AllocateBuffer(int length, AllocationOptions options = AllocationOptions.None) + where T : struct { UnmanagedBuffer buffer = UnmanagedBuffer.Allocate(length); if (options.Has(AllocationOptions.Clean)) diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index f0fa1438d..290d978d0 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -45,7 +45,7 @@ public static class Buffer2DExtensions Buffer2DRegion sourceRegion = source.GetRegion(rectangle); if (sourceRegion.IsFullBufferArea) { - sourceRegion.Buffer.FastMemoryGroup.CopyTo(buffer.FastMemoryGroup); + sourceRegion.Buffer.CopyTo(buffer); } else { @@ -81,7 +81,7 @@ public static class Buffer2DExtensions CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destinationIndex, columnCount); int elementSize = Unsafe.SizeOf(); - int width = buffer.Width * elementSize; + int rowByteStride = buffer.RowStride * elementSize; int sOffset = sourceIndex * elementSize; int dOffset = destinationIndex * elementSize; long count = columnCount * elementSize; @@ -98,66 +98,46 @@ public static class Buffer2DExtensions Buffer.MemoryCopy(sPtr, dPtr, count, count); - basePtr += width; + basePtr += rowByteStride; } } } /// - /// Returns a representing the full area of the buffer. - /// - /// The element type - /// The - /// The - internal static Rectangle FullRectangle(this Buffer2D buffer) - where T : struct - => new(0, 0, buffer.Width, buffer.Height); - - /// - /// Return a to the subregion represented by 'rectangle' + /// Return a to the subregion represented by . /// /// The element type /// The /// The rectangle subregion /// The - internal static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle) + public static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle) where T : unmanaged => new(buffer, rectangle); - internal static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height) + /// + /// Return a to the specified area of . + /// + /// The element type. + /// The . + /// The X coordinate of the region. + /// The Y coordinate of the region. + /// The region width. + /// The region height. + /// The . + public static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height) where T : unmanaged => new(buffer, new Rectangle(x, y, width, height)); /// - /// Return a to the whole area of 'buffer' + /// Return a to the whole area of . /// /// The element type /// The /// The - internal static Buffer2DRegion GetRegion(this Buffer2D buffer) + public static Buffer2DRegion GetRegion(this Buffer2D buffer) where T : unmanaged => new(buffer); - /// - /// Returns the size of the buffer. - /// - /// The element type - /// The - /// The of the buffer - internal static Size Size(this Buffer2D buffer) - where T : struct => - new(buffer.Width, buffer.Height); - - /// - /// Gets the bounds of the buffer. - /// - /// The element type - /// The - /// The - internal static Rectangle Bounds(this Buffer2D buffer) - where T : struct => - new(0, 0, buffer.Width, buffer.Height); - [Conditional("DEBUG")] private static void CheckColumnRegionsDoNotOverlap( Buffer2D buffer, diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs index f4b257b58..c78c4ce9e 100644 --- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs +++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs @@ -15,17 +15,17 @@ public readonly struct Buffer2DRegion /// Initializes a new instance of the struct. ///
/// The . - /// The defining a rectangular area within the buffer. + /// The defining a rectangular area within the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Buffer2DRegion(Buffer2D buffer, Rectangle rectangle) + public Buffer2DRegion(Buffer2D buffer, Rectangle bounds) { - DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle)); - DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle)); - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, buffer.Width, nameof(rectangle)); - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, buffer.Height, nameof(rectangle)); + DebugGuard.MustBeGreaterThanOrEqualTo(bounds.X, 0, nameof(bounds)); + DebugGuard.MustBeGreaterThanOrEqualTo(bounds.Y, 0, nameof(bounds)); + DebugGuard.MustBeLessThanOrEqualTo(bounds.Width, buffer.Width, nameof(bounds)); + DebugGuard.MustBeLessThanOrEqualTo(bounds.Height, buffer.Height, nameof(bounds)); this.Buffer = buffer; - this.Rectangle = rectangle; + this.Bounds = bounds; } /// @@ -34,15 +34,10 @@ public readonly struct Buffer2DRegion /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public Buffer2DRegion(Buffer2D buffer) - : this(buffer, buffer.FullRectangle()) + : this(buffer, buffer.Bounds) { } - /// - /// Gets the rectangle specifying the boundaries of the area in . - /// - public Rectangle Rectangle { get; } - /// /// Gets the being pointed by this instance. /// @@ -51,27 +46,32 @@ public readonly struct Buffer2DRegion /// /// Gets the width /// - public int Width => this.Rectangle.Width; + public int Width => this.Bounds.Width; /// /// Gets the height /// - public int Height => this.Rectangle.Height; + public int Height => this.Bounds.Height; /// - /// Gets the pixel stride which is equal to the width of . + /// Gets the number of elements between row starts in . /// - public int Stride => this.Buffer.Width; + public int Stride => this.Buffer.RowStride; /// /// Gets the size of the area. /// - internal Size Size => this.Rectangle.Size; + public Size Size => this.Bounds.Size; + + /// + /// Gets the rectangle specifying the boundaries of the area in . + /// + public Rectangle Bounds { get; } /// /// Gets a value indicating whether the area refers to the entire /// - internal bool IsFullBufferArea => this.Size == this.Buffer.Size(); + internal bool IsFullBufferArea => this.Size == this.Buffer.Size; /// /// Gets or sets a value at the given index. @@ -79,7 +79,7 @@ public readonly struct Buffer2DRegion /// The position inside a row /// The row index /// The reference to the value - internal ref T this[int x, int y] => ref this.Buffer[x + this.Rectangle.X, y + this.Rectangle.Y]; + internal ref T this[int x, int y] => ref this.Buffer[x + this.Bounds.X, y + this.Bounds.Y]; /// /// Gets a span to row 'y' inside this area. @@ -89,9 +89,9 @@ public readonly struct Buffer2DRegion [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span DangerousGetRowSpan(int y) { - int yy = this.Rectangle.Y + y; - int xx = this.Rectangle.X; - int width = this.Rectangle.Width; + int yy = this.Bounds.Y + y; + int xx = this.Bounds.X; + int width = this.Bounds.Width; return this.Buffer.DangerousGetRowSpan(yy).Slice(xx, width); } @@ -114,16 +114,16 @@ public readonly struct Buffer2DRegion /// /// Returns a subregion as . (Similar to .) /// - /// The specifying the boundaries of the subregion + /// The specifying the boundaries of the subregion /// The subregion [MethodImpl(MethodImplOptions.AggressiveInlining)] public Buffer2DRegion GetSubRegion(Rectangle rectangle) { - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Rectangle.Width, nameof(rectangle)); - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Rectangle.Height, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Bounds.Width, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Bounds.Height, nameof(rectangle)); - int x = this.Rectangle.X + rectangle.X; - int y = this.Rectangle.Y + rectangle.Y; + int x = this.Bounds.X + rectangle.X; + int y = this.Bounds.Y + rectangle.Y; rectangle = new Rectangle(x, y, rectangle.Width, rectangle.Height); return new Buffer2DRegion(this.Buffer, rectangle); } @@ -135,8 +135,8 @@ public readonly struct Buffer2DRegion [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ref T GetReferenceToOrigin() { - int y = this.Rectangle.Y; - int x = this.Rectangle.X; + int y = this.Bounds.Y; + int x = this.Bounds.X; return ref this.Buffer.DangerousGetRowSpan(y)[x]; } @@ -146,13 +146,13 @@ public readonly struct Buffer2DRegion internal void Clear() { // Optimization for when the size of the area is the same as the buffer size. - if (this.IsFullBufferArea) + if (this.IsFullBufferArea && this.Buffer.RowStride == this.Buffer.Width) { - this.Buffer.FastMemoryGroup.Clear(); + this.Buffer.Clear(default); return; } - for (int y = 0; y < this.Rectangle.Height; y++) + for (int y = 0; y < this.Bounds.Height; y++) { Span row = this.DangerousGetRowSpan(y); row.Clear(); @@ -166,13 +166,13 @@ public readonly struct Buffer2DRegion internal void Fill(T value) { // Optimization for when the size of the area is the same as the buffer size. - if (this.IsFullBufferArea) + if (this.IsFullBufferArea && this.Buffer.RowStride == this.Buffer.Width) { - this.Buffer.FastMemoryGroup.Fill(value); + this.Buffer.Clear(value); return; } - for (int y = 0; y < this.Rectangle.Height; y++) + for (int y = 0; y < this.Bounds.Height; y++) { Span row = this.DangerousGetRowSpan(y); row.Fill(value); diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index 39c6e62e1..cfd7664f0 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Memory; @@ -20,21 +21,53 @@ public sealed class Buffer2D : IDisposable /// The number of elements in a row. /// The number of rows. internal Buffer2D(MemoryGroup memoryGroup, int width, int height) + : this(memoryGroup, width, height, width) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to wrap. + /// The number of elements in a row. + /// The number of rows. + /// The number of elements between row starts. + internal Buffer2D(MemoryGroup memoryGroup, int width, int height, int rowStride) + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride)); + this.FastMemoryGroup = memoryGroup; - this.Width = width; - this.Height = height; + this.Size = new Size(width, height); + this.RowStride = rowStride; } /// /// Gets the width. /// - public int Width { get; private set; } + public int Width => this.Size.Width; /// /// Gets the height. /// - public int Height { get; private set; } + public int Height => this.Size.Height; + + /// + /// Gets the size of the buffer. + /// + public Size Size { get; private set; } + + /// + /// Gets the bounds of the buffer. + /// + /// The + public Rectangle Bounds => new(0, 0, this.Width, this.Height); + + /// + /// Gets the number of elements between row starts in the backing memory. + /// + public int RowStride { get; private set; } /// /// Gets the backing . @@ -75,6 +108,168 @@ public sealed class Buffer2D : IDisposable } } + /// + /// Wraps an existing memory area as a with tightly packed rows. + /// + /// + /// This method does not transfer ownership of to the returned . + /// The caller is responsible for ensuring that the memory remains valid for the entire lifetime of the returned buffer. + /// If originates from an (for example from ), + /// do not dispose that owner while the returned buffer is still in use. + /// + /// The source memory. + /// The number of elements in each row. + /// The number of rows. + /// The wrapped instance. + /// Thrown when or is not positive. + /// Thrown when is shorter than width * height. +#pragma warning disable CA1000 // Do not declare static members on generic types + public static Buffer2D WrapMemory(Memory memory, int width, int height) +#pragma warning restore CA1000 // Do not declare static members on generic types + => WrapMemory(memory, width, height, width); + + /// + /// Wraps an existing memory area as a using the specified row stride. + /// + /// + /// This method does not transfer ownership of to the returned . + /// The caller is responsible for ensuring that the memory remains valid for the entire lifetime of the returned buffer. + /// If originates from an (for example from ), + /// do not dispose that owner while the returned buffer is still in use. + /// The minimum required length is ((height - 1) * stride) + width elements. + /// + /// The source memory. + /// The number of elements in each row. + /// The number of rows. + /// The number of elements between row starts in the source memory. + /// The wrapped instance. + /// + /// Thrown when or is not positive, + /// or when is less than . + /// + /// Thrown when is shorter than the required buffer size. +#pragma warning disable CA1000 // Do not declare static members on generic types + public static Buffer2D WrapMemory(Memory memory, int width, int height, int stride) +#pragma warning restore CA1000 // Do not declare static members on generic types + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(stride, width, nameof(stride)); + + long requiredLength = checked(((long)(height - 1) * stride) + width); + Guard.IsTrue(memory.Length >= requiredLength, nameof(memory), "The length of the input memory is less than the specified buffer size"); + + MemoryGroup memorySource = MemoryGroup.Wrap(memory); + return new Buffer2D(memorySource, width, height, stride); + } + + /// + /// Gets the representation of the values as a single contiguous + /// when the backing group is a single tightly packed segment. + /// + /// The referencing the buffer. + /// + /// when the buffer can be copied as one contiguous block + /// without per-row handling; otherwise . + /// + public bool DangerousTryGetSingleMemory(out Memory memory) + { + if (this.MemoryGroup.Count > 1 || this.RowStride != this.Width) + { + memory = default; + return false; + } + + int logicalLength = checked((int)((long)this.Width * this.Height)); + memory = this.MemoryGroup[0][..logicalLength]; + return true; + } + + /// + /// Copies this buffer into using the source logical row layout. + /// + /// + /// When dimensions are equal, destination stride is respected. + /// When dimensions differ, source stride is used to copy the source logical layout into destination memory. + /// + /// The destination buffer. + internal void CopyTo(Buffer2D destination) + { + Guard.NotNull(destination, nameof(destination)); + + bool sameDimensions = this.Width == destination.Width && this.Height == destination.Height; + int destinationStride = sameDimensions ? destination.RowStride : this.RowStride; + + // Different dimensions use source logical layout. This supports SwapOrCopyContent, + // where metadata is swapped after data copy. + this.FastMemoryGroup.CopyTo( + this.RowStride, + destination.FastMemoryGroup, + destinationStride, + this.Width, + this.Height); + } + + /// + /// Copies this buffer into using the source row stride as destination layout. + /// + /// The destination span. + internal void CopyTo(Span destination) + { + long requiredLength = checked(((long)(this.Height - 1) * this.RowStride) + this.Width); + Guard.MustBeGreaterThanOrEqualTo(destination.Length, requiredLength, nameof(destination)); + + this.FastMemoryGroup.CopyTo( + this.RowStride, + destination, + this.RowStride, + this.Width, + this.Height); + } + + /// + /// Copies tightly packed row-major data from into this buffer. + /// + /// The source data. + internal void CopyFrom(ReadOnlySpan source) => this.CopyFrom(source, this.Width); + + /// + /// Copies row-major data from into this buffer using + /// elements between source row starts. + /// + /// The source data. + /// The number of elements between source row starts. + internal void CopyFrom(ReadOnlySpan source, int sourceStride) + { + Guard.MustBeGreaterThanOrEqualTo(sourceStride, this.Width, nameof(sourceStride)); + + long requiredLength = checked(((long)(this.Height - 1) * sourceStride) + this.Width); + Guard.MustBeGreaterThanOrEqualTo(source.Length, requiredLength, nameof(source)); + + // Copy row by row so padded source rows map correctly into the destination logical rows. + int sourceOffset = 0; + for (int y = 0; y < this.Height; y++) + { + source.Slice(sourceOffset, this.Width).CopyTo(this.DangerousGetRowSpan(y)); + sourceOffset += sourceStride; + } + } + + /// + /// Clears this buffer when is default; otherwise fills it with . + /// + /// The fill value. + internal void Clear(T value) + { + if (value.Equals(default)) + { + this.FastMemoryGroup.Clear(); + return; + } + + this.FastMemoryGroup.Fill(value); + } + /// /// Disposes the instance /// @@ -102,7 +297,13 @@ public sealed class Buffer2D : IDisposable this.ThrowYOutOfRangeException(y); } - return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width); + if (this.RowStride == this.Width) + { + return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width); + } + + int rowStart = checked(y * this.RowStride); + return this.FastMemoryGroup[0].Span.Slice(rowStart, this.Width); } internal bool DangerousTryGetPaddedRowSpan(int y, int padding, out Span paddedSpan) @@ -111,8 +312,10 @@ public sealed class Buffer2D : IDisposable DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); int stride = this.Width + padding; - - Span slice = this.FastMemoryGroup.GetRemainingSliceOfBuffer(y * (long)this.Width); + long rowStart = y * (long)this.RowStride; + Span slice = this.RowStride == this.Width + ? this.FastMemoryGroup.GetRemainingSliceOfBuffer(rowStart) + : this.FastMemoryGroup[0].Span[checked((int)rowStart)..]; if (slice.Length < stride) { @@ -127,7 +330,10 @@ public sealed class Buffer2D : IDisposable [MethodImpl(InliningOptions.ShortMethod)] internal ref T GetElementUnsafe(int x, int y) { - Span span = this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width); + Span span = this.RowStride == this.Width + ? this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width) + : this.FastMemoryGroup[0].Span.Slice(checked(y * this.RowStride), this.Width); + return ref span[x]; } @@ -141,6 +347,13 @@ public sealed class Buffer2D : IDisposable { DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); + + if (this.RowStride != this.Width) + { + int rowStart = checked(y * this.RowStride); + return this.FastMemoryGroup[0].Slice(rowStart, this.Width); + } + return this.FastMemoryGroup.View.GetBoundedMemorySlice(y * (long)this.Width, this.Width); } @@ -185,21 +398,30 @@ public sealed class Buffer2D : IDisposable } else { - if (destination.FastMemoryGroup.TotalLength != source.FastMemoryGroup.TotalLength) + long sourceLayoutLength = GetRequiredLength(source.Width, source.Height, source.RowStride); + long destinationLayoutLength = GetRequiredLength(destination.Width, destination.Height, destination.RowStride); + + bool destinationCanRepresentSource = destination.FastMemoryGroup.TotalLength >= sourceLayoutLength; + bool sourceCanRepresentDestination = source.FastMemoryGroup.TotalLength >= destinationLayoutLength; + if (!destinationCanRepresentSource || !sourceCanRepresentDestination) { throw new InvalidMemoryOperationException( "Trying to copy/swap incompatible buffers. This is most likely caused by applying an unsupported processor to wrapped-memory images."); } - source.FastMemoryGroup.CopyTo(destination.MemoryGroup); + source.CopyTo(destination); } - (destination.Width, source.Width) = (source.Width, destination.Width); - (destination.Height, source.Height) = (source.Height, destination.Height); + (destination.Size, source.Size) = (source.Size, destination.Size); + (destination.RowStride, source.RowStride) = (source.RowStride, destination.RowStride); return swapped; } [MethodImpl(InliningOptions.ColdPath)] private void ThrowYOutOfRangeException(int y) => throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}"); + + [MethodImpl(InliningOptions.ShortMethod)] + private static long GetRequiredLength(int width, int height, int stride) + => checked(((long)(height - 1) * stride) + width); } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs index 7e9719ea7..03f26aab0 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs @@ -15,12 +15,12 @@ public interface IMemoryGroup : IReadOnlyList> /// Gets the number of elements per contiguous sub-buffer preceding the last buffer. /// The last buffer is allowed to be smaller. /// - int BufferLength { get; } + public int BufferLength { get; } /// /// Gets the aggregate number of elements in the group. /// - long TotalLength { get; } + public long TotalLength { get; } /// /// Gets a value indicating whether the group has been invalidated. @@ -29,7 +29,7 @@ public interface IMemoryGroup : IReadOnlyList> /// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces /// the image buffers internally. /// - bool IsValid { get; } + public bool IsValid { get; } /// /// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current @@ -39,5 +39,5 @@ public interface IMemoryGroup : IReadOnlyList> /// implementation, which is still available when casting to one of the underlying interfaces. /// /// A new instance mapping the current values in use. - new MemoryGroupEnumerator GetEnumerator(); + public new MemoryGroupEnumerator GetEnumerator(); } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs index 148b5f6bf..b399d3d70 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs @@ -71,128 +71,159 @@ internal static class MemoryGroupExtensions return memory.Slice(bufferStart, length); } - internal static void CopyTo(this IMemoryGroup source, Span target) + /// + /// Copies a 2D logical region from into + /// using the provided source and target strides. + /// + /// The element type. + /// The source memory group. + /// Elements between source row starts. + /// The destination span. + /// Elements between destination row starts. + /// The logical row width to copy. + /// The number of rows to copy. + internal static void CopyTo( + this IMemoryGroup source, + int sourceStride, + Span target, + int targetStride, + int width, + int height) where T : struct { Guard.NotNull(source, nameof(source)); - Guard.MustBeGreaterThanOrEqualTo(target.Length, source.TotalLength, nameof(target)); + Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width)); + Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(sourceStride, width, nameof(sourceStride)); + Guard.MustBeGreaterThanOrEqualTo(targetStride, width, nameof(targetStride)); - MemoryGroupCursor cur = new(source); - long position = 0; - while (position < source.TotalLength) - { - int fwd = Math.Min(cur.LookAhead(), target.Length); - cur.GetSpan(fwd).CopyTo(target); + long sourceRequired = height == 0 ? 0 : checked(((long)(height - 1) * sourceStride) + width); + long targetRequired = height == 0 ? 0 : checked(((long)(height - 1) * targetStride) + width); + Guard.MustBeGreaterThanOrEqualTo(source.TotalLength, sourceRequired, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(target.Length, targetRequired, nameof(target)); - cur.Forward(fwd); - target = target[fwd..]; - position += fwd; + if (width == 0 || height == 0) + { + return; } - } - internal static void CopyTo(this Span source, IMemoryGroup target) - where T : struct - => CopyTo((ReadOnlySpan)source, target); + MemoryGroupCursor sourceCursor = new(source); + int sourceSkip = sourceStride - width; - internal static void CopyTo(this ReadOnlySpan source, IMemoryGroup target) - where T : struct - { - Guard.NotNull(target, nameof(target)); - Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, source.Length, nameof(target)); - - MemoryGroupCursor cur = new(target); - - while (!source.IsEmpty) + for (int y = 0; y < height; y++) { - int fwd = Math.Min(cur.LookAhead(), source.Length); - source[..fwd].CopyTo(cur.GetSpan(fwd)); - cur.Forward(fwd); - source = source[fwd..]; + int rowStart = checked(y * targetStride); + Span destinationRow = target.Slice(rowStart, width); + CopyFromCursorToSpan(ref sourceCursor, destinationRow); + + // Trailing padding after the last row is optional, so only skip between rows. + if (y < height - 1) + { + ForwardCursor(ref sourceCursor, sourceSkip); + } } } - internal static void CopyTo(this IMemoryGroup? source, IMemoryGroup? target) + /// + /// Copies a 2D logical region from into + /// using the provided source and target strides. + /// + /// The element type. + /// The source memory group. + /// Elements between source row starts. + /// The destination memory group. + /// Elements between destination row starts. + /// The logical row width to copy. + /// The number of rows to copy. + internal static void CopyTo( + this IMemoryGroup source, + int sourceStride, + IMemoryGroup target, + int targetStride, + int width, + int height) where T : struct { Guard.NotNull(source, nameof(source)); Guard.NotNull(target, nameof(target)); Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid."); Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid."); - Guard.MustBeLessThanOrEqualTo(source.TotalLength, target.TotalLength, "Destination buffer too short!"); + Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width)); + Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height)); + Guard.MustBeGreaterThanOrEqualTo(sourceStride, width, nameof(sourceStride)); + Guard.MustBeGreaterThanOrEqualTo(targetStride, width, nameof(targetStride)); - if (source.IsEmpty()) + long sourceRequired = height == 0 ? 0 : checked(((long)(height - 1) * sourceStride) + width); + long targetRequired = height == 0 ? 0 : checked(((long)(height - 1) * targetStride) + width); + Guard.MustBeGreaterThanOrEqualTo(source.TotalLength, sourceRequired, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, targetRequired, nameof(target)); + + if (width == 0 || height == 0) { return; } - long position = 0; - MemoryGroupCursor srcCur = new(source); - MemoryGroupCursor trgCur = new(target); + MemoryGroupCursor sourceCursor = new(source); + MemoryGroupCursor targetCursor = new(target); + int sourceSkip = sourceStride - width; + int targetSkip = targetStride - width; - while (position < source.TotalLength) + for (int y = 0; y < height; y++) { - int fwd = Math.Min(srcCur.LookAhead(), trgCur.LookAhead()); - Span srcSpan = srcCur.GetSpan(fwd); - Span trgSpan = trgCur.GetSpan(fwd); - srcSpan.CopyTo(trgSpan); - - srcCur.Forward(fwd); - trgCur.Forward(fwd); - position += fwd; + CopyFromCursorToCursor(ref sourceCursor, ref targetCursor, width); + + // Trailing padding after the last row is optional, so only skip between rows. + if (y < height - 1) + { + ForwardCursor(ref sourceCursor, sourceSkip); + ForwardCursor(ref targetCursor, targetSkip); + } } } - internal static void TransformTo( - this IMemoryGroup source, - IMemoryGroup target, - TransformItemsDelegate transform) - where TSource : struct - where TTarget : struct + private static void CopyFromCursorToCursor( + ref MemoryGroupCursor source, + ref MemoryGroupCursor target, + int count) + where T : struct { - Guard.NotNull(source, nameof(source)); - Guard.NotNull(target, nameof(target)); - Guard.NotNull(transform, nameof(transform)); - Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid."); - Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid."); - Guard.MustBeLessThanOrEqualTo(source.TotalLength, target.TotalLength, "Destination buffer too short!"); - - if (source.IsEmpty()) + int remaining = count; + while (remaining > 0) { - return; + int fwd = Math.Min(remaining, Math.Min(source.LookAhead(), target.LookAhead())); + source.GetSpan(fwd).CopyTo(target.GetSpan(fwd)); + source.Forward(fwd); + target.Forward(fwd); + remaining -= fwd; } + } - long position = 0; - MemoryGroupCursor srcCur = new(source); - MemoryGroupCursor trgCur = new(target); - - while (position < source.TotalLength) + private static void CopyFromCursorToSpan(ref MemoryGroupCursor source, Span target) + where T : struct + { + int remaining = target.Length; + while (remaining > 0) { - int fwd = Math.Min(srcCur.LookAhead(), trgCur.LookAhead()); - Span srcSpan = srcCur.GetSpan(fwd); - Span trgSpan = trgCur.GetSpan(fwd); - transform(srcSpan, trgSpan); - - srcCur.Forward(fwd); - trgCur.Forward(fwd); - position += fwd; + int copied = target.Length - remaining; + int fwd = Math.Min(remaining, source.LookAhead()); + source.GetSpan(fwd).CopyTo(target[copied..]); + source.Forward(fwd); + remaining -= fwd; } } - internal static void TransformInplace( - this IMemoryGroup memoryGroup, - TransformItemsInplaceDelegate transform) + private static void ForwardCursor(ref MemoryGroupCursor cursor, int steps) where T : struct { - foreach (Memory memory in memoryGroup) + int remaining = steps; + while (remaining > 0) { - transform(memory.Span); + int fwd = Math.Min(remaining, cursor.LookAhead()); + cursor.Forward(fwd); + remaining -= fwd; } } - internal static bool IsEmpty(this IMemoryGroup group) - where T : struct - => group.Count == 0; - private struct MemoryGroupCursor where T : struct { diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs index 950e2a019..75e93ce7f 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs @@ -31,23 +31,23 @@ internal abstract partial class MemoryGroup /// [MethodImpl(InliningOptions.ShortMethod)] - public override MemoryGroupEnumerator GetEnumerator() - { - return new MemoryGroupEnumerator(this); - } + public override MemoryGroupEnumerator GetEnumerator() => new(this); /// IEnumerator> IEnumerable>.GetEnumerator() - { + /* The runtime sees the Array class as if it implemented the * type-generic collection interfaces explicitly, so here we * can just cast the source array to IList> (or to * an equivalent type), and invoke the generic GetEnumerator * method directly from that interface reference. This saves * having to create our own iterator block here. */ - return ((IList>)this.source).GetEnumerator(); - } + => ((IList>)this.source).GetEnumerator(); - public override void Dispose() => this.View.Invalidate(); + public override void Dispose() + { + this.View.Invalidate(); + this.ReleaseAllocationTracking(); + } } } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs index af896ee0e..c4b22f156 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs @@ -60,6 +60,58 @@ internal abstract partial class MemoryGroup } } + internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) + { + if (this.groupLifetimeGuard != null) + { + // Pool-owned multi-buffer groups recover leaked handles through the group guard finalizer. + this.groupLifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes); + return; + } + + IMemoryOwner[]? memoryOwners = this.memoryOwners; + if (memoryOwners?.Length == 1 && memoryOwners[0] is AllocationTrackedMemoryManager trackedOwner) + { + // Single-buffer groups should release tracking with the buffer owner when that owner has + // a more precise lifetime, such as an existing pooled-resource finalizer. + trackedOwner.AttachAllocationTracking(allocator, lengthInBytes); + return; + } + + if (memoryOwners?.Length > 1) + { + foreach (IMemoryOwner memoryOwner in memoryOwners) + { + if (memoryOwner is not AllocationTrackedMemoryManager) + { + // Splitting is only valid when every segment can own its reservation. A single + // untracked segment makes the whole group ineligible, and this preflight has + // not attached anything yet, so the entire group can fall back immediately. + base.AttachAllocationTracking(allocator, lengthInBytes); + return; + } + } + + // Non-pool multi-buffer groups have no group-level finalizer, so each segment carries + // its own share of the reservation through the segment owner or its lifetime guard. + long remainingLengthInBytes = lengthInBytes; + int lastOwnerIndex = memoryOwners.Length - 1; + for (int i = 0; i < lastOwnerIndex; i++) + { + trackedOwner = (AllocationTrackedMemoryManager)memoryOwners[i]; + long ownerLengthInBytes = (long)trackedOwner.Memory.Length * Unsafe.SizeOf(); + trackedOwner.AttachAllocationTracking(allocator, ownerLengthInBytes); + remainingLengthInBytes -= ownerLengthInBytes; + } + + trackedOwner = (AllocationTrackedMemoryManager)memoryOwners[lastOwnerIndex]; + trackedOwner.AttachAllocationTracking(allocator, remainingLengthInBytes); + return; + } + + base.AttachAllocationTracking(allocator, lengthInBytes); + } + private static IMemoryOwner[] CreateBuffers( UnmanagedMemoryHandle[] pooledBuffers, int bufferLength, @@ -73,8 +125,8 @@ internal abstract partial class MemoryGroup result[i] = currentBuffer; } - ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer, options); - result[result.Length - 1] = lastBuffer; + ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[^1], sizeOfLastBuffer, options); + result[^1] = lastBuffer; return result; } @@ -155,6 +207,7 @@ internal abstract partial class MemoryGroup } } + this.ReleaseAllocationTracking(); this.memoryOwners = null; this.IsValid = false; this.groupLifetimeGuard = null; diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs index 6dd99fcb0..e0b9bca5e 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs @@ -21,6 +21,7 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable { private static readonly int ElementSize = Unsafe.SizeOf(); + private AllocationTrackingState allocationTracking; private MemoryGroupSpanCache memoryGroupSpanCache; private MemoryGroup(int bufferLength, long totalLength) @@ -52,16 +53,36 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable /// public abstract MemoryGroupEnumerator GetEnumerator(); + /// + /// Attaches allocation tracking by specifying the allocator and the length, in bytes, to be tracked. + /// + /// The memory allocator to use for tracking allocations. + /// The length, in bytes, of the memory region to track. Must be greater than or equal to zero. + /// + /// Intended for one-time initialization after the group has been created; callers should avoid changing + /// tracking state concurrently with disposal. + /// + internal virtual void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) => + this.allocationTracking.Attach(allocator, lengthInBytes); + + /// + /// Releases any resources or tracking information associated with allocation tracking for this instance. + /// + /// + /// This method is intended to be called when allocation tracking is no longer needed. It is safe + /// to call multiple times; subsequent calls after the first have no effect, even when called concurrently. + /// + internal void ReleaseAllocationTracking() => this.allocationTracking.Release(); + /// IEnumerator> IEnumerable>.GetEnumerator() - { + /* This method is implemented in each derived class. * Implementing the method here as non-abstract and throwing, * then reimplementing it explicitly in each derived class, is * a workaround for the lack of support for abstract explicit * interface method implementations in C#. */ - throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable>.GetEnumerator()"); - } + => throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable>.GetEnumerator()"); /// IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); @@ -81,8 +102,8 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable int bufferAlignmentInElements, AllocationOptions options = AllocationOptions.None) { - int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes(); Guard.NotNull(allocator, nameof(allocator)); + int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes(); if (totalLengthInElements < 0) { @@ -97,8 +118,8 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable if (totalLengthInElements == 0) { - IMemoryOwner[] buffers0 = [allocator.Allocate(0, options)]; - return new Owned(buffers0, 0, 0, true); + IMemoryOwner[] emptyBuffer = [allocator.AllocateGroupBuffer(0, options)]; + return new Owned(emptyBuffer, 0, 0, true); } int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements; @@ -123,12 +144,12 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable IMemoryOwner[] buffers = new IMemoryOwner[bufferCount]; for (int i = 0; i < buffers.Length - 1; i++) { - buffers[i] = allocator.Allocate(bufferLength, options); + buffers[i] = allocator.AllocateGroupBuffer(bufferLength, options); } if (bufferCount > 0) { - buffers[^1] = allocator.Allocate(sizeOfLastBuffer, options); + buffers[^1] = allocator.AllocateGroupBuffer(sizeOfLastBuffer, options); } return new Owned(buffers, bufferLength, totalLengthInElements, true); diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs index 81210f13d..724af35e1 100644 --- a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs +++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs @@ -39,4 +39,9 @@ public class InvalidMemoryOperationException : InvalidOperationException [DoesNotReturn] internal static void ThrowAllocationOverLimitException(ulong length, long limit) => throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={length} that exceeded the limit {limit}."); + + [DoesNotReturn] + internal static void ThrowAccumulativeAllocationOverLimitException(long requestedLength, long totalLength, long limit) => + throw new InvalidMemoryOperationException( + $"Attempted to allocate a buffer of length={requestedLength} that would increase the accumulative allocation size to {totalLength}, exceeding the limit {limit}."); } diff --git a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs index ff306e1e4..57ebcab76 100644 --- a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs +++ b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs @@ -29,6 +29,9 @@ public static class MemoryAllocatorExtensions AllocationOptions options = AllocationOptions.None) where T : struct { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + long groupLength = (long)width * height; MemoryGroup memoryGroup; if (preferContiguosImageBuffers && groupLength < int.MaxValue) @@ -104,6 +107,9 @@ public static class MemoryAllocatorExtensions AllocationOptions options = AllocationOptions.None) where T : struct { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + long groupLength = (long)width * height; MemoryGroup memoryGroup = memoryAllocator.AllocateGroup( groupLength, diff --git a/src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs b/src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs deleted file mode 100644 index bc3d17f8f..000000000 --- a/src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Memory; - -#pragma warning disable SA1649 // File name should match first type name -internal delegate void TransformItemsDelegate(ReadOnlySpan source, Span target); -#pragma warning restore SA1649 // File name should match first type name diff --git a/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs b/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs deleted file mode 100644 index d1ef51fb8..000000000 --- a/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Memory; - -internal delegate void TransformItemsInplaceDelegate(Span data); diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index 732e3eab2..659df01a6 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -403,6 +403,13 @@ internal sealed class ExifWriter return WriteUInt32((uint)longNumber, destination, offset); } + // ExifLong8Array retains ulong storage but reports Long when every value fits + // in 32 bits, allowing BigTIFF offsets to be serialized by classic EXIF writers. + if (value is ulong long8Value) + { + return WriteUInt32((uint)long8Value, destination, offset); + } + return WriteUInt32((uint)value, destination, offset); case ExifDataType.Long8: return WriteUInt64((ulong)value, destination, offset); diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs index 9e89d24ff..2f1b15b6b 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs @@ -20,82 +20,46 @@ internal sealed partial class IccDataReader public IccTagDataEntry ReadTagDataEntry(IccTagTableEntry info) { this.currentIndex = (int)info.Offset; - switch (this.ReadTagDataEntryHeader()) - { - case IccTypeSignature.Chromaticity: - return this.ReadChromaticityTagDataEntry(); - case IccTypeSignature.ColorantOrder: - return this.ReadColorantOrderTagDataEntry(); - case IccTypeSignature.ColorantTable: - return this.ReadColorantTableTagDataEntry(); - case IccTypeSignature.Curve: - return this.ReadCurveTagDataEntry(); - case IccTypeSignature.Data: - return this.ReadDataTagDataEntry(info.DataSize); - case IccTypeSignature.DateTime: - return this.ReadDateTimeTagDataEntry(); - case IccTypeSignature.Lut16: - return this.ReadLut16TagDataEntry(); - case IccTypeSignature.Lut8: - return this.ReadLut8TagDataEntry(); - case IccTypeSignature.LutAToB: - return this.ReadLutAtoBTagDataEntry(); - case IccTypeSignature.LutBToA: - return this.ReadLutBtoATagDataEntry(); - case IccTypeSignature.Measurement: - return this.ReadMeasurementTagDataEntry(); - case IccTypeSignature.MultiLocalizedUnicode: - return this.ReadMultiLocalizedUnicodeTagDataEntry(); - case IccTypeSignature.MultiProcessElements: - return this.ReadMultiProcessElementsTagDataEntry(); - case IccTypeSignature.NamedColor2: - return this.ReadNamedColor2TagDataEntry(); - case IccTypeSignature.ParametricCurve: - return this.ReadParametricCurveTagDataEntry(); - case IccTypeSignature.ProfileSequenceDesc: - return this.ReadProfileSequenceDescTagDataEntry(); - case IccTypeSignature.ProfileSequenceIdentifier: - return this.ReadProfileSequenceIdentifierTagDataEntry(); - case IccTypeSignature.ResponseCurveSet16: - return this.ReadResponseCurveSet16TagDataEntry(); - case IccTypeSignature.S15Fixed16Array: - return this.ReadFix16ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.Signature: - return this.ReadSignatureTagDataEntry(); - case IccTypeSignature.Text: - return this.ReadTextTagDataEntry(info.DataSize); - case IccTypeSignature.U16Fixed16Array: - return this.ReadUFix16ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.UInt16Array: - return this.ReadUInt16ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.UInt32Array: - return this.ReadUInt32ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.UInt64Array: - return this.ReadUInt64ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.UInt8Array: - return this.ReadUInt8ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.ViewingConditions: - return this.ReadViewingConditionsTagDataEntry(); - case IccTypeSignature.Xyz: - return this.ReadXyzTagDataEntry(info.DataSize); + return this.ReadTagDataEntryHeader() switch + { + IccTypeSignature.Chromaticity => this.ReadChromaticityTagDataEntry(), + IccTypeSignature.ColorantOrder => this.ReadColorantOrderTagDataEntry(), + IccTypeSignature.ColorantTable => this.ReadColorantTableTagDataEntry(), + IccTypeSignature.Curve => this.ReadCurveTagDataEntry(), + IccTypeSignature.Data => this.ReadDataTagDataEntry(info.DataSize), + IccTypeSignature.DateTime => this.ReadDateTimeTagDataEntry(), + IccTypeSignature.Lut16 => this.ReadLut16TagDataEntry(), + IccTypeSignature.Lut8 => this.ReadLut8TagDataEntry(), + IccTypeSignature.LutAToB => this.ReadLutAtoBTagDataEntry(), + IccTypeSignature.LutBToA => this.ReadLutBtoATagDataEntry(), + IccTypeSignature.Measurement => this.ReadMeasurementTagDataEntry(), + IccTypeSignature.MultiLocalizedUnicode => this.ReadMultiLocalizedUnicodeTagDataEntry(), + IccTypeSignature.MultiProcessElements => this.ReadMultiProcessElementsTagDataEntry(), + IccTypeSignature.NamedColor2 => this.ReadNamedColor2TagDataEntry(), + IccTypeSignature.ParametricCurve => this.ReadParametricCurveTagDataEntry(), + IccTypeSignature.ProfileSequenceDesc => this.ReadProfileSequenceDescTagDataEntry(), + IccTypeSignature.ProfileSequenceIdentifier => this.ReadProfileSequenceIdentifierTagDataEntry(), + IccTypeSignature.ResponseCurveSet16 => this.ReadResponseCurveSet16TagDataEntry(), + IccTypeSignature.S15Fixed16Array => this.ReadFix16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.Signature => this.ReadSignatureTagDataEntry(), + IccTypeSignature.Text => this.ReadTextTagDataEntry(info.DataSize), + IccTypeSignature.U16Fixed16Array => this.ReadUFix16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt16Array => this.ReadUInt16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt32Array => this.ReadUInt32ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt64Array => this.ReadUInt64ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt8Array => this.ReadUInt8ArrayTagDataEntry(info.DataSize), + IccTypeSignature.ViewingConditions => this.ReadViewingConditionsTagDataEntry(), + IccTypeSignature.Xyz => this.ReadXyzTagDataEntry(info.DataSize), // V2 Types: - case IccTypeSignature.TextDescription: - return this.ReadTextDescriptionTagDataEntry(); - case IccTypeSignature.CrdInfo: - return this.ReadCrdInfoTagDataEntry(); - case IccTypeSignature.Screening: - return this.ReadScreeningTagDataEntry(); - case IccTypeSignature.UcrBg: - return this.ReadUcrBgTagDataEntry(info.DataSize); + IccTypeSignature.TextDescription => this.ReadTextDescriptionTagDataEntry(), + IccTypeSignature.CrdInfo => this.ReadCrdInfoTagDataEntry(), + IccTypeSignature.Screening => this.ReadScreeningTagDataEntry(), + IccTypeSignature.UcrBg => this.ReadUcrBgTagDataEntry(info.DataSize), // Unsupported or unknown - case IccTypeSignature.DeviceSettings: - case IccTypeSignature.NamedColor: - case IccTypeSignature.Unknown: - default: - return this.ReadUnknownTagDataEntry(info.DataSize); - } + _ => this.ReadUnknownTagDataEntry(info.DataSize), + }; } /// @@ -477,7 +441,7 @@ internal sealed partial class IccDataReader return new IccMultiLocalizedUnicodeTagDataEntry(text); - CultureInfo ReadCulture(string language, string country) + static CultureInfo ReadCulture(string language, string country) { if (string.IsNullOrWhiteSpace(language)) { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index 05be3eb5d..eaba0a045 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -110,8 +110,8 @@ public sealed partial class IccProfile : IDeepCloneable // need to copy some values because they need to be zero for the hashing Span temp = stackalloc byte[24]; data.AsSpan(profileFlagPos, 4).CopyTo(temp); - data.AsSpan(renderingIntentPos, 4).CopyTo(temp.Slice(4)); - data.AsSpan(profileIdPos, 16).CopyTo(temp.Slice(8)); + data.AsSpan(renderingIntentPos, 4).CopyTo(temp[4..]); + data.AsSpan(profileIdPos, 16).CopyTo(temp[8..]); try { @@ -131,7 +131,7 @@ public sealed partial class IccProfile : IDeepCloneable } finally { - temp.Slice(0, 4).CopyTo(data.AsSpan(profileFlagPos)); + temp[..4].CopyTo(data.AsSpan(profileFlagPos)); temp.Slice(4, 4).CopyTo(data.AsSpan(renderingIntentPos)); temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos)); } diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs index 1938ad630..0f61857ba 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs @@ -140,7 +140,7 @@ internal sealed class IccChromaticityTagDataEntry : IccTagDataEntry, IEquatable< [0.155, 0.070] ]; default: - throw new ArgumentException("Unrecognized colorant encoding"); + throw new InvalidIccProfileException("Unrecognized colorant encoding"); } } diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs index 9bf323263..77bc45bd4 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs @@ -64,44 +64,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable @@ -165,7 +128,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + /// Compares two curve arrays, treating consistently. + /// private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves) { bool thisNull = thisCurves is null; @@ -202,7 +168,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable this.CurveB != null - && this.Matrix3x3 != null - && this.Matrix3x1 != null - && this.CurveM != null - && this.ClutValues != null - && this.CurveA != null; + /// + /// Validates the configured processing stages and derives the external channel counts. + /// + /// + /// Stages are evaluated in ICC mAB order: A, CLUT, M, Matrix, B. + /// Sparse pipelines are valid as long as adjacent stages agree on channel counts. + /// + private (int InputChannelCount, int OutputChannelCount) GetChannelCounts() + { + // There are at most five possible mAB stages: A, CLUT, M, Matrix, and B. + List<(int Input, int Output, string Name)> stages = new(5); - private bool IsMMatrixB() - => this.CurveB != null - && this.Matrix3x3 != null - && this.Matrix3x1 != null - && this.CurveM != null; + if (this.CurveA != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA)); + stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA))); + } - private bool IsAClutB() - => this.CurveB != null - && this.ClutValues != null - && this.CurveA != null; + if (this.ClutValues != null) + { + stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues))); + } - private bool IsB() => this.CurveB != null; + if (this.CurveM != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveM.Length, 1, 15, nameof(this.CurveM)); + stages.Add((this.CurveM.Length, this.CurveM.Length, nameof(this.CurveM))); + } + + if (this.Matrix3x3 != null || this.Matrix3x1 != null) + { + Guard.IsTrue(this.Matrix3x3 != null && this.Matrix3x1 != null, nameof(this.Matrix3x3), "Matrix must include both the 3x3 and 3x1 components"); + stages.Add((3, 3, nameof(this.Matrix3x3))); + } + if (this.CurveB != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB)); + stages.Add((this.CurveB.Length, this.CurveB.Length, nameof(this.CurveB))); + } + + Guard.IsTrue(stages.Count > 0, nameof(this.CurveB), "AToB tag must contain at least one processing element"); + + for (int i = 1; i < stages.Count; i++) + { + Guard.IsTrue( + stages[i - 1].Output == stages[i].Input, + stages[i].Name, + $"Output channel count of {stages[i - 1].Name} does not match input channel count of {stages[i].Name}"); + } + + return (stages[0].Input, stages[^1].Output); + } + + /// + /// Verifies that every supplied curve entry is a supported one-dimensional curve type. + /// private void VerifyCurve(IccTagDataEntry[] curves, string name) { if (curves != null) @@ -240,6 +242,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + /// Verifies the dimensions of the optional matrix components. + /// private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1) { if (matrix3x1 != null) @@ -254,6 +259,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + /// Creates the one-dimensional matrix vector when present. + /// private static Vector3? CreateMatrix3x1(float[] matrix) { if (matrix is null) @@ -264,6 +272,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + /// Creates the three-by-three matrix when present. + /// private static Matrix4x4? CreateMatrix3x3(float[,] matrix) { if (matrix is null) diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs index 033b80989..37e7b408d 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs @@ -64,44 +64,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable @@ -165,7 +128,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + /// Compares two curve arrays, treating consistently. + /// private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves) { bool thisNull = thisCurves is null; @@ -201,7 +167,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable this.CurveB != null && this.Matrix3x3 != null && this.Matrix3x1 != null && this.CurveM != null && this.ClutValues != null && this.CurveA != null; + /// + /// Validates the configured processing stages and derives the external channel counts. + /// + /// + /// Stages are evaluated in ICC mBA order: B, Matrix, M, CLUT, A. + /// Sparse pipelines are valid as long as adjacent stages agree on channel counts. + /// + private (int InputChannelCount, int OutputChannelCount) GetChannelCounts() + { + // There are at most five possible mBA stages: B, Matrix, M, CLUT, and A. + List<(int Input, int Output, string Name)> stages = new(5); - private bool IsBMatrixM() - => this.CurveB != null && this.Matrix3x3 != null && this.Matrix3x1 != null && this.CurveM != null; + if (this.CurveB != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB)); + stages.Add((this.CurveB.Length, this.CurveB.Length, nameof(this.CurveB))); + } - private bool IsBClutA() - => this.CurveB != null && this.ClutValues != null && this.CurveA != null; + if (this.Matrix3x3 != null || this.Matrix3x1 != null) + { + Guard.IsTrue(this.Matrix3x3 != null && this.Matrix3x1 != null, nameof(this.Matrix3x3), "Matrix must include both the 3x3 and 3x1 components"); + stages.Add((3, 3, nameof(this.Matrix3x3))); + } - private bool IsB() => this.CurveB != null; + if (this.CurveM != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveM.Length, 1, 15, nameof(this.CurveM)); + stages.Add((this.CurveM.Length, this.CurveM.Length, nameof(this.CurveM))); + } + + if (this.ClutValues != null) + { + stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues))); + } + if (this.CurveA != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA)); + stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA))); + } + + Guard.IsTrue(stages.Count > 0, nameof(this.CurveB), "BToA tag must contain at least one processing element"); + + for (int i = 1; i < stages.Count; i++) + { + Guard.IsTrue( + stages[i - 1].Output == stages[i].Input, + stages[i].Name, + $"Output channel count of {stages[i - 1].Name} does not match input channel count of {stages[i].Name}"); + } + + return (stages[0].Input, stages[^1].Output); + } + + /// + /// Verifies that every supplied curve entry is a supported one-dimensional curve type. + /// private void VerifyCurve(IccTagDataEntry[] curves, string name) { if (curves != null) @@ -229,6 +241,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + /// Verifies the dimensions of the optional matrix components. + ///
private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1) { if (matrix3x1 != null) @@ -243,6 +258,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + /// Creates the one-dimensional matrix vector when present. + ///
private static Vector3? CreateMatrix3x1(float[] matrix) { if (matrix is null) @@ -253,6 +271,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + /// Creates the three-by-three matrix when present. + ///
private static Matrix4x4? CreateMatrix3x3(float[,] matrix) { if (matrix is null) diff --git a/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs b/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs index 5f07e5589..fb47409be 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs @@ -23,13 +23,8 @@ internal readonly struct IccLut : IEquatable { Guard.NotNull(values, nameof(values)); - const float max = ushort.MaxValue; - this.Values = new float[values.Length]; - for (int i = 0; i < values.Length; i++) - { - this.Values[i] = values[i] / max; - } + IccLutNormalizer.Normalize(values, this.Values); } /// @@ -40,13 +35,8 @@ internal readonly struct IccLut : IEquatable { Guard.NotNull(values, nameof(values)); - const float max = byte.MaxValue; - this.Values = new float[values.Length]; - for (int i = 0; i < values.Length; i++) - { - this.Values[i] = values[i] / max; - } + IccLutNormalizer.Normalize(values, this.Values); } /// diff --git a/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLutNormalizer.cs b/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLutNormalizer.cs new file mode 100644 index 000000000..67796cad3 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLutNormalizer.cs @@ -0,0 +1,253 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; + +/// +/// Converts integer ICC lookup-table entries to their normalized single-precision representation. +/// +internal static class IccLutNormalizer +{ + /// + /// Defines the scalar and SIMD conversion for an integer lookup-table element type. + /// + /// The integer element type. + private interface INormalizeOperator + where T : unmanaged + { + /// + /// Gets the divisor that maps the integer range to [0, 1]. + /// + public static abstract float Divisor { get; } + + /// + /// Converts one scalar value. + /// + /// The integer value. + /// The normalized value. + public static abstract float Invoke(T source); + + /// + /// Converts one 128-bit input vector and stores the expanded single-precision results. + /// + /// The packed integer values. + /// The normalization divisor. + /// The first destination element. + public static abstract void Invoke(Vector128 source, Vector128 divisor, ref float destination); + + /// + /// Converts one 256-bit input vector and stores the expanded single-precision results. + /// + /// The packed integer values. + /// The normalization divisor. + /// The first destination element. + public static abstract void Invoke(Vector256 source, Vector256 divisor, ref float destination); + + /// + /// Converts one 512-bit input vector and stores the expanded single-precision results. + /// + /// The packed integer values. + /// The normalization divisor. + /// The first destination element. + public static abstract void Invoke(Vector512 source, Vector512 divisor, ref float destination); + } + + /// + /// Converts byte lookup-table entries to normalized single-precision values. + /// + /// The integer lookup-table entries. + /// The normalized destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Normalize(ReadOnlySpan source, Span destination) + => Normalize(source, destination); + + /// + /// Converts unsigned-short lookup-table entries to normalized single-precision values. + /// + /// The integer lookup-table entries. + /// The normalized destination values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Normalize(ReadOnlySpan source, Span destination) + => Normalize(source, destination); + + /// + /// Converts an integer lookup table using the widest available portable SIMD width, followed by narrower + /// widths and a scalar remainder. + /// + /// The integer element type. + /// The conversion implementation. + /// The integer lookup-table entries. + /// The normalized destination values. + private static void Normalize(ReadOnlySpan source, Span destination) + where T : unmanaged + where TOperator : struct, INormalizeOperator + { + ref T sourceRef = ref MemoryMarshal.GetReference(source); + ref float destinationRef = ref MemoryMarshal.GetReference(destination); + nuint length = (uint)source.Length; + nuint index = 0; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 divisor = Vector512.Create(TOperator.Divisor); + nuint count = (uint)Vector512.Count; + + while (length - index >= count) + { + ref float destinationStart = ref Unsafe.Add(ref destinationRef, index); + TOperator.Invoke(Vector512.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart); + index += count; + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 divisor = Vector256.Create(TOperator.Divisor); + nuint count = (uint)Vector256.Count; + + while (length - index >= count) + { + ref float destinationStart = ref Unsafe.Add(ref destinationRef, index); + TOperator.Invoke(Vector256.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart); + index += count; + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 divisor = Vector128.Create(TOperator.Divisor); + nuint count = (uint)Vector128.Count; + + while (length - index >= count) + { + ref float destinationStart = ref Unsafe.Add(ref destinationRef, index); + TOperator.Invoke(Vector128.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart); + index += count; + } + } + + // Preserve the scalar division expression for the final partial vector. Multiplication by a reciprocal + // is not bit-equivalent for every input and would change the values stored in the ICC profile model. + while (index < length) + { + Unsafe.Add(ref destinationRef, index) = TOperator.Invoke(Unsafe.Add(ref sourceRef, index)); + index++; + } + } + + /// + /// Converts packed byte entries to normalized single-precision values. + /// + private readonly struct ByteNormalizeOperator : INormalizeOperator + { + /// + public static float Divisor => byte.MaxValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Invoke(byte source) + => source / (float)byte.MaxValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Invoke(Vector128 source, Vector128 divisor, ref float destination) + { + // [b0..b15] becomes four ordered groups of four UInt32 values. Every widened value is at most + // 255, so reinterpreting UInt32 as Int32 before conversion preserves its numeric value. + (Vector128 lower16, Vector128 upper16) = Vector128.Widen(source); + (Vector128 values0, Vector128 values1) = Vector128.Widen(lower16); + (Vector128 values2, Vector128 values3) = Vector128.Widen(upper16); + + (Vector128.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination); + (Vector128.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 4); + (Vector128.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 8); + (Vector128.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 12); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Invoke(Vector256 source, Vector256 divisor, ref float destination) + { + // [b0..b31] becomes four ordered groups of eight UInt32 values, matching four contiguous + // Vector256 stores without shuffling the converted results. + (Vector256 lower16, Vector256 upper16) = Vector256.Widen(source); + (Vector256 values0, Vector256 values1) = Vector256.Widen(lower16); + (Vector256 values2, Vector256 values3) = Vector256.Widen(upper16); + + (Vector256.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination); + (Vector256.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 8); + (Vector256.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 16); + (Vector256.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 24); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Invoke(Vector512 source, Vector512 divisor, ref float destination) + { + // [b0..b63] becomes four ordered groups of sixteen UInt32 values, matching four contiguous + // Vector512 stores. The portable widening APIs map to zero-extension instructions. + (Vector512 lower16, Vector512 upper16) = Vector512.Widen(source); + (Vector512 values0, Vector512 values1) = Vector512.Widen(lower16); + (Vector512 values2, Vector512 values3) = Vector512.Widen(upper16); + + (Vector512.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination); + (Vector512.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 16); + (Vector512.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 32); + (Vector512.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 48); + } + } + + /// + /// Converts packed unsigned-short entries to normalized single-precision values. + /// + private readonly struct UInt16NormalizeOperator : INormalizeOperator + { + /// + public static float Divisor => ushort.MaxValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Invoke(ushort source) + => source / (float)ushort.MaxValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Invoke(Vector128 source, Vector128 divisor, ref float destination) + { + // [u0..u7] becomes two ordered groups of four UInt32 values. Every value is at most 65535, + // so signed conversion after reinterpretation is numerically identical to unsigned conversion. + (Vector128 lower, Vector128 upper) = Vector128.Widen(source); + + (Vector128.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination); + (Vector128.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 4); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Invoke(Vector256 source, Vector256 divisor, ref float destination) + { + // [u0..u15] becomes two ordered groups of eight UInt32 values, matching two contiguous + // Vector256 stores without a result shuffle. + (Vector256 lower, Vector256 upper) = Vector256.Widen(source); + + (Vector256.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination); + (Vector256.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 8); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Invoke(Vector512 source, Vector512 divisor, ref float destination) + { + // [u0..u31] becomes two ordered groups of sixteen UInt32 values, matching two contiguous + // Vector512 stores. The portable widening APIs map to zero-extension instructions. + (Vector512 lower, Vector512 upper) = Vector512.Widen(source); + + (Vector512.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination); + (Vector512.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 16); + } + } +} diff --git a/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs b/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs new file mode 100644 index 000000000..ad26eb4e6 --- /dev/null +++ b/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs @@ -0,0 +1,387 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides bulk operations for pixel formats that store associated alpha. +/// +/// The associated-alpha pixel format. +public abstract class AssociatedAlphaPixelOperations : PixelOperations + where TPixel : unmanaged, IPixel +{ + /// + public override PixelBlender GetPixelBlender(PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode) + => AssociatedAlphaPixelBlenders.GetPixelBlender(colorMode, alphaMode); + + /// + protected abstract override void ToUnassociatedVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination); + + /// + protected abstract override void ToAssociatedVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination); + + /// + protected abstract override void FromUnassociatedVector4Destructive( + Configuration configuration, + Span source, + Span destination); + + /// + protected abstract override void FromAssociatedVector4Destructive( + Configuration configuration, + Span source, + Span destination); + + /// + protected abstract override void ToUnassociatedScaledVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination); + + /// + protected abstract override void ToAssociatedScaledVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination); + + /// + protected abstract override void FromUnassociatedScaledVector4Destructive( + Configuration configuration, + Span source, + Span destination); + + /// + protected abstract override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination); + + /// + public override void From( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + if (source.IsEmpty) + { + return; + } + + // Cap large conversions at 1,024 vectors while avoiding a 16 KiB rental for short spans. + int sliceLength = Math.Min(source.Length, 1024); + int numberOfSlices = source.Length / sliceLength; + + using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength); + Span vectorSpan = tempVectors.GetSpan()[..sliceLength]; + + // Convert through unassociated vectors so the destination operation can quantize alpha to its own storage before associating RGB. + for (int i = 0; i < numberOfSlices; i++) + { + int start = i * sliceLength; + ReadOnlySpan sourceSlice = source.Slice(start, sliceLength); + Span destinationSlice = destination.Slice(start, sliceLength); + PixelOperations.Instance.ToVector4( + configuration, + sourceSlice, + vectorSpan, + PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + this.FromUnassociatedScaledVector4Destructive(configuration, vectorSpan, destinationSlice); + } + + int endOfCompleteSlices = numberOfSlices * sliceLength; + int remainder = source.Length - endOfCompleteSlices; + + if (remainder > 0) + { + ReadOnlySpan sourceSlice = source[endOfCompleteSlices..]; + Span destinationSlice = destination.Slice(endOfCompleteSlices, remainder); + vectorSpan = vectorSpan[..remainder]; + PixelOperations.Instance.ToVector4( + configuration, + sourceSlice, + vectorSpan, + PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + this.FromUnassociatedScaledVector4Destructive(configuration, vectorSpan, destinationSlice); + } + } + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span destination, + PixelConversionModifiers modifiers) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(sourceVectors, destination, nameof(destination)); + + bool associated = modifiers.IsDefined(PixelConversionModifiers.Premultiply) || !modifiers.IsDefined(PixelConversionModifiers.UnPremultiply); + bool scaled = modifiers.IsDefined(PixelConversionModifiers.Scale); + + if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand)) + { + // Transfer functions operate on straight color components. Associated input must therefore be unassociated before companding. + if (associated) + { + Numerics.UnPremultiply(sourceVectors); + } + + SRgbCompanding.Compress(sourceVectors); + + if (scaled) + { + this.FromUnassociatedScaledVector4Destructive(configuration, sourceVectors, destination); + } + else + { + this.FromUnassociatedVector4Destructive(configuration, sourceVectors, destination); + } + + return; + } + + if (scaled) + { + if (associated) + { + this.FromAssociatedScaledVector4Destructive(configuration, sourceVectors, destination); + } + else + { + this.FromUnassociatedScaledVector4Destructive(configuration, sourceVectors, destination); + } + } + else if (associated) + { + this.FromAssociatedVector4Destructive(configuration, sourceVectors, destination); + } + else + { + this.FromUnassociatedVector4Destructive(configuration, sourceVectors, destination); + } + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan source, + Span destinationVectors, + PixelConversionModifiers modifiers) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destinationVectors, nameof(destinationVectors)); + + bool associated = modifiers.IsDefined(PixelConversionModifiers.Premultiply) || !modifiers.IsDefined(PixelConversionModifiers.UnPremultiply); + bool scaled = modifiers.IsDefined(PixelConversionModifiers.Scale); + + if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand)) + { + // Extract straight color before applying the transfer function; companding associated components would make RGB depend on alpha. + if (scaled) + { + this.ToUnassociatedScaledVector4(configuration, source, destinationVectors); + } + else + { + this.ToUnassociatedVector4(configuration, source, destinationVectors); + } + + Span converted = destinationVectors[..source.Length]; + SRgbCompanding.Expand(converted); + + if (associated) + { + Numerics.Premultiply(converted); + } + + return; + } + + if (scaled) + { + if (associated) + { + this.ToAssociatedScaledVector4(configuration, source, destinationVectors); + } + else + { + this.ToUnassociatedScaledVector4(configuration, source, destinationVectors); + } + } + else if (associated) + { + this.ToAssociatedVector4(configuration, source, destinationVectors); + } + else + { + this.ToUnassociatedVector4(configuration, source, destinationVectors); + } + } + + /// + public override void FromArgb32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromAbgr32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromBgr24(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromBgra32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromL8(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromL16(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromLa16(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromLa32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromRgb24(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromRgba32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromRgb48(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromRgba64(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void FromBgra5551(Configuration configuration, ReadOnlySpan source, Span destination) + => this.From(configuration, source, destination); + + /// + public override void ToArgb32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToAbgr32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToBgr24(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToBgra32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToL8(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToL16(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToLa16(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToLa32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToRgb24(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToRgba32(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToRgb48(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToRgba64(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + public override void ToBgra5551(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ConvertToUnassociated(configuration, source, destination); + + /// + /// Converts associated source pixels to an unassociated destination format. + /// + /// The destination pixel format. + /// The configuration. + /// The source pixels. + /// The destination pixels. + private void ConvertToUnassociated( + Configuration configuration, + ReadOnlySpan source, + Span destination) + where TDestinationPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + if (source.IsEmpty) + { + return; + } + + int sliceLength = Math.Min(source.Length, 1024); + int numberOfSlices = source.Length / sliceLength; + + using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength); + Span vectorSpan = tempVectors.GetSpan()[..sliceLength]; + PixelOperations destinationOperations = PixelOperations.Instance; + + // Generated destination dispatch routes conversion back through this source operation's ToX override. Extract through this + // operation's protected bulk hook, then use the destination's public modifier contract to avoid recursive dispatch. + for (int i = 0; i < numberOfSlices; i++) + { + int start = i * sliceLength; + ReadOnlySpan sourceSlice = source.Slice(start, sliceLength); + Span destinationSlice = destination.Slice(start, sliceLength); + this.ToUnassociatedScaledVector4(configuration, sourceSlice, vectorSpan); + destinationOperations.FromVector4Destructive(configuration, vectorSpan, destinationSlice, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + } + + int endOfCompleteSlices = numberOfSlices * sliceLength; + int remainder = source.Length - endOfCompleteSlices; + + if (remainder > 0) + { + ReadOnlySpan sourceSlice = source[endOfCompleteSlices..]; + Span destinationSlice = destination.Slice(endOfCompleteSlices, remainder); + vectorSpan = vectorSpan[..remainder]; + this.ToUnassociatedScaledVector4(configuration, sourceSlice, vectorSpan); + destinationOperations.FromVector4Destructive(configuration, vectorSpan, destinationSlice, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + } + } +} diff --git a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs index 02936f160..53f22eb09 100644 --- a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs +++ b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; @@ -10,6 +12,27 @@ namespace SixLabors.ImageSharp.PixelFormats; /// internal static class HalfTypeHelper { + // IEEE 754 binary16 has a largest finite magnitude of 65504. Scaled pixel vectors map that complete finite + // interval to [0, 1], while native vectors continue to expose the stored floating-point value directly. + internal const float FiniteMinimum = -65504F; + internal const float FiniteMaximum = 65504F; + internal const float FiniteRange = FiniteMaximum - FiniteMinimum; + internal const float InverseFiniteRange = (float)(1D / FiniteRange); + internal const float ScaledMidpoint = .5F; + + // These constants mirror the binary16 conversion used by System.Half. Keeping the vector conversion + // bit-for-bit equivalent to the scalar runtime conversion makes SIMD a pure throughput optimization. + private const uint HalfExponentMask = 0x7C00; + private const uint HalfSignMask = 0x8000; + private const uint HalfToSingleBitsMask = 0x0FFF_E000; + private const uint SingleExponentLowerBound = 0x3880_0000; + private const uint SingleExponentOffset = 0x3800_0000; + private const uint SingleExponent126 = 0x3F00_0000; + private const uint SingleBiasedExponentMask = 0x7F80_0000; + private const uint SingleExponent13 = 0x0680_0000; + private const uint SingleSignMask = 0x8000_0000; + private const float MaxHalfValueBelowInfinity = 65520F; + /// /// Packs a into an /// @@ -25,4 +48,304 @@ internal static class HalfTypeHelper /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value); + + /// + /// Normalizes a finite binary16 value to the scaled pixel range. + /// + /// The native binary16 value represented as a . + /// The normalized value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static float ToScaled(float value) => (value * InverseFiniteRange) + ScaledMidpoint; + + /// + /// Normalizes finite binary16 values to the scaled pixel range. + /// + /// The native binary16 values. + /// The normalized values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint); + + /// + /// Normalizes finite binary16 values to the scaled pixel range. + /// + /// The native binary16 values. + /// The normalized values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint); + + /// + /// Expands a normalized value to the finite binary16 range. + /// + /// The normalized value. + /// The native binary16 value represented as a . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static float FromScaled(float value) => (value * FiniteRange) + FiniteMinimum; + + /// + /// Expands normalized values to the finite binary16 range. + /// + /// The normalized values. + /// The native binary16 values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum); + + /// + /// Expands normalized values to the finite binary16 range. + /// + /// The normalized values. + /// The native binary16 values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum); + + /// + /// Unpacks eight binary16 values into two vectors of single-precision values. + /// + /// The packed binary16 values. + /// The unpacked lower and upper values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static (Vector128 Lower, Vector128 Upper) Unpack(Vector128 value) + { + (Vector128 lower, Vector128 upper) = Vector128.Widen(value); + return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); + } + + /// + /// Unpacks sixteen binary16 values into two vectors of single-precision values. + /// + /// The packed binary16 values. + /// The unpacked lower and upper values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static (Vector256 Lower, Vector256 Upper) Unpack(Vector256 value) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(value); + return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); + } + + /// + /// Unpacks thirty-two binary16 values into two vectors of single-precision values. + /// + /// The packed binary16 values. + /// The unpacked lower and upper values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static (Vector512 Lower, Vector512 Upper) Unpack(Vector512 value) + { + (Vector512 lower, Vector512 upper) = Vector512.Widen(value); + return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper)); + } + + /// + /// Packs eight single-precision values into binary16 storage. + /// + /// The lower single-precision values. + /// The upper single-precision values. + /// The packed binary16 values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector128 Pack(Vector128 lower, Vector128 upper) + => Vector128.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); + + /// + /// Packs sixteen single-precision values into binary16 storage. + /// + /// The lower single-precision values. + /// The upper single-precision values. + /// The packed binary16 values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector256 Pack(Vector256 lower, Vector256 upper) + => Vector256.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); + + /// + /// Packs thirty-two single-precision values into binary16 storage. + /// + /// The lower single-precision values. + /// The upper single-precision values. + /// The packed binary16 values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector512 Pack(Vector512 lower, Vector512 upper) + => Vector512.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper)); + + /// + /// Rounds single-precision values through binary16 without changing the vector width. + /// + /// The single-precision values. + /// The values after binary16 quantization. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector128 RoundToHalf(Vector128 value) + => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value)); + + /// + /// Rounds single-precision values through binary16 without changing the vector width. + /// + /// The single-precision values. + /// The values after binary16 quantization. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector256 RoundToHalf(Vector256 value) + => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value)); + + /// + /// Rounds single-precision values through binary16 without changing the vector width. + /// + /// The single-precision values. + /// The values after binary16 quantization. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector512 RoundToHalf(Vector512 value) + => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value)); + + /// + /// Converts zero-extended binary16 bit patterns to single-precision values. + /// + /// The binary16 bit patterns. + /// The converted single-precision values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ConvertHalfBitsToSingle(Vector128 value) + { + Vector128 sign = Vector128.ShiftLeft(value & Vector128.Create(HalfSignMask), 16); + Vector128 exponent = value & Vector128.Create(HalfExponentMask); + Vector128 subnormalMask = Vector128.Equals(exponent, Vector128.Zero); + Vector128 infinityOrNaNMask = Vector128.Equals(exponent, Vector128.Create(HalfExponentMask)); + Vector128 maskedExponentLowerBound = subnormalMask & Vector128.Create(SingleExponentLowerBound); + Vector128 exponentOffset = Vector128.Create(SingleExponentOffset) | maskedExponentLowerBound; + + // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values + // need different exponent offsets before that shared field layout can be reinterpreted as float. + Vector128 bits = Vector128.ShiftLeft(value, 13) & Vector128.Create(HalfToSingleBitsMask); + exponentOffset = Vector128.ConditionalSelect(infinityOrNaNMask, Vector128.ShiftLeft(exponentOffset, 1), exponentOffset); + bits += exponentOffset; + Vector128 absoluteValue = (bits.AsSingle() - maskedExponentLowerBound.AsSingle()).AsUInt32(); + return (absoluteValue | sign).AsSingle(); + } + + /// + /// Converts zero-extended binary16 bit patterns to single-precision values. + /// + /// The binary16 bit patterns. + /// The converted single-precision values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConvertHalfBitsToSingle(Vector256 value) + { + Vector256 sign = Vector256.ShiftLeft(value & Vector256.Create(HalfSignMask), 16); + Vector256 exponent = value & Vector256.Create(HalfExponentMask); + Vector256 subnormalMask = Vector256.Equals(exponent, Vector256.Zero); + Vector256 infinityOrNaNMask = Vector256.Equals(exponent, Vector256.Create(HalfExponentMask)); + Vector256 maskedExponentLowerBound = subnormalMask & Vector256.Create(SingleExponentLowerBound); + Vector256 exponentOffset = Vector256.Create(SingleExponentOffset) | maskedExponentLowerBound; + + // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values + // need different exponent offsets before that shared field layout can be reinterpreted as float. + Vector256 bits = Vector256.ShiftLeft(value, 13) & Vector256.Create(HalfToSingleBitsMask); + exponentOffset = Vector256.ConditionalSelect(infinityOrNaNMask, Vector256.ShiftLeft(exponentOffset, 1), exponentOffset); + bits += exponentOffset; + Vector256 absoluteValue = (bits.AsSingle() - maskedExponentLowerBound.AsSingle()).AsUInt32(); + return (absoluteValue | sign).AsSingle(); + } + + /// + /// Converts zero-extended binary16 bit patterns to single-precision values. + /// + /// The binary16 bit patterns. + /// The converted single-precision values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ConvertHalfBitsToSingle(Vector512 value) + { + Vector512 sign = Vector512.ShiftLeft(value & Vector512.Create(HalfSignMask), 16); + Vector512 exponent = value & Vector512.Create(HalfExponentMask); + Vector512 subnormalMask = Vector512.Equals(exponent, Vector512.Zero); + Vector512 infinityOrNaNMask = Vector512.Equals(exponent, Vector512.Create(HalfExponentMask)); + Vector512 maskedExponentLowerBound = subnormalMask & Vector512.Create(SingleExponentLowerBound); + Vector512 exponentOffset = Vector512.Create(SingleExponentOffset) | maskedExponentLowerBound; + + // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values + // need different exponent offsets before that shared field layout can be reinterpreted as float. + Vector512 bits = Vector512.ShiftLeft(value, 13) & Vector512.Create(HalfToSingleBitsMask); + exponentOffset = Vector512.ConditionalSelect(infinityOrNaNMask, Vector512.ShiftLeft(exponentOffset, 1), exponentOffset); + bits += exponentOffset; + Vector512 absoluteValue = (bits.AsSingle() - maskedExponentLowerBound.AsSingle()).AsUInt32(); + return (absoluteValue | sign).AsSingle(); + } + + /// + /// Converts single-precision values to zero-extended binary16 bit patterns. + /// + /// The single-precision values. + /// The binary16 bit patterns in 32-bit lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ConvertSingleToHalfBits(Vector128 value) + { + Vector128 bits = value.AsUInt32(); + Vector128 sign = Vector128.ShiftRightLogical(bits & Vector128.Create(SingleSignMask), 16); + Vector128 realMask = Vector128.Equals(value, value).AsUInt32(); + value = Vector128.Abs(value); + value = Vector128.Min(Vector128.Create(MaxHalfValueBelowInfinity), value); + Vector128 exponentOffset = Vector128.Max(value, Vector128.Create(SingleExponentLowerBound).AsSingle()).AsUInt32(); + exponentOffset &= Vector128.Create(SingleBiasedExponentMask); + exponentOffset += Vector128.Create(SingleExponent13); + + // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE + // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields. + value += exponentOffset.AsSingle(); + bits = value.AsUInt32() - Vector128.Create(SingleExponent126); + Vector128 newExponent = Vector128.ShiftRightLogical(bits, 13); + Vector128 maskedHalfExponentForNaN = ~realMask & Vector128.Create(HalfExponentMask); + bits &= realMask; + bits += newExponent; + bits &= ~maskedHalfExponentForNaN; + return bits | maskedHalfExponentForNaN | sign; + } + + /// + /// Converts single-precision values to zero-extended binary16 bit patterns. + /// + /// The single-precision values. + /// The binary16 bit patterns in 32-bit lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConvertSingleToHalfBits(Vector256 value) + { + Vector256 bits = value.AsUInt32(); + Vector256 sign = Vector256.ShiftRightLogical(bits & Vector256.Create(SingleSignMask), 16); + Vector256 realMask = Vector256.Equals(value, value).AsUInt32(); + value = Vector256.Abs(value); + value = Vector256.Min(Vector256.Create(MaxHalfValueBelowInfinity), value); + Vector256 exponentOffset = Vector256.Max(value, Vector256.Create(SingleExponentLowerBound).AsSingle()).AsUInt32(); + exponentOffset &= Vector256.Create(SingleBiasedExponentMask); + exponentOffset += Vector256.Create(SingleExponent13); + + // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE + // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields. + value += exponentOffset.AsSingle(); + bits = value.AsUInt32() - Vector256.Create(SingleExponent126); + Vector256 newExponent = Vector256.ShiftRightLogical(bits, 13); + Vector256 maskedHalfExponentForNaN = ~realMask & Vector256.Create(HalfExponentMask); + bits &= realMask; + bits += newExponent; + bits &= ~maskedHalfExponentForNaN; + return bits | maskedHalfExponentForNaN | sign; + } + + /// + /// Converts single-precision values to zero-extended binary16 bit patterns. + /// + /// The single-precision values. + /// The binary16 bit patterns in 32-bit lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ConvertSingleToHalfBits(Vector512 value) + { + Vector512 bits = value.AsUInt32(); + Vector512 sign = Vector512.ShiftRightLogical(bits & Vector512.Create(SingleSignMask), 16); + Vector512 realMask = Vector512.Equals(value, value).AsUInt32(); + value = Vector512.Abs(value); + value = Vector512.Min(Vector512.Create(MaxHalfValueBelowInfinity), value); + Vector512 exponentOffset = Vector512.Max(value, Vector512.Create(SingleExponentLowerBound).AsSingle()).AsUInt32(); + exponentOffset &= Vector512.Create(SingleBiasedExponentMask); + exponentOffset += Vector512.Create(SingleExponent13); + + // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE + // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields. + value += exponentOffset.AsSingle(); + bits = value.AsUInt32() - Vector512.Create(SingleExponent126); + Vector512 newExponent = Vector512.ShiftRightLogical(bits, 13); + Vector512 maskedHalfExponentForNaN = ~realMask & Vector512.Create(HalfExponentMask); + bits &= realMask; + bits += newExponent; + bits &= ~maskedHalfExponentForNaN; + return bits | maskedHalfExponentForNaN | sign; + } } diff --git a/src/ImageSharp/PixelFormats/IPixel.cs b/src/ImageSharp/PixelFormats/IPixel.cs index 528b3e76d..a4f6312dd 100644 --- a/src/ImageSharp/PixelFormats/IPixel.cs +++ b/src/ImageSharp/PixelFormats/IPixel.cs @@ -14,120 +14,150 @@ namespace SixLabors.ImageSharp.PixelFormats; public interface IPixel : IPixel, IEquatable where TSelf : unmanaged, IPixel { -#pragma warning disable CA1000 // Do not declare static members on generic types /// /// Creates a instance for this pixel type. /// This method is not intended to be consumed directly. Use instead. /// /// The instance. - static abstract PixelOperations CreatePixelOperations(); + public static abstract PixelOperations CreatePixelOperations(); /// - /// Initializes the pixel instance from a generic a generic ("scaled") representation - /// with values scaled and clamped between 0 and 1 + /// Initializes the pixel instance from a generic ("scaled") representation using the pixel type's native alpha representation. + /// The scaled representation uses 0 and 1 as its nominal component bounds. /// /// The vector to load the pixel from. /// The . - static abstract TSelf FromScaledVector4(Vector4 source); + public static abstract TSelf FromScaledVector4(Vector4 source); /// - /// Initializes the pixel instance from a which is specific to the current pixel type. + /// Initializes the pixel instance from a generic ("scaled") whose color components use unassociated alpha. + /// The scaled representation uses 0 and 1 as its nominal component bounds. /// /// The vector to load the pixel from. /// The . - static abstract TSelf FromVector4(Vector4 source); + public static abstract TSelf FromUnassociatedScaledVector4(Vector4 source); + + /// + /// Initializes the pixel instance from a generic ("scaled") whose color components use associated alpha, + /// representing color multiplied by the logical opacity represented by alpha. + /// The scaled representation uses 0 and 1 as its nominal component bounds. + /// + /// The vector to load the pixel from. + /// The . + public static abstract TSelf FromAssociatedScaledVector4(Vector4 source); + + /// + /// Initializes the pixel instance from a which is specific to the current pixel type and uses its native alpha representation. + /// + /// The vector to load the pixel from. + /// The . + public static abstract TSelf FromVector4(Vector4 source); + + /// + /// Initializes the pixel instance from a which is specific to the current pixel type and whose color components use unassociated alpha. + /// + /// The vector to load the pixel from. + /// The . + public static abstract TSelf FromUnassociatedVector4(Vector4 source); + + /// + /// Initializes the pixel instance from a which is specific to the current pixel type and whose color components use associated alpha, + /// representing color multiplied by the logical opacity represented by alpha. + /// + /// The vector to load the pixel from. + /// The . + public static abstract TSelf FromAssociatedVector4(Vector4 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromAbgr32(Abgr32 source); + public static abstract TSelf FromAbgr32(Abgr32 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromArgb32(Argb32 source); + public static abstract TSelf FromArgb32(Argb32 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromBgra5551(Bgra5551 source); + public static abstract TSelf FromBgra5551(Bgra5551 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromBgr24(Bgr24 source); + public static abstract TSelf FromBgr24(Bgr24 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromBgra32(Bgra32 source); + public static abstract TSelf FromBgra32(Bgra32 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromL8(L8 source); + public static abstract TSelf FromL8(L8 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromL16(L16 source); + public static abstract TSelf FromL16(L16 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromLa16(La16 source); + public static abstract TSelf FromLa16(La16 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromLa32(La32 source); + public static abstract TSelf FromLa32(La32 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromRgb24(Rgb24 source); + public static abstract TSelf FromRgb24(Rgb24 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromRgba32(Rgba32 source); + public static abstract TSelf FromRgba32(Rgba32 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromRgb48(Rgb48 source); + public static abstract TSelf FromRgb48(Rgb48 source); /// /// Initializes the pixel instance from an value. /// /// The value. /// The . - static abstract TSelf FromRgba64(Rgba64 source); -#pragma warning restore CA1000 // Do not declare static members on generic types + public static abstract TSelf FromRgba64(Rgba64 source); } /// @@ -139,26 +169,58 @@ public interface IPixel /// Gets the pixel type information. /// /// The . - static abstract PixelTypeInfo GetPixelTypeInfo(); + public static abstract PixelTypeInfo GetPixelTypeInfo(); /// /// Convert the pixel instance into representation. /// /// The - Rgba32 ToRgba32(); + public Rgba32 ToRgba32(); /// - /// Expands the pixel into a generic ("scaled") representation - /// with values scaled and clamped between 0 and 1. + /// Expands the pixel into a generic ("scaled") representation using the pixel type's native alpha representation. + /// The scaled representation uses 0 and 1 as its nominal component bounds. /// The vector components are typically expanded in least to greatest significance order. /// /// The . - Vector4 ToScaledVector4(); + public Vector4 ToScaledVector4(); + + /// + /// Expands the pixel into a generic ("scaled") whose color components use unassociated alpha. + /// The scaled representation uses 0 and 1 as its nominal component bounds. + /// When alpha is zero and the pixel's native representation is associated, the color components remain unchanged + /// because no unassociated value can be recovered. + /// + /// The . + public Vector4 ToUnassociatedScaledVector4(); + + /// + /// Expands the pixel into a generic ("scaled") whose color components use associated alpha, + /// representing color multiplied by the logical opacity represented by alpha. + /// The scaled representation uses 0 and 1 as its nominal component bounds. + /// + /// The . + public Vector4 ToAssociatedScaledVector4(); /// - /// Expands the pixel into a which is specific to the current pixel type. + /// Expands the pixel into a which is specific to the current pixel type and uses its native alpha representation. /// The vector components are typically expanded in least to greatest significance order. /// /// The . - Vector4 ToVector4(); + public Vector4 ToVector4(); + + /// + /// Expands the pixel into a which is specific to the current pixel type and whose color components use unassociated alpha. + /// When alpha is zero and the pixel's native representation is associated, the color components remain unchanged + /// because no unassociated value can be recovered. + /// + /// The . + public Vector4 ToUnassociatedVector4(); + + /// + /// Expands the pixel into a which is specific to the current pixel type and whose color components use associated alpha, + /// representing color multiplied by the logical opacity represented by alpha. + /// + /// The . + public Vector4 ToAssociatedVector4(); } diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.cs b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.cs new file mode 100644 index 000000000..acd9fba9c --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.cs @@ -0,0 +1,2832 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System.Numerics; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +/// +/// Provides the associated-alpha equations consumed by the shared pixel-blending traversal. +/// +internal static class AssociatedAlphaPixelBlenderOperators +{ + /// + /// Applies the "NormalSrc" associated-alpha composition equation. + /// + public readonly struct NormalSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrc(background, source, amount); + } + + /// + /// Applies the "MultiplySrc" associated-alpha composition equation. + /// + public readonly struct MultiplySrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrc(background, source, amount); + } + + /// + /// Applies the "AddSrc" associated-alpha composition equation. + /// + public readonly struct AddSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrc(background, source, amount); + } + + /// + /// Applies the "SubtractSrc" associated-alpha composition equation. + /// + public readonly struct SubtractSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrc(background, source, amount); + } + + /// + /// Applies the "ScreenSrc" associated-alpha composition equation. + /// + public readonly struct ScreenSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrc(background, source, amount); + } + + /// + /// Applies the "DarkenSrc" associated-alpha composition equation. + /// + public readonly struct DarkenSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrc(background, source, amount); + } + + /// + /// Applies the "LightenSrc" associated-alpha composition equation. + /// + public readonly struct LightenSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrc(background, source, amount); + } + + /// + /// Applies the "OverlaySrc" associated-alpha composition equation. + /// + public readonly struct OverlaySrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrc(background, source, amount); + } + + /// + /// Applies the "HardLightSrc" associated-alpha composition equation. + /// + public readonly struct HardLightSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrc(background, source, amount); + } + + /// + /// Applies the "NormalSrcAtop" associated-alpha composition equation. + /// + public readonly struct NormalSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcAtop(background, source, amount); + } + + /// + /// Applies the "MultiplySrcAtop" associated-alpha composition equation. + /// + public readonly struct MultiplySrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcAtop(background, source, amount); + } + + /// + /// Applies the "AddSrcAtop" associated-alpha composition equation. + /// + public readonly struct AddSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcAtop(background, source, amount); + } + + /// + /// Applies the "SubtractSrcAtop" associated-alpha composition equation. + /// + public readonly struct SubtractSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcAtop(background, source, amount); + } + + /// + /// Applies the "ScreenSrcAtop" associated-alpha composition equation. + /// + public readonly struct ScreenSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcAtop(background, source, amount); + } + + /// + /// Applies the "DarkenSrcAtop" associated-alpha composition equation. + /// + public readonly struct DarkenSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcAtop(background, source, amount); + } + + /// + /// Applies the "LightenSrcAtop" associated-alpha composition equation. + /// + public readonly struct LightenSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcAtop(background, source, amount); + } + + /// + /// Applies the "OverlaySrcAtop" associated-alpha composition equation. + /// + public readonly struct OverlaySrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcAtop(background, source, amount); + } + + /// + /// Applies the "HardLightSrcAtop" associated-alpha composition equation. + /// + public readonly struct HardLightSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcAtop(background, source, amount); + } + + /// + /// Applies the "NormalSrcOver" associated-alpha composition equation. + /// + public readonly struct NormalSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcOver(background, source, amount); + } + + /// + /// Applies the "MultiplySrcOver" associated-alpha composition equation. + /// + public readonly struct MultiplySrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcOver(background, source, amount); + } + + /// + /// Applies the "AddSrcOver" associated-alpha composition equation. + /// + public readonly struct AddSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcOver(background, source, amount); + } + + /// + /// Applies the "SubtractSrcOver" associated-alpha composition equation. + /// + public readonly struct SubtractSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcOver(background, source, amount); + } + + /// + /// Applies the "ScreenSrcOver" associated-alpha composition equation. + /// + public readonly struct ScreenSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcOver(background, source, amount); + } + + /// + /// Applies the "DarkenSrcOver" associated-alpha composition equation. + /// + public readonly struct DarkenSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcOver(background, source, amount); + } + + /// + /// Applies the "LightenSrcOver" associated-alpha composition equation. + /// + public readonly struct LightenSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcOver(background, source, amount); + } + + /// + /// Applies the "OverlaySrcOver" associated-alpha composition equation. + /// + public readonly struct OverlaySrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcOver(background, source, amount); + } + + /// + /// Applies the "HardLightSrcOver" associated-alpha composition equation. + /// + public readonly struct HardLightSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcOver(background, source, amount); + } + + /// + /// Applies the "NormalSrcIn" associated-alpha composition equation. + /// + public readonly struct NormalSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcIn(background, source, amount); + } + + /// + /// Applies the "MultiplySrcIn" associated-alpha composition equation. + /// + public readonly struct MultiplySrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcIn(background, source, amount); + } + + /// + /// Applies the "AddSrcIn" associated-alpha composition equation. + /// + public readonly struct AddSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcIn(background, source, amount); + } + + /// + /// Applies the "SubtractSrcIn" associated-alpha composition equation. + /// + public readonly struct SubtractSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcIn(background, source, amount); + } + + /// + /// Applies the "ScreenSrcIn" associated-alpha composition equation. + /// + public readonly struct ScreenSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcIn(background, source, amount); + } + + /// + /// Applies the "DarkenSrcIn" associated-alpha composition equation. + /// + public readonly struct DarkenSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcIn(background, source, amount); + } + + /// + /// Applies the "LightenSrcIn" associated-alpha composition equation. + /// + public readonly struct LightenSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcIn(background, source, amount); + } + + /// + /// Applies the "OverlaySrcIn" associated-alpha composition equation. + /// + public readonly struct OverlaySrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcIn(background, source, amount); + } + + /// + /// Applies the "HardLightSrcIn" associated-alpha composition equation. + /// + public readonly struct HardLightSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcIn(background, source, amount); + } + + /// + /// Applies the "NormalSrcOut" associated-alpha composition equation. + /// + public readonly struct NormalSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalSrcOut(background, source, amount); + } + + /// + /// Applies the "MultiplySrcOut" associated-alpha composition equation. + /// + public readonly struct MultiplySrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplySrcOut(background, source, amount); + } + + /// + /// Applies the "AddSrcOut" associated-alpha composition equation. + /// + public readonly struct AddSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddSrcOut(background, source, amount); + } + + /// + /// Applies the "SubtractSrcOut" associated-alpha composition equation. + /// + public readonly struct SubtractSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractSrcOut(background, source, amount); + } + + /// + /// Applies the "ScreenSrcOut" associated-alpha composition equation. + /// + public readonly struct ScreenSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenSrcOut(background, source, amount); + } + + /// + /// Applies the "DarkenSrcOut" associated-alpha composition equation. + /// + public readonly struct DarkenSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenSrcOut(background, source, amount); + } + + /// + /// Applies the "LightenSrcOut" associated-alpha composition equation. + /// + public readonly struct LightenSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenSrcOut(background, source, amount); + } + + /// + /// Applies the "OverlaySrcOut" associated-alpha composition equation. + /// + public readonly struct OverlaySrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlaySrcOut(background, source, amount); + } + + /// + /// Applies the "HardLightSrcOut" associated-alpha composition equation. + /// + public readonly struct HardLightSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightSrcOut(background, source, amount); + } + + /// + /// Applies the "NormalDest" associated-alpha composition equation. + /// + public readonly struct NormalDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDest(background, source, amount); + } + + /// + /// Applies the "MultiplyDest" associated-alpha composition equation. + /// + public readonly struct MultiplyDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDest(background, source, amount); + } + + /// + /// Applies the "AddDest" associated-alpha composition equation. + /// + public readonly struct AddDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddDest(background, source, amount); + } + + /// + /// Applies the "SubtractDest" associated-alpha composition equation. + /// + public readonly struct SubtractDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDest(background, source, amount); + } + + /// + /// Applies the "ScreenDest" associated-alpha composition equation. + /// + public readonly struct ScreenDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDest(background, source, amount); + } + + /// + /// Applies the "DarkenDest" associated-alpha composition equation. + /// + public readonly struct DarkenDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDest(background, source, amount); + } + + /// + /// Applies the "LightenDest" associated-alpha composition equation. + /// + public readonly struct LightenDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDest(background, source, amount); + } + + /// + /// Applies the "OverlayDest" associated-alpha composition equation. + /// + public readonly struct OverlayDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDest(background, source, amount); + } + + /// + /// Applies the "HardLightDest" associated-alpha composition equation. + /// + public readonly struct HardLightDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDest(background, source, amount); + } + + /// + /// Applies the "NormalDestAtop" associated-alpha composition equation. + /// + public readonly struct NormalDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestAtop(background, source, amount); + } + + /// + /// Applies the "MultiplyDestAtop" associated-alpha composition equation. + /// + public readonly struct MultiplyDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestAtop(background, source, amount); + } + + /// + /// Applies the "AddDestAtop" associated-alpha composition equation. + /// + public readonly struct AddDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddDestAtop(background, source, amount); + } + + /// + /// Applies the "SubtractDestAtop" associated-alpha composition equation. + /// + public readonly struct SubtractDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestAtop(background, source, amount); + } + + /// + /// Applies the "ScreenDestAtop" associated-alpha composition equation. + /// + public readonly struct ScreenDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestAtop(background, source, amount); + } + + /// + /// Applies the "DarkenDestAtop" associated-alpha composition equation. + /// + public readonly struct DarkenDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestAtop(background, source, amount); + } + + /// + /// Applies the "LightenDestAtop" associated-alpha composition equation. + /// + public readonly struct LightenDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestAtop(background, source, amount); + } + + /// + /// Applies the "OverlayDestAtop" associated-alpha composition equation. + /// + public readonly struct OverlayDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestAtop(background, source, amount); + } + + /// + /// Applies the "HardLightDestAtop" associated-alpha composition equation. + /// + public readonly struct HardLightDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestAtop(background, source, amount); + } + + /// + /// Applies the "NormalDestOver" associated-alpha composition equation. + /// + public readonly struct NormalDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestOver(background, source, amount); + } + + /// + /// Applies the "MultiplyDestOver" associated-alpha composition equation. + /// + public readonly struct MultiplyDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestOver(background, source, amount); + } + + /// + /// Applies the "AddDestOver" associated-alpha composition equation. + /// + public readonly struct AddDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddDestOver(background, source, amount); + } + + /// + /// Applies the "SubtractDestOver" associated-alpha composition equation. + /// + public readonly struct SubtractDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestOver(background, source, amount); + } + + /// + /// Applies the "ScreenDestOver" associated-alpha composition equation. + /// + public readonly struct ScreenDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestOver(background, source, amount); + } + + /// + /// Applies the "DarkenDestOver" associated-alpha composition equation. + /// + public readonly struct DarkenDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestOver(background, source, amount); + } + + /// + /// Applies the "LightenDestOver" associated-alpha composition equation. + /// + public readonly struct LightenDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestOver(background, source, amount); + } + + /// + /// Applies the "OverlayDestOver" associated-alpha composition equation. + /// + public readonly struct OverlayDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestOver(background, source, amount); + } + + /// + /// Applies the "HardLightDestOver" associated-alpha composition equation. + /// + public readonly struct HardLightDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestOver(background, source, amount); + } + + /// + /// Applies the "NormalDestIn" associated-alpha composition equation. + /// + public readonly struct NormalDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestIn(background, source, amount); + } + + /// + /// Applies the "MultiplyDestIn" associated-alpha composition equation. + /// + public readonly struct MultiplyDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestIn(background, source, amount); + } + + /// + /// Applies the "AddDestIn" associated-alpha composition equation. + /// + public readonly struct AddDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddDestIn(background, source, amount); + } + + /// + /// Applies the "SubtractDestIn" associated-alpha composition equation. + /// + public readonly struct SubtractDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestIn(background, source, amount); + } + + /// + /// Applies the "ScreenDestIn" associated-alpha composition equation. + /// + public readonly struct ScreenDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestIn(background, source, amount); + } + + /// + /// Applies the "DarkenDestIn" associated-alpha composition equation. + /// + public readonly struct DarkenDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestIn(background, source, amount); + } + + /// + /// Applies the "LightenDestIn" associated-alpha composition equation. + /// + public readonly struct LightenDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestIn(background, source, amount); + } + + /// + /// Applies the "OverlayDestIn" associated-alpha composition equation. + /// + public readonly struct OverlayDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestIn(background, source, amount); + } + + /// + /// Applies the "HardLightDestIn" associated-alpha composition equation. + /// + public readonly struct HardLightDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestIn(background, source, amount); + } + + /// + /// Applies the "NormalDestOut" associated-alpha composition equation. + /// + public readonly struct NormalDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalDestOut(background, source, amount); + } + + /// + /// Applies the "MultiplyDestOut" associated-alpha composition equation. + /// + public readonly struct MultiplyDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyDestOut(background, source, amount); + } + + /// + /// Applies the "AddDestOut" associated-alpha composition equation. + /// + public readonly struct AddDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddDestOut(background, source, amount); + } + + /// + /// Applies the "SubtractDestOut" associated-alpha composition equation. + /// + public readonly struct SubtractDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractDestOut(background, source, amount); + } + + /// + /// Applies the "ScreenDestOut" associated-alpha composition equation. + /// + public readonly struct ScreenDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenDestOut(background, source, amount); + } + + /// + /// Applies the "DarkenDestOut" associated-alpha composition equation. + /// + public readonly struct DarkenDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenDestOut(background, source, amount); + } + + /// + /// Applies the "LightenDestOut" associated-alpha composition equation. + /// + public readonly struct LightenDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenDestOut(background, source, amount); + } + + /// + /// Applies the "OverlayDestOut" associated-alpha composition equation. + /// + public readonly struct OverlayDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayDestOut(background, source, amount); + } + + /// + /// Applies the "HardLightDestOut" associated-alpha composition equation. + /// + public readonly struct HardLightDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightDestOut(background, source, amount); + } + + /// + /// Applies the "NormalClear" associated-alpha composition equation. + /// + public readonly struct NormalClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalClear(background, source, amount); + } + + /// + /// Applies the "MultiplyClear" associated-alpha composition equation. + /// + public readonly struct MultiplyClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyClear(background, source, amount); + } + + /// + /// Applies the "AddClear" associated-alpha composition equation. + /// + public readonly struct AddClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddClear(background, source, amount); + } + + /// + /// Applies the "SubtractClear" associated-alpha composition equation. + /// + public readonly struct SubtractClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractClear(background, source, amount); + } + + /// + /// Applies the "ScreenClear" associated-alpha composition equation. + /// + public readonly struct ScreenClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenClear(background, source, amount); + } + + /// + /// Applies the "DarkenClear" associated-alpha composition equation. + /// + public readonly struct DarkenClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenClear(background, source, amount); + } + + /// + /// Applies the "LightenClear" associated-alpha composition equation. + /// + public readonly struct LightenClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenClear(background, source, amount); + } + + /// + /// Applies the "OverlayClear" associated-alpha composition equation. + /// + public readonly struct OverlayClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlayClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayClear(background, source, amount); + } + + /// + /// Applies the "HardLightClear" associated-alpha composition equation. + /// + public readonly struct HardLightClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightClear(background, source, amount); + } + + /// + /// Applies the "NormalXor" associated-alpha composition equation. + /// + public readonly struct NormalXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.NormalXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.NormalXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.NormalXor(background, source, amount); + } + + /// + /// Applies the "MultiplyXor" associated-alpha composition equation. + /// + public readonly struct MultiplyXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.MultiplyXor(background, source, amount); + } + + /// + /// Applies the "AddXor" associated-alpha composition equation. + /// + public readonly struct AddXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.AddXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.AddXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.AddXor(background, source, amount); + } + + /// + /// Applies the "SubtractXor" associated-alpha composition equation. + /// + public readonly struct SubtractXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.SubtractXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.SubtractXor(background, source, amount); + } + + /// + /// Applies the "ScreenXor" associated-alpha composition equation. + /// + public readonly struct ScreenXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.ScreenXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.ScreenXor(background, source, amount); + } + + /// + /// Applies the "DarkenXor" associated-alpha composition equation. + /// + public readonly struct DarkenXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.DarkenXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.DarkenXor(background, source, amount); + } + + /// + /// Applies the "LightenXor" associated-alpha composition equation. + /// + public readonly struct LightenXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.LightenXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.LightenXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.LightenXor(background, source, amount); + } + + /// + /// Applies the "OverlayXor" associated-alpha composition equation. + /// + public readonly struct OverlayXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.OverlayXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.OverlayXor(background, source, amount); + } + + /// + /// Applies the "HardLightXor" associated-alpha composition equation. + /// + public readonly struct HardLightXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.HardLightXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.HardLightXor(background, source, amount); + } + +} + +/// +/// Provides associated-alpha pixel blenders for each color and alpha composition pair. +/// +/// The associated-alpha destination pixel format. +internal static partial class AssociatedAlphaPixelBlenders + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the shared blender for the "NormalSrc" composition equation. + /// + public static PixelBlender NormalSrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplySrc" composition equation. + /// + public static PixelBlender MultiplySrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddSrc" composition equation. + /// + public static PixelBlender AddSrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractSrc" composition equation. + /// + public static PixelBlender SubtractSrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenSrc" composition equation. + /// + public static PixelBlender ScreenSrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenSrc" composition equation. + /// + public static PixelBlender DarkenSrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenSrc" composition equation. + /// + public static PixelBlender LightenSrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlaySrc" composition equation. + /// + public static PixelBlender OverlaySrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightSrc" composition equation. + /// + public static PixelBlender HardLightSrc => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalSrcAtop" composition equation. + /// + public static PixelBlender NormalSrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplySrcAtop" composition equation. + /// + public static PixelBlender MultiplySrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddSrcAtop" composition equation. + /// + public static PixelBlender AddSrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractSrcAtop" composition equation. + /// + public static PixelBlender SubtractSrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenSrcAtop" composition equation. + /// + public static PixelBlender ScreenSrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenSrcAtop" composition equation. + /// + public static PixelBlender DarkenSrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenSrcAtop" composition equation. + /// + public static PixelBlender LightenSrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlaySrcAtop" composition equation. + /// + public static PixelBlender OverlaySrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightSrcAtop" composition equation. + /// + public static PixelBlender HardLightSrcAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalSrcOver" composition equation. + /// + public static PixelBlender NormalSrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplySrcOver" composition equation. + /// + public static PixelBlender MultiplySrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddSrcOver" composition equation. + /// + public static PixelBlender AddSrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractSrcOver" composition equation. + /// + public static PixelBlender SubtractSrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenSrcOver" composition equation. + /// + public static PixelBlender ScreenSrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenSrcOver" composition equation. + /// + public static PixelBlender DarkenSrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenSrcOver" composition equation. + /// + public static PixelBlender LightenSrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlaySrcOver" composition equation. + /// + public static PixelBlender OverlaySrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightSrcOver" composition equation. + /// + public static PixelBlender HardLightSrcOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalSrcIn" composition equation. + /// + public static PixelBlender NormalSrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplySrcIn" composition equation. + /// + public static PixelBlender MultiplySrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddSrcIn" composition equation. + /// + public static PixelBlender AddSrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractSrcIn" composition equation. + /// + public static PixelBlender SubtractSrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenSrcIn" composition equation. + /// + public static PixelBlender ScreenSrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenSrcIn" composition equation. + /// + public static PixelBlender DarkenSrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenSrcIn" composition equation. + /// + public static PixelBlender LightenSrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlaySrcIn" composition equation. + /// + public static PixelBlender OverlaySrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightSrcIn" composition equation. + /// + public static PixelBlender HardLightSrcIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalSrcOut" composition equation. + /// + public static PixelBlender NormalSrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplySrcOut" composition equation. + /// + public static PixelBlender MultiplySrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddSrcOut" composition equation. + /// + public static PixelBlender AddSrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractSrcOut" composition equation. + /// + public static PixelBlender SubtractSrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenSrcOut" composition equation. + /// + public static PixelBlender ScreenSrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenSrcOut" composition equation. + /// + public static PixelBlender DarkenSrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenSrcOut" composition equation. + /// + public static PixelBlender LightenSrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlaySrcOut" composition equation. + /// + public static PixelBlender OverlaySrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightSrcOut" composition equation. + /// + public static PixelBlender HardLightSrcOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalDest" composition equation. + /// + public static PixelBlender NormalDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplyDest" composition equation. + /// + public static PixelBlender MultiplyDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddDest" composition equation. + /// + public static PixelBlender AddDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractDest" composition equation. + /// + public static PixelBlender SubtractDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenDest" composition equation. + /// + public static PixelBlender ScreenDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenDest" composition equation. + /// + public static PixelBlender DarkenDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenDest" composition equation. + /// + public static PixelBlender LightenDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlayDest" composition equation. + /// + public static PixelBlender OverlayDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightDest" composition equation. + /// + public static PixelBlender HardLightDest => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalDestAtop" composition equation. + /// + public static PixelBlender NormalDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplyDestAtop" composition equation. + /// + public static PixelBlender MultiplyDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddDestAtop" composition equation. + /// + public static PixelBlender AddDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractDestAtop" composition equation. + /// + public static PixelBlender SubtractDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenDestAtop" composition equation. + /// + public static PixelBlender ScreenDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenDestAtop" composition equation. + /// + public static PixelBlender DarkenDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenDestAtop" composition equation. + /// + public static PixelBlender LightenDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlayDestAtop" composition equation. + /// + public static PixelBlender OverlayDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightDestAtop" composition equation. + /// + public static PixelBlender HardLightDestAtop => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalDestOver" composition equation. + /// + public static PixelBlender NormalDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplyDestOver" composition equation. + /// + public static PixelBlender MultiplyDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddDestOver" composition equation. + /// + public static PixelBlender AddDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractDestOver" composition equation. + /// + public static PixelBlender SubtractDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenDestOver" composition equation. + /// + public static PixelBlender ScreenDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenDestOver" composition equation. + /// + public static PixelBlender DarkenDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenDestOver" composition equation. + /// + public static PixelBlender LightenDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlayDestOver" composition equation. + /// + public static PixelBlender OverlayDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightDestOver" composition equation. + /// + public static PixelBlender HardLightDestOver => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalDestIn" composition equation. + /// + public static PixelBlender NormalDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplyDestIn" composition equation. + /// + public static PixelBlender MultiplyDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddDestIn" composition equation. + /// + public static PixelBlender AddDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractDestIn" composition equation. + /// + public static PixelBlender SubtractDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenDestIn" composition equation. + /// + public static PixelBlender ScreenDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenDestIn" composition equation. + /// + public static PixelBlender DarkenDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenDestIn" composition equation. + /// + public static PixelBlender LightenDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlayDestIn" composition equation. + /// + public static PixelBlender OverlayDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightDestIn" composition equation. + /// + public static PixelBlender HardLightDestIn => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalDestOut" composition equation. + /// + public static PixelBlender NormalDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplyDestOut" composition equation. + /// + public static PixelBlender MultiplyDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddDestOut" composition equation. + /// + public static PixelBlender AddDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractDestOut" composition equation. + /// + public static PixelBlender SubtractDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenDestOut" composition equation. + /// + public static PixelBlender ScreenDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenDestOut" composition equation. + /// + public static PixelBlender DarkenDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenDestOut" composition equation. + /// + public static PixelBlender LightenDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlayDestOut" composition equation. + /// + public static PixelBlender OverlayDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightDestOut" composition equation. + /// + public static PixelBlender HardLightDestOut => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalClear" composition equation. + /// + public static PixelBlender NormalClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplyClear" composition equation. + /// + public static PixelBlender MultiplyClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddClear" composition equation. + /// + public static PixelBlender AddClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractClear" composition equation. + /// + public static PixelBlender SubtractClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenClear" composition equation. + /// + public static PixelBlender ScreenClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenClear" composition equation. + /// + public static PixelBlender DarkenClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenClear" composition equation. + /// + public static PixelBlender LightenClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlayClear" composition equation. + /// + public static PixelBlender OverlayClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightClear" composition equation. + /// + public static PixelBlender HardLightClear => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "NormalXor" composition equation. + /// + public static PixelBlender NormalXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "MultiplyXor" composition equation. + /// + public static PixelBlender MultiplyXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "AddXor" composition equation. + /// + public static PixelBlender AddXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "SubtractXor" composition equation. + /// + public static PixelBlender SubtractXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "ScreenXor" composition equation. + /// + public static PixelBlender ScreenXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenXor" composition equation. + /// + public static PixelBlender DarkenXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenXor" composition equation. + /// + public static PixelBlender LightenXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlayXor" composition equation. + /// + public static PixelBlender OverlayXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightXor" composition equation. + /// + public static PixelBlender HardLightXor => PixelBlender.Instance; + +} diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.tt b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.tt new file mode 100644 index 000000000..5fc7f91c4 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.tt @@ -0,0 +1,109 @@ +<# +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#> +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ output extension=".cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System.Numerics; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +<# +var composers = new [] +{ + "Src", + "SrcAtop", + "SrcOver", + "SrcIn", + "SrcOut", + "Dest", + "DestAtop", + "DestOver", + "DestIn", + "DestOut", + "Clear", + "Xor", +}; + +var blenders = new [] +{ + "Normal", + "Multiply", + "Add", + "Subtract", + "Screen", + "Darken", + "Lighten", + "Overlay", + "HardLight", +}; +#> +/// +/// Provides the associated-alpha equations consumed by the shared pixel-blending traversal. +/// +internal static class AssociatedAlphaPixelBlenderOperators +{ +<# +foreach (var composer in composers) +{ + foreach (var blender in blenders) + { + var blenderComposer = $"{blender}{composer}"; +#> + /// + /// Applies the "<#= blenderComposer #>" associated-alpha composition equation. + /// + public readonly struct <#= blenderComposer #> : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => true; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => AssociatedAlphaPorterDuffFunctions.<#= blenderComposer #>(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => AssociatedAlphaPorterDuffFunctions.<#= blenderComposer #>(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => AssociatedAlphaPorterDuffFunctions.<#= blenderComposer #>(background, source, amount); + } + +<# + } +} +#> +} + +/// +/// Provides associated-alpha pixel blenders for each color and alpha composition pair. +/// +/// The associated-alpha destination pixel format. +internal static partial class AssociatedAlphaPixelBlenders + where TPixel : unmanaged, IPixel +{ +<# +foreach (var composer in composers) +{ + foreach (var blender in blenders) + { + var blenderComposer = $"{blender}{composer}"; +#> + /// + /// Gets the shared blender for the "<#= blenderComposer #>" composition equation. + /// + public static PixelBlender <#= blenderComposer #> => PixelBlender>.Instance; + +<# + } +} +#> +} diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.cs b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.cs new file mode 100644 index 000000000..0664e3fa0 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.cs @@ -0,0 +1,168 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +/// +/// Provides pixel blenders for formats that store associated alpha. +/// +internal static partial class AssociatedAlphaPixelBlenders + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the blender for the requested color blending and alpha composition modes. + /// + /// The color blending mode. + /// The alpha composition mode. + /// The pixel blender. + public static PixelBlender GetPixelBlender(PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode) + { + return alphaMode switch + { + PixelAlphaCompositionMode.Src => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplySrc, + PixelColorBlendingMode.Add => AddSrc, + PixelColorBlendingMode.Subtract => SubtractSrc, + PixelColorBlendingMode.Screen => ScreenSrc, + PixelColorBlendingMode.Darken => DarkenSrc, + PixelColorBlendingMode.Lighten => LightenSrc, + PixelColorBlendingMode.Overlay => OverlaySrc, + PixelColorBlendingMode.HardLight => HardLightSrc, + _ => NormalSrc, + }, + PixelAlphaCompositionMode.SrcAtop => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplySrcAtop, + PixelColorBlendingMode.Add => AddSrcAtop, + PixelColorBlendingMode.Subtract => SubtractSrcAtop, + PixelColorBlendingMode.Screen => ScreenSrcAtop, + PixelColorBlendingMode.Darken => DarkenSrcAtop, + PixelColorBlendingMode.Lighten => LightenSrcAtop, + PixelColorBlendingMode.Overlay => OverlaySrcAtop, + PixelColorBlendingMode.HardLight => HardLightSrcAtop, + _ => NormalSrcAtop, + }, + PixelAlphaCompositionMode.SrcIn => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplySrcIn, + PixelColorBlendingMode.Add => AddSrcIn, + PixelColorBlendingMode.Subtract => SubtractSrcIn, + PixelColorBlendingMode.Screen => ScreenSrcIn, + PixelColorBlendingMode.Darken => DarkenSrcIn, + PixelColorBlendingMode.Lighten => LightenSrcIn, + PixelColorBlendingMode.Overlay => OverlaySrcIn, + PixelColorBlendingMode.HardLight => HardLightSrcIn, + _ => NormalSrcIn, + }, + PixelAlphaCompositionMode.SrcOut => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplySrcOut, + PixelColorBlendingMode.Add => AddSrcOut, + PixelColorBlendingMode.Subtract => SubtractSrcOut, + PixelColorBlendingMode.Screen => ScreenSrcOut, + PixelColorBlendingMode.Darken => DarkenSrcOut, + PixelColorBlendingMode.Lighten => LightenSrcOut, + PixelColorBlendingMode.Overlay => OverlaySrcOut, + PixelColorBlendingMode.HardLight => HardLightSrcOut, + _ => NormalSrcOut, + }, + PixelAlphaCompositionMode.Dest => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplyDest, + PixelColorBlendingMode.Add => AddDest, + PixelColorBlendingMode.Subtract => SubtractDest, + PixelColorBlendingMode.Screen => ScreenDest, + PixelColorBlendingMode.Darken => DarkenDest, + PixelColorBlendingMode.Lighten => LightenDest, + PixelColorBlendingMode.Overlay => OverlayDest, + PixelColorBlendingMode.HardLight => HardLightDest, + _ => NormalDest, + }, + PixelAlphaCompositionMode.DestAtop => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplyDestAtop, + PixelColorBlendingMode.Add => AddDestAtop, + PixelColorBlendingMode.Subtract => SubtractDestAtop, + PixelColorBlendingMode.Screen => ScreenDestAtop, + PixelColorBlendingMode.Darken => DarkenDestAtop, + PixelColorBlendingMode.Lighten => LightenDestAtop, + PixelColorBlendingMode.Overlay => OverlayDestAtop, + PixelColorBlendingMode.HardLight => HardLightDestAtop, + _ => NormalDestAtop, + }, + PixelAlphaCompositionMode.DestOver => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplyDestOver, + PixelColorBlendingMode.Add => AddDestOver, + PixelColorBlendingMode.Subtract => SubtractDestOver, + PixelColorBlendingMode.Screen => ScreenDestOver, + PixelColorBlendingMode.Darken => DarkenDestOver, + PixelColorBlendingMode.Lighten => LightenDestOver, + PixelColorBlendingMode.Overlay => OverlayDestOver, + PixelColorBlendingMode.HardLight => HardLightDestOver, + _ => NormalDestOver, + }, + PixelAlphaCompositionMode.DestIn => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplyDestIn, + PixelColorBlendingMode.Add => AddDestIn, + PixelColorBlendingMode.Subtract => SubtractDestIn, + PixelColorBlendingMode.Screen => ScreenDestIn, + PixelColorBlendingMode.Darken => DarkenDestIn, + PixelColorBlendingMode.Lighten => LightenDestIn, + PixelColorBlendingMode.Overlay => OverlayDestIn, + PixelColorBlendingMode.HardLight => HardLightDestIn, + _ => NormalDestIn, + }, + PixelAlphaCompositionMode.DestOut => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplyDestOut, + PixelColorBlendingMode.Add => AddDestOut, + PixelColorBlendingMode.Subtract => SubtractDestOut, + PixelColorBlendingMode.Screen => ScreenDestOut, + PixelColorBlendingMode.Darken => DarkenDestOut, + PixelColorBlendingMode.Lighten => LightenDestOut, + PixelColorBlendingMode.Overlay => OverlayDestOut, + PixelColorBlendingMode.HardLight => HardLightDestOut, + _ => NormalDestOut, + }, + PixelAlphaCompositionMode.Clear => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplyClear, + PixelColorBlendingMode.Add => AddClear, + PixelColorBlendingMode.Subtract => SubtractClear, + PixelColorBlendingMode.Screen => ScreenClear, + PixelColorBlendingMode.Darken => DarkenClear, + PixelColorBlendingMode.Lighten => LightenClear, + PixelColorBlendingMode.Overlay => OverlayClear, + PixelColorBlendingMode.HardLight => HardLightClear, + _ => NormalClear, + }, + PixelAlphaCompositionMode.Xor => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplyXor, + PixelColorBlendingMode.Add => AddXor, + PixelColorBlendingMode.Subtract => SubtractXor, + PixelColorBlendingMode.Screen => ScreenXor, + PixelColorBlendingMode.Darken => DarkenXor, + PixelColorBlendingMode.Lighten => LightenXor, + PixelColorBlendingMode.Overlay => OverlayXor, + PixelColorBlendingMode.HardLight => HardLightXor, + _ => NormalXor, + }, + _ => colorMode switch + { + PixelColorBlendingMode.Multiply => MultiplySrcOver, + PixelColorBlendingMode.Add => AddSrcOver, + PixelColorBlendingMode.Subtract => SubtractSrcOver, + PixelColorBlendingMode.Screen => ScreenSrcOver, + PixelColorBlendingMode.Darken => DarkenSrcOver, + PixelColorBlendingMode.Lighten => LightenSrcOver, + PixelColorBlendingMode.Overlay => OverlaySrcOver, + PixelColorBlendingMode.HardLight => HardLightSrcOver, + _ => NormalSrcOver, + }, + }; + } +} diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.Generated.cs b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.Generated.cs new file mode 100644 index 000000000..cd5a37a11 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.Generated.cs @@ -0,0 +1,5035 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +internal static partial class AssociatedAlphaPorterDuffFunctions +{ + /// + /// Returns the associated-alpha result of the "NormalSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "NormalSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "NormalSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "MultiplySrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "MultiplySrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "MultiplySrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "AddSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "AddSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "AddSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "SubtractSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "SubtractSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "SubtractSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "ScreenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "ScreenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "ScreenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "DarkenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "DarkenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "DarkenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "LightenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "LightenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "LightenSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "OverlaySrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "OverlaySrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "OverlaySrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "HardLightSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrc(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "HardLightSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "HardLightSrc" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return source; + } + + /// + /// Returns the associated-alpha result of the "NormalSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return AtopNormal(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return AtopNormal(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return AtopNormal(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "AddSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "AddSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "AddSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrcAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return OverNormal(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return OverNormal(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return OverNormal(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "AddSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "AddSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "AddSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrcOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplySrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplySrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplySrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlaySrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlaySrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlaySrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightSrcOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightSrcOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "NormalDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "NormalDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "MultiplyDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "MultiplyDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "MultiplyDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "AddDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "AddDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "AddDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "SubtractDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "SubtractDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "SubtractDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "ScreenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "ScreenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "ScreenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "DarkenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "DarkenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "DarkenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "LightenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "LightenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "LightenDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "OverlayDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "OverlayDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "OverlayDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "HardLightDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDest(Vector4 backdrop, Vector4 source, float opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "HardLightDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDest(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "HardLightDest" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + + /// + /// Returns the associated-alpha result of the "NormalDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return AtopNormal(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return AtopNormal(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return AtopNormal(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "AddDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "AddDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "AddDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "LightenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "LightenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "LightenDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDestAtop(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestAtop" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Atop(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "NormalDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return OverNormal(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return OverNormal(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return OverNormal(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "AddDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "AddDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "AddDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "LightenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "LightenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "LightenDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDestOver(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestOver" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Over(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the associated-alpha result of the "NormalDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "AddDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "AddDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "AddDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "LightenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "LightenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "LightenDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDestIn(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestIn" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return In(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "MultiplyDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "AddDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "AddDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "AddDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "SubtractDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "ScreenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "DarkenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "LightenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "LightenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "LightenDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "OverlayDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightDestOut(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "HardLightDestOut" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Out(source, backdrop); + } + + /// + /// Returns the associated-alpha result of the "NormalClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "NormalClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "NormalClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "MultiplyClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "MultiplyClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "MultiplyClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "AddClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "AddClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "AddClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "SubtractClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "SubtractClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "SubtractClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "ScreenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "ScreenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "ScreenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "DarkenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "DarkenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "DarkenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "LightenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "LightenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "LightenClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "OverlayClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "OverlayClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "OverlayClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "HardLightClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightClear(Vector4 backdrop, Vector4 source, float opacity) + { + return Vector4.Zero; + } + + /// + /// Returns the associated-alpha result of the "HardLightClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightClear(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + return Vector256.Zero; + } + + /// + /// Returns the associated-alpha result of the "HardLightClear" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return Vector512.Zero; + } + + /// + /// Returns the associated-alpha result of the "NormalXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 NormalXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NormalXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "NormalXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplyXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 MultiplyXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplyXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "MultiplyXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AddXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "AddXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 SubtractXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 SubtractXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "SubtractXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ScreenXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "ScreenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 DarkenXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DarkenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "DarkenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 LightenXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "LightenXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlayXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverlayXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlayXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverlayXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "OverlayXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLightXor(Vector4 backdrop, Vector4 source, float opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLightXor(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + + /// + /// Returns the associated-alpha result of the "HardLightXor" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + + return Xor(backdrop, source); + } + +} diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.Generated.tt b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.Generated.tt new file mode 100644 index 000000000..e0652d679 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.Generated.tt @@ -0,0 +1,138 @@ +<# +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +#> +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ output extension=".cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +internal static partial class AssociatedAlphaPorterDuffFunctions +{ +<# + foreach (string composer in Composers) + { + foreach (string blender in Blenders) + { + string function = blender + composer; + + foreach (string vectorType in VectorTypes) + { + string opacityType = vectorType == "Vector4" ? "float" : vectorType; + string zero = vectorType == "Vector4" ? "Vector4.Zero" : vectorType.Replace("", ".Zero"); +#> + /// + /// Returns the associated-alpha result of the "<#= function #>" compositing equation. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The source opacity in the range 0 through 1. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static <#= vectorType #> <#= function #>(<#= vectorType #> backdrop, <#= vectorType #> source, <#= opacityType #> opacity) + { +<# + if (composer != "Dest" && composer != "Clear") + { +#> + // Associated RGB and alpha are scaled together so opacity cannot change the represented straight colour. + source *= opacity; + +<# + } +#> + return <#= GetComposition(blender, composer, zero) #>; + } + +<# + } + } + } +#> +} +<#+ + private static readonly string[] Composers = + { + "Src", + "SrcAtop", + "SrcOver", + "SrcIn", + "SrcOut", + "Dest", + "DestAtop", + "DestOver", + "DestIn", + "DestOut", + "Clear", + "Xor", + }; + + private static readonly string[] Blenders = + { + "Normal", + "Multiply", + "Add", + "Subtract", + "Screen", + "Darken", + "Lighten", + "Overlay", + "HardLight", + }; + + private static readonly string[] VectorTypes = + { + "Vector4", + "Vector256", + "Vector512", + }; + + private static string GetComposition(string blender, string composer, string zero) + { + bool normal = blender == "Normal"; + + switch (composer) + { + case "Src": + return "source"; + case "SrcAtop": + return normal + ? "AtopNormal(backdrop, source)" + : $"Atop(backdrop, source, {blender}(backdrop, source))"; + case "SrcOver": + return normal + ? "OverNormal(backdrop, source)" + : $"Over(backdrop, source, {blender}(backdrop, source))"; + case "SrcIn": + return "In(backdrop, source)"; + case "SrcOut": + return "Out(backdrop, source)"; + case "Dest": + return "backdrop"; + case "DestAtop": + return normal + ? "AtopNormal(source, backdrop)" + : $"Atop(source, backdrop, {blender}(source, backdrop))"; + case "DestOver": + return normal + ? "OverNormal(source, backdrop)" + : $"Over(source, backdrop, {blender}(source, backdrop))"; + case "DestIn": + return "In(source, backdrop)"; + case "DestOut": + return "Out(source, backdrop)"; + case "Clear": + return zero; + default: + return "Xor(backdrop, source)"; + } + } +#> diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.cs b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.cs new file mode 100644 index 000000000..d7b5260d3 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPorterDuffFunctions.cs @@ -0,0 +1,550 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +/// +/// Provides Porter-Duff composition functions for associated-alpha vectors. +/// +internal static partial class AssociatedAlphaPorterDuffFunctions +{ + private const int BlendAlphaControl = 0b_10_00_10_00; + private const int ShuffleAlphaControl = 0b_11_11_11_11; + + /// + /// Calculates the associated overlap term for Multiply blending. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The associated overlap term. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Multiply(Vector4 backdrop, Vector4 source) => backdrop * source; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Multiply(Vector256 backdrop, Vector256 source) => backdrop * source; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Multiply(Vector512 backdrop, Vector512 source) => backdrop * source; + + /// + /// Calculates the associated overlap term for Add blending. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The associated overlap term. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Add(Vector4 backdrop, Vector4 source) + { + Vector4 backdropAlpha = Numerics.PermuteW(backdrop); + Vector4 sourceAlpha = Numerics.PermuteW(source); + return Vector4.Min(backdropAlpha * sourceAlpha, (backdrop * sourceAlpha) + (source * backdropAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Add(Vector256 backdrop, Vector256 source) + { + Vector256 backdropAlpha = Avx.Permute(backdrop, ShuffleAlphaControl); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + return Vector256.Min(backdropAlpha * sourceAlpha, (backdrop * sourceAlpha) + (source * backdropAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Add(Vector512 backdrop, Vector512 source) + { + Vector512 backdropAlpha = Vector512_.ShuffleNative(backdrop, ShuffleAlphaControl); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + return Vector512.Min(backdropAlpha * sourceAlpha, (backdrop * sourceAlpha) + (source * backdropAlpha)); + } + + /// + /// Calculates the associated overlap term for Subtract blending. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The associated overlap term. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Subtract(Vector4 backdrop, Vector4 source) + { + Vector4 backdropAlpha = Numerics.PermuteW(backdrop); + Vector4 sourceAlpha = Numerics.PermuteW(source); + return Vector4.Max(Vector4.Zero, (backdrop * sourceAlpha) - (source * backdropAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Subtract(Vector256 backdrop, Vector256 source) + { + Vector256 backdropAlpha = Avx.Permute(backdrop, ShuffleAlphaControl); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + return Vector256.Max(Vector256.Zero, (backdrop * sourceAlpha) - (source * backdropAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Subtract(Vector512 backdrop, Vector512 source) + { + Vector512 backdropAlpha = Vector512_.ShuffleNative(backdrop, ShuffleAlphaControl); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + return Vector512.Max(Vector512.Zero, (backdrop * sourceAlpha) - (source * backdropAlpha)); + } + + /// + /// Calculates the associated overlap term for Screen blending. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The associated overlap term. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Screen(Vector4 backdrop, Vector4 source) + { + Vector4 backdropAlpha = Numerics.PermuteW(backdrop); + Vector4 sourceAlpha = Numerics.PermuteW(source); + return (backdrop * sourceAlpha) + (source * backdropAlpha) - (backdrop * source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Screen(Vector256 backdrop, Vector256 source) + { + Vector256 backdropAlpha = Avx.Permute(backdrop, ShuffleAlphaControl); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + return (backdrop * sourceAlpha) + (source * backdropAlpha) - (backdrop * source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Screen(Vector512 backdrop, Vector512 source) + { + Vector512 backdropAlpha = Vector512_.ShuffleNative(backdrop, ShuffleAlphaControl); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + return (backdrop * sourceAlpha) + (source * backdropAlpha) - (backdrop * source); + } + + /// + /// Calculates the associated overlap term for Darken blending. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The associated overlap term. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Darken(Vector4 backdrop, Vector4 source) + { + Vector4 backdropAlpha = Numerics.PermuteW(backdrop); + Vector4 sourceAlpha = Numerics.PermuteW(source); + return Vector4.Min(backdrop * sourceAlpha, source * backdropAlpha); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Darken(Vector256 backdrop, Vector256 source) + { + Vector256 backdropAlpha = Avx.Permute(backdrop, ShuffleAlphaControl); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + return Vector256.Min(backdrop * sourceAlpha, source * backdropAlpha); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Darken(Vector512 backdrop, Vector512 source) + { + Vector512 backdropAlpha = Vector512_.ShuffleNative(backdrop, ShuffleAlphaControl); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + return Vector512.Min(backdrop * sourceAlpha, source * backdropAlpha); + } + + /// + /// Calculates the associated overlap term for Lighten blending. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The associated overlap term. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Lighten(Vector4 backdrop, Vector4 source) + { + Vector4 backdropAlpha = Numerics.PermuteW(backdrop); + Vector4 sourceAlpha = Numerics.PermuteW(source); + return Vector4.Max(backdrop * sourceAlpha, source * backdropAlpha); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Lighten(Vector256 backdrop, Vector256 source) + { + Vector256 backdropAlpha = Avx.Permute(backdrop, ShuffleAlphaControl); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + return Vector256.Max(backdrop * sourceAlpha, source * backdropAlpha); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Lighten(Vector512 backdrop, Vector512 source) + { + Vector512 backdropAlpha = Vector512_.ShuffleNative(backdrop, ShuffleAlphaControl); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + return Vector512.Max(backdrop * sourceAlpha, source * backdropAlpha); + } + + /// + /// Calculates the associated overlap term for Overlay blending. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The associated overlap term. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Overlay(Vector4 backdrop, Vector4 source) + { + Vector4 backdropAlpha = Numerics.PermuteW(backdrop); + Vector4 sourceAlpha = Numerics.PermuteW(source); + + return new Vector4( + OverlayValue(backdrop.X, backdropAlpha.X, source.X, sourceAlpha.X), + OverlayValue(backdrop.Y, backdropAlpha.Y, source.Y, sourceAlpha.Y), + OverlayValue(backdrop.Z, backdropAlpha.Z, source.Z, sourceAlpha.Z), + 0F); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Overlay(Vector256 backdrop, Vector256 source) + { + Vector256 backdropAlpha = Avx.Permute(backdrop, ShuffleAlphaControl); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + Vector256 left = (backdrop + backdrop) * source; + Vector256 right = (backdropAlpha * sourceAlpha) - (((backdropAlpha - backdrop) * (sourceAlpha - source)) * Vector256.Create(2F)); + Vector256 useRight = Avx.CompareGreaterThan(backdrop + backdrop, backdropAlpha); + return Avx.BlendVariable(left, right, useRight); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Overlay(Vector512 backdrop, Vector512 source) + { + Vector512 backdropAlpha = Vector512_.ShuffleNative(backdrop, ShuffleAlphaControl); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 left = (backdrop + backdrop) * source; + Vector512 right = (backdropAlpha * sourceAlpha) - (((backdropAlpha - backdrop) * (sourceAlpha - source)) * Vector512.Create(2F)); + Vector512 useRight = Avx512F.CompareGreaterThan(backdrop + backdrop, backdropAlpha); + return Vector512.ConditionalSelect(useRight, right, left); + } + + /// + /// Calculates the associated overlap term for HardLight blending. + /// + /// The associated backdrop vector. + /// The associated source vector. + /// The associated overlap term. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 HardLight(Vector4 backdrop, Vector4 source) + { + Vector4 backdropAlpha = Numerics.PermuteW(backdrop); + Vector4 sourceAlpha = Numerics.PermuteW(source); + + return new Vector4( + OverlayValue(source.X, sourceAlpha.X, backdrop.X, backdropAlpha.X), + OverlayValue(source.Y, sourceAlpha.Y, backdrop.Y, backdropAlpha.Y), + OverlayValue(source.Z, sourceAlpha.Z, backdrop.Z, backdropAlpha.Z), + 0F); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HardLight(Vector256 backdrop, Vector256 source) + { + Vector256 backdropAlpha = Avx.Permute(backdrop, ShuffleAlphaControl); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + Vector256 left = (backdrop + backdrop) * source; + Vector256 right = (backdropAlpha * sourceAlpha) - (((backdropAlpha - backdrop) * (sourceAlpha - source)) * Vector256.Create(2F)); + Vector256 useRight = Avx.CompareGreaterThan(source + source, sourceAlpha); + return Avx.BlendVariable(left, right, useRight); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLight(Vector512 backdrop, Vector512 source) + { + Vector512 backdropAlpha = Vector512_.ShuffleNative(backdrop, ShuffleAlphaControl); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 left = (backdrop + backdrop) * source; + Vector512 right = (backdropAlpha * sourceAlpha) - (((backdropAlpha - backdrop) * (sourceAlpha - source)) * Vector512.Create(2F)); + Vector512 useRight = Avx512F.CompareGreaterThan(source + source, sourceAlpha); + return Vector512.ConditionalSelect(useRight, right, left); + } + + /// + /// Composites an associated source over an associated destination without a color-blending function. + /// + /// The associated destination vector. + /// The associated source vector. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 OverNormal(Vector4 destination, Vector4 source) + { + // Associated source-over is Ps + Pb(1 - As); both color and alpha therefore use the same coefficient. + Vector4 sourceAlpha = Numerics.PermuteW(source); + return source + (destination * (Vector4.One - sourceAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 OverNormal(Vector256 destination, Vector256 source) + { + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + return source + (destination * (Vector256.Create(1F) - sourceAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverNormal(Vector512 destination, Vector512 source) + { + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + return source + (destination * (Vector512.Create(1F) - sourceAlpha)); + } + + /// + /// Composites an associated source atop an associated destination without a color-blending function. + /// + /// The associated destination vector. + /// The associated source vector. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AtopNormal(Vector4 destination, Vector4 source) + { + // Source-atop retains the destination alpha while replacing its covered contribution with the source. + Vector4 sourceAlpha = Numerics.PermuteW(source); + Vector4 destinationAlpha = Numerics.PermuteW(destination); + return (source * destinationAlpha) + (destination * (Vector4.One - sourceAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AtopNormal(Vector256 destination, Vector256 source) + { + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + Vector256 destinationAlpha = Avx.Permute(destination, ShuffleAlphaControl); + return (source * destinationAlpha) + (destination * (Vector256.Create(1F) - sourceAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AtopNormal(Vector512 destination, Vector512 source) + { + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 destinationAlpha = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + return (source * destinationAlpha) + (destination * (Vector512.Create(1F) - sourceAlpha)); + } + + /// + /// Composites an associated source over an associated destination using an unassociated blended color. + /// + /// The associated destination vector. + /// The associated source vector. + /// The associated overlap term produced by the color-blending function. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Over(Vector4 destination, Vector4 source, Vector4 overlap) + { + // The three terms cover destination-only, source-only, and overlapping color respectively. + Vector4 sourceAlpha = Numerics.PermuteW(source); + Vector4 destinationAlpha = Numerics.PermuteW(destination); + Vector4 result = (destination * (Vector4.One - sourceAlpha)) + (source * (Vector4.One - destinationAlpha)) + overlap; + Vector4 alpha = source + (destination * (Vector4.One - sourceAlpha)); + return Numerics.WithW(result, alpha); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Over(Vector256 destination, Vector256 source, Vector256 overlap) + { + Vector256 one = Vector256.Create(1F); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + Vector256 destinationAlpha = Avx.Permute(destination, ShuffleAlphaControl); + Vector256 result = (destination * (one - sourceAlpha)) + (source * (one - destinationAlpha)) + overlap; + Vector256 alpha = source + (destination * (one - sourceAlpha)); + return Avx.Blend(result, alpha, BlendAlphaControl); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Over(Vector512 destination, Vector512 source, Vector512 overlap) + { + Vector512 one = Vector512.Create(1F); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 destinationAlpha = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + Vector512 result = (destination * (one - sourceAlpha)) + (source * (one - destinationAlpha)) + overlap; + Vector512 alpha = source + (destination * (one - sourceAlpha)); + return Vector512.ConditionalSelect(AlphaMask512(), alpha, result); + } + + /// + /// Composites an associated source atop an associated destination using an unassociated blended color. + /// + /// The associated destination vector. + /// The associated source vector. + /// The associated overlap term produced by the color-blending function. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Atop(Vector4 destination, Vector4 source, Vector4 overlap) + { + // Atop discards source-only color and retains the destination alpha unchanged. + Vector4 sourceAlpha = Numerics.PermuteW(source); + Vector4 destinationAlpha = Numerics.PermuteW(destination); + Vector4 coefficient = Vector4.One - sourceAlpha; + Vector4 result = Vector128_.FusedMultiplyAdd(destination.AsVector128(), coefficient.AsVector128(), overlap.AsVector128()).AsVector4(); + + return Numerics.WithW(result, destinationAlpha); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Atop(Vector256 destination, Vector256 source, Vector256 overlap) + { + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + Vector256 destinationAlpha = Avx.Permute(destination, ShuffleAlphaControl); + Vector256 result = Vector256_.FusedMultiplyAdd(destination, Vector256.Create(1F) - sourceAlpha, overlap); + + return Avx.Blend(result, destinationAlpha, BlendAlphaControl); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Atop(Vector512 destination, Vector512 source, Vector512 overlap) + { + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 destinationAlpha = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + Vector512 result = Vector512_.FusedMultiplyAdd(destination, Vector512.Create(1F) - sourceAlpha, overlap); + + return Vector512.ConditionalSelect(AlphaMask512(), destinationAlpha, result); + } + + /// + /// Retains the associated source within the destination coverage. + /// + /// The associated destination vector. + /// The associated source vector. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 In(Vector4 destination, Vector4 source) => source * Numerics.PermuteW(destination); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 In(Vector256 destination, Vector256 source) + => source * Avx.Permute(destination, ShuffleAlphaControl); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 In(Vector512 destination, Vector512 source) + => source * Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + + /// + /// Retains the associated source outside the destination coverage. + /// + /// The associated destination vector. + /// The associated source vector. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Out(Vector4 destination, Vector4 source) + => source * (Vector4.One - Numerics.PermuteW(destination)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Out(Vector256 destination, Vector256 source) + => source * (Vector256.Create(1F) - Avx.Permute(destination, ShuffleAlphaControl)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Out(Vector512 destination, Vector512 source) + => source * (Vector512.Create(1F) - Vector512_.ShuffleNative(destination, ShuffleAlphaControl)); + + /// + /// Retains only the non-overlapping parts of two associated vectors. + /// + /// The associated destination vector. + /// The associated source vector. + /// The associated composition result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 Xor(Vector4 destination, Vector4 source) + { + Vector4 sourceAlpha = Numerics.PermuteW(source); + Vector4 destinationAlpha = Numerics.PermuteW(destination); + return (source * (Vector4.One - destinationAlpha)) + (destination * (Vector4.One - sourceAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Xor(Vector256 destination, Vector256 source) + { + Vector256 one = Vector256.Create(1F); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + Vector256 destinationAlpha = Avx.Permute(destination, ShuffleAlphaControl); + return (source * (one - destinationAlpha)) + (destination * (one - sourceAlpha)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Xor(Vector512 destination, Vector512 source) + { + Vector512 one = Vector512.Create(1F); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 destinationAlpha = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + return (source * (one - destinationAlpha)) + (destination * (one - sourceAlpha)); + } + + /// + /// Applies raster coverage to an associated composition result. + /// + /// The associated backdrop vector. + /// The associated composition result. + /// The raster coverage in the range 0 through 1. + /// The covered associated result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 BlendWithCoverage(Vector4 backdrop, Vector4 source, float coverage) + { + // Use the same fused operation as the wider paths so exact midpoints cannot change across vector widths. + return Vector128_.FusedMultiplyAdd((source - backdrop).AsVector128(), Vector128.Create(coverage), backdrop.AsVector128()).AsVector4(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 BlendWithCoverage(Vector256 backdrop, Vector256 source, Vector256 coverage) + => Vector256_.FusedMultiplyAdd(source - backdrop, coverage, backdrop); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 BlendWithCoverage(Vector512 backdrop, Vector512 source, Vector512 coverage) + => Vector512_.FusedMultiplyAdd(source - backdrop, coverage, backdrop); + + /// + /// Calculates one associated Overlay overlap component without recovering either straight component. + /// + /// The associated backdrop component. + /// The backdrop alpha. + /// The associated source component. + /// The source alpha. + /// The associated overlap component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float OverlayValue(float backdrop, float backdropAlpha, float source, float sourceAlpha) + { + // Comparing 2Pb with Ab is equivalent to comparing the straight backdrop component with one half. + return (backdrop + backdrop) <= backdropAlpha + ? (backdrop + backdrop) * source + : (backdropAlpha * sourceAlpha) - (2F * (backdropAlpha - backdrop) * (sourceAlpha - source)); + } + + /// + /// Creates a SIMD lane mask selecting the alpha component of each packed vector. + /// + /// The alpha-component mask. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 AlphaMask512() + => Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); +} diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs b/src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs index f62d3c676..298c35999 100644 --- a/src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs @@ -3,11150 +3,2830 @@ // using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; /// -/// Collection of Porter Duff alpha blending functions applying different composition models. +/// Provides the straight-alpha equations consumed by the shared pixel-blending traversal. /// -/// -/// These functions are designed to be a general solution for all color cases, -/// that is, they take in account the alpha value of both the backdrop -/// and source, and there's no need to alpha-premultiply neither the backdrop -/// nor the source. -/// Note there are faster functions for when the backdrop color is known -/// to be opaque -/// -internal static class DefaultPixelBlenders - where TPixel : unmanaged, IPixel +internal static class DefaultPixelBlenderOperators { + /// + /// Applies the "NormalSrc" straight-alpha composition equation. + /// + public readonly struct NormalSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalSrc(background, source, amount); + } + + /// + /// Applies the "MultiplySrc" straight-alpha composition equation. + /// + public readonly struct MultiplySrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplySrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplySrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplySrc(background, source, amount); + } + + /// + /// Applies the "AddSrc" straight-alpha composition equation. + /// + public readonly struct AddSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddSrc(background, source, amount); + } + + /// + /// Applies the "SubtractSrc" straight-alpha composition equation. + /// + public readonly struct SubtractSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractSrc(background, source, amount); + } + + /// + /// Applies the "ScreenSrc" straight-alpha composition equation. + /// + public readonly struct ScreenSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenSrc(background, source, amount); + } + + /// + /// Applies the "DarkenSrc" straight-alpha composition equation. + /// + public readonly struct DarkenSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenSrc(background, source, amount); + } + + /// + /// Applies the "LightenSrc" straight-alpha composition equation. + /// + public readonly struct LightenSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenSrc(background, source, amount); + } + + /// + /// Applies the "OverlaySrc" straight-alpha composition equation. + /// + public readonly struct OverlaySrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlaySrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlaySrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlaySrc(background, source, amount); + } + + /// + /// Applies the "HardLightSrc" straight-alpha composition equation. + /// + public readonly struct HardLightSrc : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightSrc(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightSrc(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightSrc(background, source, amount); + } + + /// + /// Applies the "NormalSrcAtop" straight-alpha composition equation. + /// + public readonly struct NormalSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalSrcAtop(background, source, amount); + } + + /// + /// Applies the "MultiplySrcAtop" straight-alpha composition equation. + /// + public readonly struct MultiplySrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplySrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplySrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplySrcAtop(background, source, amount); + } + + /// + /// Applies the "AddSrcAtop" straight-alpha composition equation. + /// + public readonly struct AddSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddSrcAtop(background, source, amount); + } + + /// + /// Applies the "SubtractSrcAtop" straight-alpha composition equation. + /// + public readonly struct SubtractSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractSrcAtop(background, source, amount); + } + + /// + /// Applies the "ScreenSrcAtop" straight-alpha composition equation. + /// + public readonly struct ScreenSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenSrcAtop(background, source, amount); + } + + /// + /// Applies the "DarkenSrcAtop" straight-alpha composition equation. + /// + public readonly struct DarkenSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenSrcAtop(background, source, amount); + } + + /// + /// Applies the "LightenSrcAtop" straight-alpha composition equation. + /// + public readonly struct LightenSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenSrcAtop(background, source, amount); + } + + /// + /// Applies the "OverlaySrcAtop" straight-alpha composition equation. + /// + public readonly struct OverlaySrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlaySrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlaySrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlaySrcAtop(background, source, amount); + } + + /// + /// Applies the "HardLightSrcAtop" straight-alpha composition equation. + /// + public readonly struct HardLightSrcAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightSrcAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightSrcAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightSrcAtop(background, source, amount); + } + + /// + /// Applies the "NormalSrcOver" straight-alpha composition equation. + /// + public readonly struct NormalSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalSrcOver(background, source, amount); + } + + /// + /// Applies the "MultiplySrcOver" straight-alpha composition equation. + /// + public readonly struct MultiplySrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplySrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplySrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplySrcOver(background, source, amount); + } + + /// + /// Applies the "AddSrcOver" straight-alpha composition equation. + /// + public readonly struct AddSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddSrcOver(background, source, amount); + } + + /// + /// Applies the "SubtractSrcOver" straight-alpha composition equation. + /// + public readonly struct SubtractSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractSrcOver(background, source, amount); + } + + /// + /// Applies the "ScreenSrcOver" straight-alpha composition equation. + /// + public readonly struct ScreenSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenSrcOver(background, source, amount); + } + + /// + /// Applies the "DarkenSrcOver" straight-alpha composition equation. + /// + public readonly struct DarkenSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenSrcOver(background, source, amount); + } + + /// + /// Applies the "LightenSrcOver" straight-alpha composition equation. + /// + public readonly struct LightenSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenSrcOver(background, source, amount); + } + + /// + /// Applies the "OverlaySrcOver" straight-alpha composition equation. + /// + public readonly struct OverlaySrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlaySrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlaySrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlaySrcOver(background, source, amount); + } + + /// + /// Applies the "HardLightSrcOver" straight-alpha composition equation. + /// + public readonly struct HardLightSrcOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightSrcOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightSrcOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightSrcOver(background, source, amount); + } + + /// + /// Applies the "NormalSrcIn" straight-alpha composition equation. + /// + public readonly struct NormalSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalSrcIn(background, source, amount); + } + + /// + /// Applies the "MultiplySrcIn" straight-alpha composition equation. + /// + public readonly struct MultiplySrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplySrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplySrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplySrcIn(background, source, amount); + } + + /// + /// Applies the "AddSrcIn" straight-alpha composition equation. + /// + public readonly struct AddSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddSrcIn(background, source, amount); + } + + /// + /// Applies the "SubtractSrcIn" straight-alpha composition equation. + /// + public readonly struct SubtractSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractSrcIn(background, source, amount); + } + + /// + /// Applies the "ScreenSrcIn" straight-alpha composition equation. + /// + public readonly struct ScreenSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenSrcIn(background, source, amount); + } + + /// + /// Applies the "DarkenSrcIn" straight-alpha composition equation. + /// + public readonly struct DarkenSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenSrcIn(background, source, amount); + } + + /// + /// Applies the "LightenSrcIn" straight-alpha composition equation. + /// + public readonly struct LightenSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenSrcIn(background, source, amount); + } + + /// + /// Applies the "OverlaySrcIn" straight-alpha composition equation. + /// + public readonly struct OverlaySrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlaySrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlaySrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlaySrcIn(background, source, amount); + } + + /// + /// Applies the "HardLightSrcIn" straight-alpha composition equation. + /// + public readonly struct HardLightSrcIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightSrcIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightSrcIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightSrcIn(background, source, amount); + } + + /// + /// Applies the "NormalSrcOut" straight-alpha composition equation. + /// + public readonly struct NormalSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalSrcOut(background, source, amount); + } + + /// + /// Applies the "MultiplySrcOut" straight-alpha composition equation. + /// + public readonly struct MultiplySrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplySrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplySrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplySrcOut(background, source, amount); + } + + /// + /// Applies the "AddSrcOut" straight-alpha composition equation. + /// + public readonly struct AddSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddSrcOut(background, source, amount); + } + + /// + /// Applies the "SubtractSrcOut" straight-alpha composition equation. + /// + public readonly struct SubtractSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractSrcOut(background, source, amount); + } + + /// + /// Applies the "ScreenSrcOut" straight-alpha composition equation. + /// + public readonly struct ScreenSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenSrcOut(background, source, amount); + } + + /// + /// Applies the "DarkenSrcOut" straight-alpha composition equation. + /// + public readonly struct DarkenSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenSrcOut(background, source, amount); + } + + /// + /// Applies the "LightenSrcOut" straight-alpha composition equation. + /// + public readonly struct LightenSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenSrcOut(background, source, amount); + } + + /// + /// Applies the "OverlaySrcOut" straight-alpha composition equation. + /// + public readonly struct OverlaySrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlaySrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlaySrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlaySrcOut(background, source, amount); + } + + /// + /// Applies the "HardLightSrcOut" straight-alpha composition equation. + /// + public readonly struct HardLightSrcOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightSrcOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightSrcOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightSrcOut(background, source, amount); + } + + /// + /// Applies the "NormalDest" straight-alpha composition equation. + /// + public readonly struct NormalDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalDest(background, source, amount); + } + + /// + /// Applies the "MultiplyDest" straight-alpha composition equation. + /// + public readonly struct MultiplyDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplyDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplyDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplyDest(background, source, amount); + } + + /// + /// Applies the "AddDest" straight-alpha composition equation. + /// + public readonly struct AddDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddDest(background, source, amount); + } + + /// + /// Applies the "SubtractDest" straight-alpha composition equation. + /// + public readonly struct SubtractDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractDest(background, source, amount); + } + + /// + /// Applies the "ScreenDest" straight-alpha composition equation. + /// + public readonly struct ScreenDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenDest(background, source, amount); + } + + /// + /// Applies the "DarkenDest" straight-alpha composition equation. + /// + public readonly struct DarkenDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenDest(background, source, amount); + } + + /// + /// Applies the "LightenDest" straight-alpha composition equation. + /// + public readonly struct LightenDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenDest(background, source, amount); + } + + /// + /// Applies the "OverlayDest" straight-alpha composition equation. + /// + public readonly struct OverlayDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlayDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlayDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlayDest(background, source, amount); + } + + /// + /// Applies the "HardLightDest" straight-alpha composition equation. + /// + public readonly struct HardLightDest : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightDest(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightDest(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightDest(background, source, amount); + } + + /// + /// Applies the "NormalDestAtop" straight-alpha composition equation. + /// + public readonly struct NormalDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalDestAtop(background, source, amount); + } + + /// + /// Applies the "MultiplyDestAtop" straight-alpha composition equation. + /// + public readonly struct MultiplyDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplyDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplyDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplyDestAtop(background, source, amount); + } + + /// + /// Applies the "AddDestAtop" straight-alpha composition equation. + /// + public readonly struct AddDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddDestAtop(background, source, amount); + } + + /// + /// Applies the "SubtractDestAtop" straight-alpha composition equation. + /// + public readonly struct SubtractDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractDestAtop(background, source, amount); + } + + /// + /// Applies the "ScreenDestAtop" straight-alpha composition equation. + /// + public readonly struct ScreenDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenDestAtop(background, source, amount); + } + + /// + /// Applies the "DarkenDestAtop" straight-alpha composition equation. + /// + public readonly struct DarkenDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenDestAtop(background, source, amount); + } + + /// + /// Applies the "LightenDestAtop" straight-alpha composition equation. + /// + public readonly struct LightenDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenDestAtop(background, source, amount); + } + + /// + /// Applies the "OverlayDestAtop" straight-alpha composition equation. + /// + public readonly struct OverlayDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlayDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlayDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlayDestAtop(background, source, amount); + } + + /// + /// Applies the "HardLightDestAtop" straight-alpha composition equation. + /// + public readonly struct HardLightDestAtop : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightDestAtop(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightDestAtop(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightDestAtop(background, source, amount); + } + + /// + /// Applies the "NormalDestOver" straight-alpha composition equation. + /// + public readonly struct NormalDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalDestOver(background, source, amount); + } + + /// + /// Applies the "MultiplyDestOver" straight-alpha composition equation. + /// + public readonly struct MultiplyDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplyDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplyDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplyDestOver(background, source, amount); + } + + /// + /// Applies the "AddDestOver" straight-alpha composition equation. + /// + public readonly struct AddDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddDestOver(background, source, amount); + } + + /// + /// Applies the "SubtractDestOver" straight-alpha composition equation. + /// + public readonly struct SubtractDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractDestOver(background, source, amount); + } + + /// + /// Applies the "ScreenDestOver" straight-alpha composition equation. + /// + public readonly struct ScreenDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenDestOver(background, source, amount); + } + + /// + /// Applies the "DarkenDestOver" straight-alpha composition equation. + /// + public readonly struct DarkenDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenDestOver(background, source, amount); + } + + /// + /// Applies the "LightenDestOver" straight-alpha composition equation. + /// + public readonly struct LightenDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenDestOver(background, source, amount); + } + + /// + /// Applies the "OverlayDestOver" straight-alpha composition equation. + /// + public readonly struct OverlayDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlayDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlayDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlayDestOver(background, source, amount); + } + + /// + /// Applies the "HardLightDestOver" straight-alpha composition equation. + /// + public readonly struct HardLightDestOver : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightDestOver(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightDestOver(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightDestOver(background, source, amount); + } + + /// + /// Applies the "NormalDestIn" straight-alpha composition equation. + /// + public readonly struct NormalDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalDestIn(background, source, amount); + } + + /// + /// Applies the "MultiplyDestIn" straight-alpha composition equation. + /// + public readonly struct MultiplyDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplyDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplyDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplyDestIn(background, source, amount); + } + + /// + /// Applies the "AddDestIn" straight-alpha composition equation. + /// + public readonly struct AddDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddDestIn(background, source, amount); + } + + /// + /// Applies the "SubtractDestIn" straight-alpha composition equation. + /// + public readonly struct SubtractDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractDestIn(background, source, amount); + } + + /// + /// Applies the "ScreenDestIn" straight-alpha composition equation. + /// + public readonly struct ScreenDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenDestIn(background, source, amount); + } + + /// + /// Applies the "DarkenDestIn" straight-alpha composition equation. + /// + public readonly struct DarkenDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenDestIn(background, source, amount); + } + + /// + /// Applies the "LightenDestIn" straight-alpha composition equation. + /// + public readonly struct LightenDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenDestIn(background, source, amount); + } + + /// + /// Applies the "OverlayDestIn" straight-alpha composition equation. + /// + public readonly struct OverlayDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlayDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlayDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlayDestIn(background, source, amount); + } + + /// + /// Applies the "HardLightDestIn" straight-alpha composition equation. + /// + public readonly struct HardLightDestIn : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightDestIn(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightDestIn(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightDestIn(background, source, amount); + } + + /// + /// Applies the "NormalDestOut" straight-alpha composition equation. + /// + public readonly struct NormalDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalDestOut(background, source, amount); + } + + /// + /// Applies the "MultiplyDestOut" straight-alpha composition equation. + /// + public readonly struct MultiplyDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplyDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplyDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplyDestOut(background, source, amount); + } + + /// + /// Applies the "AddDestOut" straight-alpha composition equation. + /// + public readonly struct AddDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddDestOut(background, source, amount); + } + + /// + /// Applies the "SubtractDestOut" straight-alpha composition equation. + /// + public readonly struct SubtractDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractDestOut(background, source, amount); + } + + /// + /// Applies the "ScreenDestOut" straight-alpha composition equation. + /// + public readonly struct ScreenDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenDestOut(background, source, amount); + } + + /// + /// Applies the "DarkenDestOut" straight-alpha composition equation. + /// + public readonly struct DarkenDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenDestOut(background, source, amount); + } + + /// + /// Applies the "LightenDestOut" straight-alpha composition equation. + /// + public readonly struct LightenDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenDestOut(background, source, amount); + } + + /// + /// Applies the "OverlayDestOut" straight-alpha composition equation. + /// + public readonly struct OverlayDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlayDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlayDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlayDestOut(background, source, amount); + } + + /// + /// Applies the "HardLightDestOut" straight-alpha composition equation. + /// + public readonly struct HardLightDestOut : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightDestOut(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightDestOut(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightDestOut(background, source, amount); + } + + /// + /// Applies the "NormalClear" straight-alpha composition equation. + /// + public readonly struct NormalClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalClear(background, source, amount); + } + + /// + /// Applies the "MultiplyClear" straight-alpha composition equation. + /// + public readonly struct MultiplyClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplyClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplyClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplyClear(background, source, amount); + } + + /// + /// Applies the "AddClear" straight-alpha composition equation. + /// + public readonly struct AddClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddClear(background, source, amount); + } + + /// + /// Applies the "SubtractClear" straight-alpha composition equation. + /// + public readonly struct SubtractClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractClear(background, source, amount); + } + + /// + /// Applies the "ScreenClear" straight-alpha composition equation. + /// + public readonly struct ScreenClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenClear(background, source, amount); + } + + /// + /// Applies the "DarkenClear" straight-alpha composition equation. + /// + public readonly struct DarkenClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenClear(background, source, amount); + } + + /// + /// Applies the "LightenClear" straight-alpha composition equation. + /// + public readonly struct LightenClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenClear(background, source, amount); + } + + /// + /// Applies the "OverlayClear" straight-alpha composition equation. + /// + public readonly struct OverlayClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlayClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlayClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlayClear(background, source, amount); + } + + /// + /// Applies the "HardLightClear" straight-alpha composition equation. + /// + public readonly struct HardLightClear : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightClear(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightClear(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightClear(background, source, amount); + } + + /// + /// Applies the "NormalXor" straight-alpha composition equation. + /// + public readonly struct NormalXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.NormalXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.NormalXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.NormalXor(background, source, amount); + } + + /// + /// Applies the "MultiplyXor" straight-alpha composition equation. + /// + public readonly struct MultiplyXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.MultiplyXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.MultiplyXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.MultiplyXor(background, source, amount); + } + + /// + /// Applies the "AddXor" straight-alpha composition equation. + /// + public readonly struct AddXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.AddXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.AddXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.AddXor(background, source, amount); + } + + /// + /// Applies the "SubtractXor" straight-alpha composition equation. + /// + public readonly struct SubtractXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.SubtractXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.SubtractXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.SubtractXor(background, source, amount); + } + + /// + /// Applies the "ScreenXor" straight-alpha composition equation. + /// + public readonly struct ScreenXor : IPixelBlenderOperator + { + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.ScreenXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.ScreenXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.ScreenXor(background, source, amount); + } /// - /// A pixel blender that implements the "NormalSrc" composition equation. + /// Applies the "DarkenXor" straight-alpha composition equation. /// - public class NormalSrc : PixelBlender + public readonly struct DarkenXor : IPixelBlenderOperator { - /// - /// Gets the static instance of this blender. - /// - public static NormalSrc Instance { get; } = new NormalSrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.DarkenXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.DarkenXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.DarkenXor(background, source, amount); } /// - /// A pixel blender that implements the "MultiplySrc" composition equation. + /// Applies the "LightenXor" straight-alpha composition equation. /// - public class MultiplySrc : PixelBlender + public readonly struct LightenXor : IPixelBlenderOperator { - /// - /// Gets the static instance of this blender. - /// - public static MultiplySrc Instance { get; } = new MultiplySrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplySrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.LightenXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.LightenXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.LightenXor(background, source, amount); } /// - /// A pixel blender that implements the "AddSrc" composition equation. + /// Applies the "OverlayXor" straight-alpha composition equation. /// - public class AddSrc : PixelBlender + public readonly struct OverlayXor : IPixelBlenderOperator { - /// - /// Gets the static instance of this blender. - /// - public static AddSrc Instance { get; } = new AddSrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.OverlayXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.OverlayXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.OverlayXor(background, source, amount); } /// - /// A pixel blender that implements the "SubtractSrc" composition equation. + /// Applies the "HardLightXor" straight-alpha composition equation. /// - public class SubtractSrc : PixelBlender + public readonly struct HardLightXor : IPixelBlenderOperator { - /// - /// Gets the static instance of this blender. - /// - public static SubtractSrc Instance { get; } = new SubtractSrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } + /// + public static bool IsAssociatedAlpha => false; + + /// + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.HardLightXor(background, source, amount); + + /// + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.HardLightXor(background, source, amount); + + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.HardLightXor(background, source, amount); } +} + +/// +/// Provides straight-alpha pixel blenders for each color and alpha composition pair. +/// +/// The destination pixel format. +internal static class DefaultPixelBlenders + where TPixel : unmanaged, IPixel +{ /// - /// A pixel blender that implements the "ScreenSrc" composition equation. + /// Gets the shared blender for the "NormalSrc" composition equation. /// - public class ScreenSrc : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenSrc Instance { get; } = new ScreenSrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalSrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenSrc" composition equation. + /// Gets the shared blender for the "MultiplySrc" composition equation. /// - public class DarkenSrc : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenSrc Instance { get; } = new DarkenSrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplySrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenSrc" composition equation. + /// Gets the shared blender for the "AddSrc" composition equation. /// - public class LightenSrc : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenSrc Instance { get; } = new LightenSrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddSrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlaySrc" composition equation. + /// Gets the shared blender for the "SubtractSrc" composition equation. /// - public class OverlaySrc : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlaySrc Instance { get; } = new OverlaySrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlaySrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractSrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightSrc" composition equation. + /// Gets the shared blender for the "ScreenSrc" composition equation. /// - public class HardLightSrc : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightSrc Instance { get; } = new HardLightSrc(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrc(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightSrc(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrc(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenSrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalSrcAtop" composition equation. + /// Gets the shared blender for the "DarkenSrc" composition equation. /// - public class NormalSrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalSrcAtop Instance { get; } = new NormalSrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenSrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplySrcAtop" composition equation. + /// Gets the shared blender for the "LightenSrc" composition equation. /// - public class MultiplySrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplySrcAtop Instance { get; } = new MultiplySrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplySrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenSrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddSrcAtop" composition equation. + /// Gets the shared blender for the "OverlaySrc" composition equation. /// - public class AddSrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddSrcAtop Instance { get; } = new AddSrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlaySrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractSrcAtop" composition equation. + /// Gets the shared blender for the "HardLightSrc" composition equation. /// - public class SubtractSrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractSrcAtop Instance { get; } = new SubtractSrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightSrc => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenSrcAtop" composition equation. + /// Gets the shared blender for the "NormalSrcAtop" composition equation. /// - public class ScreenSrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenSrcAtop Instance { get; } = new ScreenSrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalSrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenSrcAtop" composition equation. + /// Gets the shared blender for the "MultiplySrcAtop" composition equation. /// - public class DarkenSrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenSrcAtop Instance { get; } = new DarkenSrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplySrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenSrcAtop" composition equation. + /// Gets the shared blender for the "AddSrcAtop" composition equation. /// - public class LightenSrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenSrcAtop Instance { get; } = new LightenSrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddSrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlaySrcAtop" composition equation. + /// Gets the shared blender for the "SubtractSrcAtop" composition equation. /// - public class OverlaySrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlaySrcAtop Instance { get; } = new OverlaySrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlaySrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractSrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightSrcAtop" composition equation. + /// Gets the shared blender for the "ScreenSrcAtop" composition equation. /// - public class HardLightSrcAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightSrcAtop Instance { get; } = new HardLightSrcAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrcAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightSrcAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrcAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenSrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalSrcOver" composition equation. + /// Gets the shared blender for the "DarkenSrcAtop" composition equation. /// - public class NormalSrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalSrcOver Instance { get; } = new NormalSrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenSrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplySrcOver" composition equation. + /// Gets the shared blender for the "LightenSrcAtop" composition equation. /// - public class MultiplySrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplySrcOver Instance { get; } = new MultiplySrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplySrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenSrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddSrcOver" composition equation. + /// Gets the shared blender for the "OverlaySrcAtop" composition equation. /// - public class AddSrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddSrcOver Instance { get; } = new AddSrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlaySrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractSrcOver" composition equation. + /// Gets the shared blender for the "HardLightSrcAtop" composition equation. /// - public class SubtractSrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractSrcOver Instance { get; } = new SubtractSrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightSrcAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenSrcOver" composition equation. + /// Gets the shared blender for the "NormalSrcOver" composition equation. /// - public class ScreenSrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenSrcOver Instance { get; } = new ScreenSrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalSrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenSrcOver" composition equation. + /// Gets the shared blender for the "MultiplySrcOver" composition equation. /// - public class DarkenSrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenSrcOver Instance { get; } = new DarkenSrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplySrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenSrcOver" composition equation. + /// Gets the shared blender for the "AddSrcOver" composition equation. /// - public class LightenSrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenSrcOver Instance { get; } = new LightenSrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddSrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlaySrcOver" composition equation. + /// Gets the shared blender for the "SubtractSrcOver" composition equation. /// - public class OverlaySrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlaySrcOver Instance { get; } = new OverlaySrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlaySrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractSrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightSrcOver" composition equation. + /// Gets the shared blender for the "ScreenSrcOver" composition equation. /// - public class HardLightSrcOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightSrcOver Instance { get; } = new HardLightSrcOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrcOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightSrcOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrcOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenSrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalSrcIn" composition equation. + /// Gets the shared blender for the "DarkenSrcOver" composition equation. /// - public class NormalSrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalSrcIn Instance { get; } = new NormalSrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenSrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplySrcIn" composition equation. + /// Gets the shared blender for the "LightenSrcOver" composition equation. /// - public class MultiplySrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplySrcIn Instance { get; } = new MultiplySrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplySrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenSrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddSrcIn" composition equation. + /// Gets the shared blender for the "OverlaySrcOver" composition equation. /// - public class AddSrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddSrcIn Instance { get; } = new AddSrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlaySrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractSrcIn" composition equation. + /// Gets the shared blender for the "HardLightSrcOver" composition equation. /// - public class SubtractSrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractSrcIn Instance { get; } = new SubtractSrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightSrcOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenSrcIn" composition equation. + /// Gets the shared blender for the "NormalSrcIn" composition equation. /// - public class ScreenSrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenSrcIn Instance { get; } = new ScreenSrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalSrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenSrcIn" composition equation. + /// Gets the shared blender for the "MultiplySrcIn" composition equation. /// - public class DarkenSrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenSrcIn Instance { get; } = new DarkenSrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplySrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenSrcIn" composition equation. + /// Gets the shared blender for the "AddSrcIn" composition equation. /// - public class LightenSrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenSrcIn Instance { get; } = new LightenSrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddSrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlaySrcIn" composition equation. + /// Gets the shared blender for the "SubtractSrcIn" composition equation. /// - public class OverlaySrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlaySrcIn Instance { get; } = new OverlaySrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlaySrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractSrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightSrcIn" composition equation. + /// Gets the shared blender for the "ScreenSrcIn" composition equation. /// - public class HardLightSrcIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightSrcIn Instance { get; } = new HardLightSrcIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrcIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightSrcIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrcIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenSrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalSrcOut" composition equation. + /// Gets the shared blender for the "DarkenSrcIn" composition equation. /// - public class NormalSrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalSrcOut Instance { get; } = new NormalSrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenSrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplySrcOut" composition equation. + /// Gets the shared blender for the "LightenSrcIn" composition equation. /// - public class MultiplySrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplySrcOut Instance { get; } = new MultiplySrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplySrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplySrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenSrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddSrcOut" composition equation. + /// Gets the shared blender for the "OverlaySrcIn" composition equation. /// - public class AddSrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddSrcOut Instance { get; } = new AddSrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlaySrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractSrcOut" composition equation. + /// Gets the shared blender for the "HardLightSrcIn" composition equation. /// - public class SubtractSrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractSrcOut Instance { get; } = new SubtractSrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightSrcIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenSrcOut" composition equation. + /// Gets the shared blender for the "NormalSrcOut" composition equation. /// - public class ScreenSrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenSrcOut Instance { get; } = new ScreenSrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalSrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenSrcOut" composition equation. + /// Gets the shared blender for the "MultiplySrcOut" composition equation. /// - public class DarkenSrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenSrcOut Instance { get; } = new DarkenSrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplySrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenSrcOut" composition equation. + /// Gets the shared blender for the "AddSrcOut" composition equation. /// - public class LightenSrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenSrcOut Instance { get; } = new LightenSrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddSrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlaySrcOut" composition equation. + /// Gets the shared blender for the "SubtractSrcOut" composition equation. /// - public class OverlaySrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlaySrcOut Instance { get; } = new OverlaySrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlaySrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlaySrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlaySrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractSrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightSrcOut" composition equation. + /// Gets the shared blender for the "ScreenSrcOut" composition equation. /// - public class HardLightSrcOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightSrcOut Instance { get; } = new HardLightSrcOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightSrcOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightSrcOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightSrcOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenSrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalDest" composition equation. + /// Gets the shared blender for the "DarkenSrcOut" composition equation. /// - public class NormalDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalDest Instance { get; } = new NormalDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenSrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplyDest" composition equation. + /// Gets the shared blender for the "LightenSrcOut" composition equation. /// - public class MultiplyDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplyDest Instance { get; } = new MultiplyDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplyDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenSrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddDest" composition equation. + /// Gets the shared blender for the "OverlaySrcOut" composition equation. /// - public class AddDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddDest Instance { get; } = new AddDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlaySrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractDest" composition equation. + /// Gets the shared blender for the "HardLightSrcOut" composition equation. /// - public class SubtractDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractDest Instance { get; } = new SubtractDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightSrcOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenDest" composition equation. + /// Gets the shared blender for the "NormalDest" composition equation. /// - public class ScreenDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenDest Instance { get; } = new ScreenDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenDest" composition equation. + /// Gets the shared blender for the "MultiplyDest" composition equation. /// - public class DarkenDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenDest Instance { get; } = new DarkenDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplyDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenDest" composition equation. + /// Gets the shared blender for the "AddDest" composition equation. /// - public class LightenDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenDest Instance { get; } = new LightenDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlayDest" composition equation. + /// Gets the shared blender for the "SubtractDest" composition equation. /// - public class OverlayDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlayDest Instance { get; } = new OverlayDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlayDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightDest" composition equation. + /// Gets the shared blender for the "ScreenDest" composition equation. /// - public class HardLightDest : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightDest Instance { get; } = new HardLightDest(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDest(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightDest(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDest(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalDestAtop" composition equation. + /// Gets the shared blender for the "DarkenDest" composition equation. /// - public class NormalDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalDestAtop Instance { get; } = new NormalDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplyDestAtop" composition equation. + /// Gets the shared blender for the "LightenDest" composition equation. /// - public class MultiplyDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplyDestAtop Instance { get; } = new MultiplyDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplyDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddDestAtop" composition equation. + /// Gets the shared blender for the "OverlayDest" composition equation. /// - public class AddDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddDestAtop Instance { get; } = new AddDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlayDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractDestAtop" composition equation. + /// Gets the shared blender for the "HardLightDest" composition equation. /// - public class SubtractDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractDestAtop Instance { get; } = new SubtractDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightDest => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenDestAtop" composition equation. + /// Gets the shared blender for the "NormalDestAtop" composition equation. /// - public class ScreenDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenDestAtop Instance { get; } = new ScreenDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenDestAtop" composition equation. + /// Gets the shared blender for the "MultiplyDestAtop" composition equation. /// - public class DarkenDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenDestAtop Instance { get; } = new DarkenDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplyDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenDestAtop" composition equation. + /// Gets the shared blender for the "AddDestAtop" composition equation. /// - public class LightenDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenDestAtop Instance { get; } = new LightenDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlayDestAtop" composition equation. + /// Gets the shared blender for the "SubtractDestAtop" composition equation. /// - public class OverlayDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlayDestAtop Instance { get; } = new OverlayDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlayDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightDestAtop" composition equation. + /// Gets the shared blender for the "ScreenDestAtop" composition equation. /// - public class HardLightDestAtop : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightDestAtop Instance { get; } = new HardLightDestAtop(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDestAtop(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightDestAtop(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDestAtop(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalDestOver" composition equation. + /// Gets the shared blender for the "DarkenDestAtop" composition equation. /// - public class NormalDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalDestOver Instance { get; } = new NormalDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplyDestOver" composition equation. + /// Gets the shared blender for the "LightenDestAtop" composition equation. /// - public class MultiplyDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplyDestOver Instance { get; } = new MultiplyDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplyDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddDestOver" composition equation. + /// Gets the shared blender for the "OverlayDestAtop" composition equation. /// - public class AddDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddDestOver Instance { get; } = new AddDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlayDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractDestOver" composition equation. + /// Gets the shared blender for the "HardLightDestAtop" composition equation. /// - public class SubtractDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractDestOver Instance { get; } = new SubtractDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightDestAtop => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenDestOver" composition equation. + /// Gets the shared blender for the "NormalDestOver" composition equation. /// - public class ScreenDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenDestOver Instance { get; } = new ScreenDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenDestOver" composition equation. + /// Gets the shared blender for the "MultiplyDestOver" composition equation. /// - public class DarkenDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenDestOver Instance { get; } = new DarkenDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplyDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenDestOver" composition equation. + /// Gets the shared blender for the "AddDestOver" composition equation. /// - public class LightenDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenDestOver Instance { get; } = new LightenDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlayDestOver" composition equation. + /// Gets the shared blender for the "SubtractDestOver" composition equation. /// - public class OverlayDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlayDestOver Instance { get; } = new OverlayDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlayDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightDestOver" composition equation. + /// Gets the shared blender for the "ScreenDestOver" composition equation. /// - public class HardLightDestOver : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightDestOver Instance { get; } = new HardLightDestOver(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDestOver(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightDestOver(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDestOver(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalDestIn" composition equation. + /// Gets the shared blender for the "DarkenDestOver" composition equation. /// - public class NormalDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalDestIn Instance { get; } = new NormalDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplyDestIn" composition equation. + /// Gets the shared blender for the "LightenDestOver" composition equation. /// - public class MultiplyDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplyDestIn Instance { get; } = new MultiplyDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplyDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddDestIn" composition equation. + /// Gets the shared blender for the "OverlayDestOver" composition equation. /// - public class AddDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddDestIn Instance { get; } = new AddDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlayDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractDestIn" composition equation. + /// Gets the shared blender for the "HardLightDestOver" composition equation. /// - public class SubtractDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractDestIn Instance { get; } = new SubtractDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightDestOver => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenDestIn" composition equation. + /// Gets the shared blender for the "NormalDestIn" composition equation. /// - public class ScreenDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenDestIn Instance { get; } = new ScreenDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenDestIn" composition equation. + /// Gets the shared blender for the "MultiplyDestIn" composition equation. /// - public class DarkenDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenDestIn Instance { get; } = new DarkenDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplyDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenDestIn" composition equation. + /// Gets the shared blender for the "AddDestIn" composition equation. /// - public class LightenDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenDestIn Instance { get; } = new LightenDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlayDestIn" composition equation. + /// Gets the shared blender for the "SubtractDestIn" composition equation. /// - public class OverlayDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlayDestIn Instance { get; } = new OverlayDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlayDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightDestIn" composition equation. + /// Gets the shared blender for the "ScreenDestIn" composition equation. /// - public class HardLightDestIn : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightDestIn Instance { get; } = new HardLightDestIn(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDestIn(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightDestIn(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDestIn(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalDestOut" composition equation. + /// Gets the shared blender for the "DarkenDestIn" composition equation. /// - public class NormalDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalDestOut Instance { get; } = new NormalDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplyDestOut" composition equation. + /// Gets the shared blender for the "LightenDestIn" composition equation. /// - public class MultiplyDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplyDestOut Instance { get; } = new MultiplyDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplyDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddDestOut" composition equation. + /// Gets the shared blender for the "OverlayDestIn" composition equation. /// - public class AddDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddDestOut Instance { get; } = new AddDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlayDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractDestOut" composition equation. + /// Gets the shared blender for the "HardLightDestIn" composition equation. /// - public class SubtractDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractDestOut Instance { get; } = new SubtractDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightDestIn => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenDestOut" composition equation. + /// Gets the shared blender for the "NormalDestOut" composition equation. /// - public class ScreenDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenDestOut Instance { get; } = new ScreenDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenDestOut" composition equation. + /// Gets the shared blender for the "MultiplyDestOut" composition equation. /// - public class DarkenDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenDestOut Instance { get; } = new DarkenDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplyDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenDestOut" composition equation. + /// Gets the shared blender for the "AddDestOut" composition equation. /// - public class LightenDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenDestOut Instance { get; } = new LightenDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlayDestOut" composition equation. + /// Gets the shared blender for the "SubtractDestOut" composition equation. /// - public class OverlayDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlayDestOut Instance { get; } = new OverlayDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlayDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightDestOut" composition equation. + /// Gets the shared blender for the "ScreenDestOut" composition equation. /// - public class HardLightDestOut : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightDestOut Instance { get; } = new HardLightDestOut(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightDestOut(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightDestOut(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightDestOut(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalClear" composition equation. + /// Gets the shared blender for the "DarkenDestOut" composition equation. /// - public class NormalClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalClear Instance { get; } = new NormalClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplyClear" composition equation. + /// Gets the shared blender for the "LightenDestOut" composition equation. /// - public class MultiplyClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplyClear Instance { get; } = new MultiplyClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplyClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddClear" composition equation. + /// Gets the shared blender for the "OverlayDestOut" composition equation. /// - public class AddClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddClear Instance { get; } = new AddClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlayDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractClear" composition equation. + /// Gets the shared blender for the "HardLightDestOut" composition equation. /// - public class SubtractClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractClear Instance { get; } = new SubtractClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightDestOut => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenClear" composition equation. + /// Gets the shared blender for the "NormalClear" composition equation. /// - public class ScreenClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenClear Instance { get; } = new ScreenClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenClear" composition equation. + /// Gets the shared blender for the "MultiplyClear" composition equation. /// - public class DarkenClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenClear Instance { get; } = new DarkenClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplyClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenClear" composition equation. + /// Gets the shared blender for the "AddClear" composition equation. /// - public class LightenClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenClear Instance { get; } = new LightenClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlayClear" composition equation. + /// Gets the shared blender for the "SubtractClear" composition equation. /// - public class OverlayClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlayClear Instance { get; } = new OverlayClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlayClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightClear" composition equation. + /// Gets the shared blender for the "ScreenClear" composition equation. /// - public class HardLightClear : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightClear Instance { get; } = new HardLightClear(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightClear(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightClear(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightClear(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "NormalXor" composition equation. + /// Gets the shared blender for the "DarkenClear" composition equation. /// - public class NormalXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static NormalXor Instance { get; } = new NormalXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.NormalXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.NormalXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.NormalXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender DarkenClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "MultiplyXor" composition equation. + /// Gets the shared blender for the "LightenClear" composition equation. /// - public class MultiplyXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static MultiplyXor Instance { get; } = new MultiplyXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.MultiplyXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.MultiplyXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.MultiplyXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender LightenClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "AddXor" composition equation. + /// Gets the shared blender for the "OverlayClear" composition equation. /// - public class AddXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static AddXor Instance { get; } = new AddXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.AddXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.AddXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.AddXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender OverlayClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "SubtractXor" composition equation. + /// Gets the shared blender for the "HardLightClear" composition equation. /// - public class SubtractXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static SubtractXor Instance { get; } = new SubtractXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.SubtractXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.SubtractXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.SubtractXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender HardLightClear => PixelBlender.Instance; /// - /// A pixel blender that implements the "ScreenXor" composition equation. + /// Gets the shared blender for the "NormalXor" composition equation. /// - public class ScreenXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static ScreenXor Instance { get; } = new ScreenXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.ScreenXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.ScreenXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.ScreenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender NormalXor => PixelBlender.Instance; /// - /// A pixel blender that implements the "DarkenXor" composition equation. + /// Gets the shared blender for the "MultiplyXor" composition equation. /// - public class DarkenXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static DarkenXor Instance { get; } = new DarkenXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.DarkenXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.DarkenXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.DarkenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender MultiplyXor => PixelBlender.Instance; /// - /// A pixel blender that implements the "LightenXor" composition equation. + /// Gets the shared blender for the "AddXor" composition equation. /// - public class LightenXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static LightenXor Instance { get; } = new LightenXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.LightenXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.LightenXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.LightenXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender AddXor => PixelBlender.Instance; /// - /// A pixel blender that implements the "OverlayXor" composition equation. + /// Gets the shared blender for the "SubtractXor" composition equation. /// - public class OverlayXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static OverlayXor Instance { get; } = new OverlayXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.OverlayXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.OverlayXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.OverlayXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender SubtractXor => PixelBlender.Instance; /// - /// A pixel blender that implements the "HardLightXor" composition equation. + /// Gets the shared blender for the "ScreenXor" composition equation. /// - public class HardLightXor : PixelBlender - { - /// - /// Gets the static instance of this blender. - /// - public static HardLightXor Instance { get; } = new HardLightXor(); - - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.HardLightXor(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], amount); - } - } - } - - /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.HardLightXor(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.HardLightXor(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } - } + public static PixelBlender ScreenXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "DarkenXor" composition equation. + /// + public static PixelBlender DarkenXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "LightenXor" composition equation. + /// + public static PixelBlender LightenXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "OverlayXor" composition equation. + /// + public static PixelBlender OverlayXor => PixelBlender.Instance; + + /// + /// Gets the shared blender for the "HardLightXor" composition equation. + /// + public static PixelBlender HardLightXor => PixelBlender.Instance; } diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt b/src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt index 5b71bb0a6..e9bdbaf3c 100644 --- a/src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt +++ b/src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt @@ -4,39 +4,19 @@ #> <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> -<#@ import namespace="System.Linq" #> -<#@ import namespace="System.Text" #> -<#@ import namespace="System.Collections.Generic" #> <#@ output extension=".cs" #> // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. // using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; -/// -/// Collection of Porter Duff alpha blending functions applying different composition models. -/// -/// -/// These functions are designed to be a general solution for all color cases, -/// that is, they take in account the alpha value of both the backdrop -/// and source, and there's no need to alpha-premultiply neither the backdrop -/// nor the source. -/// Note there are faster functions for when the backdrop color is known -/// to be opaque -/// -internal static class DefaultPixelBlenders - where TPixel : unmanaged, IPixel -{ - <# -var composers = new []{ +var composers = new [] +{ "Src", "SrcAtop", "SrcOver", @@ -51,7 +31,8 @@ var composers = new []{ "Xor", }; -var blenders = new []{ +var blenders = new [] +{ "Normal", "Multiply", "Add", @@ -60,120 +41,69 @@ var blenders = new []{ "Darken", "Lighten", "Overlay", - "HardLight" + "HardLight", }; - - foreach(var composer in composers) { - foreach(var blender in blenders) { - - var blender_composer= $"{blender}{composer}"; +#> +/// +/// Provides the straight-alpha equations consumed by the shared pixel-blending traversal. +/// +internal static class DefaultPixelBlenderOperators +{ +<# +foreach (var composer in composers) +{ + foreach (var blender in blenders) + { + var blenderComposer = $"{blender}{composer}"; #> /// - /// A pixel blender that implements the "<#= blender_composer#>" composition equation. + /// Applies the "<#= blenderComposer #>" straight-alpha composition equation. /// - public class <#= blender_composer#> : PixelBlender + public readonly struct <#= blenderComposer #> : IPixelBlenderOperator { - /// - /// Gets the static instance of this blender. - /// - public static <#=blender_composer#> Instance { get; } = new <#=blender_composer#>(); - /// - public override TPixel Blend(TPixel background, TPixel source, float amount) - { - return TPixel.FromScaledVector4(PorterDuffFunctions.<#=blender_composer#>(background.ToScaledVector4(), source.ToScaledVector4(), Numerics.Clamp(amount, 0, 1))); - } + public static bool IsAssociatedAlpha => false; /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) - { - amount = Numerics.Clamp(amount, 0, 1); - - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); - - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - Vector256 opacity = Vector256.Create(amount); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], amount); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], amount); - } - } - } + public static Vector4 Invoke(Vector4 background, Vector4 source, float amount) + => PorterDuffFunctions.<#= blenderComposer #>(background, source, amount); /// - protected override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) - { - if (Avx2.IsSupported && destination.Length >= 2) - { - // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - ref Vector256 destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u); + public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount) + => PorterDuffFunctions.<#= blenderComposer #>(background, source, amount); - ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref float amountBase = ref MemoryMarshal.GetReference(amount); - - Vector256 vOne = Vector256.Create(1F); - - while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast)) - { - // We need to create a Vector256 containing the current and next amount values - // taking up each half of the Vector256 and then clamp them. - Vector256 opacity = Vector256.Create( - Vector128.Create(amountBase), - Vector128.Create(Unsafe.Add(ref amountBase, 1))); - opacity = Avx.Min(Avx.Max(Vector256.Zero, opacity), vOne); - - destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity); - destinationBase = ref Unsafe.Add(ref destinationBase, 1); - backgroundBase = ref Unsafe.Add(ref backgroundBase, 1); - sourceBase = ref Unsafe.Add(ref sourceBase, 1); - amountBase = ref Unsafe.Add(ref amountBase, 2); - } - - if (Numerics.Modulo2(destination.Length) != 0) - { - // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. - int i = destination.Length - 1; - destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - else - { - for (int i = 0; i < destination.Length; i++) - { - destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source[i], Numerics.Clamp(amount[i], 0, 1F)); - } - } - } + /// + public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount) + => PorterDuffFunctions.<#= blenderComposer #>(background, source, amount); } <# } } +#> +} + +/// +/// Provides straight-alpha pixel blenders for each color and alpha composition pair. +/// +/// The destination pixel format. +internal static class DefaultPixelBlenders + where TPixel : unmanaged, IPixel +{ +<# +foreach (var composer in composers) +{ + foreach (var blender in blenders) + { + var blenderComposer = $"{blender}{composer}"; +#> + /// + /// Gets the shared blender for the "<#= blenderComposer #>" composition equation. + /// + public static PixelBlender <#= blenderComposer #> => PixelBlender>.Instance; +<# + } +} #> } diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/IPixelBlenderOperator.cs b/src/ImageSharp/PixelFormats/PixelBlenders/IPixelBlenderOperator.cs new file mode 100644 index 000000000..4b06e9523 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/IPixelBlenderOperator.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +/// +/// Defines a Porter-Duff equation that can be applied by the shared pixel-blending traversal. +/// +internal interface IPixelBlenderOperator +{ + /// + /// Gets a value indicating whether pixel conversion uses associated-alpha vectors. + /// + public static abstract bool IsAssociatedAlpha { get; } + + /// + /// Blends one pixel represented by four RGBA lanes. + /// + /// The background RGBA lanes. + /// The source RGBA lanes. + /// The source opacity in the range 0 through 1. + /// The blended RGBA lanes. + public static abstract Vector4 Invoke(Vector4 background, Vector4 source, float amount); + + /// + /// Blends two pixels represented by two consecutive groups of four RGBA lanes. + /// + /// The background RGBA lanes. + /// The source RGBA lanes. + /// The source opacity repeated across each pixel's four lanes. + /// The blended RGBA lanes. + public static abstract Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount); + + /// + /// Blends four pixels represented by four consecutive groups of four RGBA lanes. + /// + /// The background RGBA lanes. + /// The source RGBA lanes. + /// The source opacity repeated across each pixel's four lanes. + /// The blended RGBA lanes. + public static abstract Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount); +} diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs b/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs new file mode 100644 index 000000000..ee8c6905f --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs @@ -0,0 +1,586 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; + +/// +/// Applies a statically selected Porter-Duff equation across pixel-vector rows. +/// +/// The destination pixel format. +/// The Porter-Duff equation. +internal sealed class PixelBlender : PixelBlender + where TPixel : unmanaged, IPixel + where TOperator : struct, IPixelBlenderOperator +{ + /// + /// Initializes a new instance of the class. + /// + private PixelBlender() + { + } + + /// + /// Gets the shared blender instance for this exact pixel and equation combination. + /// + // Keeping the singleton on the already-required closed blender type preserves per-mode lazy initialization + // without introducing another NativeAOT holder type or eagerly allocating every blender for a pixel format. + public static PixelBlender Instance { get; } = new PixelBlender(); + + /// + public override TPixel Blend(TPixel background, TPixel source, float amount) + { + // The operator type uniquely determines the alpha representation. NativeAOT therefore needs only the + // pixel/operator closure, while the JIT can remove this static choice from the generated hot path. + Vector4 backgroundVector = TOperator.IsAssociatedAlpha + ? background.ToAssociatedScaledVector4() + : background.ToUnassociatedScaledVector4(); + Vector4 sourceVector = TOperator.IsAssociatedAlpha + ? source.ToAssociatedScaledVector4() + : source.ToUnassociatedScaledVector4(); + + Vector4 result = TOperator.Invoke(backgroundVector, sourceVector, Numerics.Clamp(amount, 0, 1F)); + + return TOperator.IsAssociatedAlpha + ? TPixel.FromAssociatedScaledVector4(result) + : TPixel.FromUnassociatedScaledVector4(result); + } + + /// + protected sealed override void ToBlendVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination) + { + PixelConversionModifiers modifiers = TOperator.IsAssociatedAlpha + ? PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply + : PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply; + + PixelOperations.Instance.ToVector4(configuration, source, destination, modifiers); + } + + /// + protected sealed override Vector4 ToBlendVector4(TPixel source) + => TOperator.IsAssociatedAlpha + ? source.ToAssociatedScaledVector4() + : source.ToUnassociatedScaledVector4(); + + /// + protected sealed override void FromBlendVector4( + Configuration configuration, + Span source, + Span destination) + { + PixelConversionModifiers modifiers = TOperator.IsAssociatedAlpha + ? PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply + : PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply; + + PixelOperations.Instance.FromVector4Destructive(configuration, source, destination, modifiers); + } + + /// + protected sealed override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount) + { + // Public entry points validate the row lengths, so all three references can advance in lockstep. + int scalarStart = 0; + amount = Numerics.Clamp(amount, 0, 1F); + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + // A 512-bit register holds four complete RGBA pixels in [R,G,B,A] groups. + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 4; + Vector512 amountVector = Vector512.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + // A 256-bit register holds two complete RGBA pixels in [R,G,B,A] groups. + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 2; + Vector256 amountVector = Vector256.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); + } + + scalarStart = vectorCount * 2; + } + + // Vector4 is both the scalar pixel representation and the portable SIMD fallback. + for (int i = scalarStart; i < destination.Length; i++) + { + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke(Unsafe.Add(ref backgroundRef, (uint)i), Unsafe.Add(ref sourceRef, (uint)i), amount); + } + } + + /// + protected sealed override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount) + { + // Public entry points validate the row lengths, so the destination and background advance together. + int scalarStart = 0; + amount = Numerics.Clamp(amount, 0, 1F); + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + // Repeat one [R,G,B,A] source group four times to match the four background pixels. + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 4; + Vector512 sourceVector = CreateVector512(source); + Vector512 amountVector = Vector512.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + // Repeat one [R,G,B,A] source group twice to match the two background pixels. + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 2; + Vector256 sourceVector = CreateVector256(source); + Vector256 amountVector = Vector256.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); + } + + scalarStart = vectorCount * 2; + } + + // The remaining pixel count is at most three after AVX-512 or one after AVX2. + for (int i = scalarStart; i < destination.Length; i++) + { + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke(Unsafe.Add(ref backgroundRef, (uint)i), source, amount); + } + } + + /// + protected sealed override void BlendFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount) + { + // Each amount belongs to one pixel and must be repeated across that pixel's four RGBA lanes. + int scalarStart = 0; + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); + ref float amountRef = ref MemoryMarshal.GetReference(amount); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 4; + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + // Four scalar amounts become four contiguous [a,a,a,a] groups before clamping. + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); + Vector512 amountVector = CreateClampedVector512(ref amountBase); + + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 2; + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + // Two scalar amounts become two contiguous [a,a,a,a] groups before clamping. + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); + Vector256 amountVector = CreateClampedVector256(ref amountBase); + + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), Unsafe.Add(ref sourceBase, i), amountVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke(Unsafe.Add(ref backgroundRef, (uint)i), Unsafe.Add(ref sourceRef, (uint)i), Numerics.Clamp(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); + } + } + + /// + protected sealed override void BlendFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount) + { + // The source is invariant, while each background pixel has its own independently clamped amount. + int scalarStart = 0; + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref float amountRef = ref MemoryMarshal.GetReference(amount); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 4; + Vector512 sourceVector = CreateVector512(source); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); + Vector512 amountVector = CreateClampedVector512(ref amountBase); + + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 2; + Vector256 sourceVector = CreateVector256(source); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); + Vector256 amountVector = CreateClampedVector256(ref amountBase); + + Unsafe.Add(ref destinationBase, i) = TOperator.Invoke(Unsafe.Add(ref backgroundBase, i), sourceVector, amountVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Unsafe.Add(ref destinationRef, (uint)i) = TOperator.Invoke(Unsafe.Add(ref backgroundRef, (uint)i), source, Numerics.Clamp(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); + } + } + + /// + protected sealed override void BlendWithCoverageFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, float amount, ReadOnlySpan coverage) + { + // Coverage mixes the composed result back toward the original background, so it is fused into this pass. + int scalarStart = 0; + amount = Numerics.Clamp(amount, 0, 1F); + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); + ref float coverageRef = ref MemoryMarshal.GetReference(coverage); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 4; + Vector512 amountVector = Vector512.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); + Vector512 coverageVector = CreateClampedVector512(ref coverageBase); + Vector512 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); + + Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 2; + Vector256 amountVector = Vector256.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); + Vector256 coverageVector = CreateClampedVector256(ref coverageBase); + Vector256 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); + + Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); + Vector4 blended = TOperator.Invoke(backgroundPixel, Unsafe.Add(ref sourceRef, (uint)i), amount); + + Unsafe.Add(ref destinationRef, (uint)i) = PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F)); + } + } + + /// + protected sealed override void BlendWithCoverageFunction(Span destination, ReadOnlySpan background, Vector4 source, float amount, ReadOnlySpan coverage) + { + // The constant source is expanded once per selected width and reused for the complete row. + int scalarStart = 0; + amount = Numerics.Clamp(amount, 0, 1F); + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref float coverageRef = ref MemoryMarshal.GetReference(coverage); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 4; + Vector512 sourceVector = CreateVector512(source); + Vector512 amountVector = Vector512.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); + Vector512 coverageVector = CreateClampedVector512(ref coverageBase); + Vector512 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); + + Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 2; + Vector256 sourceVector = CreateVector256(source); + Vector256 amountVector = Vector256.Create(amount); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); + Vector256 coverageVector = CreateClampedVector256(ref coverageBase); + Vector256 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); + + Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); + Vector4 blended = TOperator.Invoke(backgroundPixel, source, amount); + + Unsafe.Add(ref destinationRef, (uint)i) = PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F)); + } + } + + /// + protected sealed override void BlendWithCoverageFunction(Span destination, ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount, ReadOnlySpan coverage) + { + // Amount controls composition while coverage controls the final mix with the untouched background. + int scalarStart = 0; + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(source); + ref float amountRef = ref MemoryMarshal.GetReference(amount); + ref float coverageRef = ref MemoryMarshal.GetReference(coverage); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector512 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 4; + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); + Vector512 amountVector = CreateClampedVector512(ref amountBase); + Vector512 coverageVector = CreateClampedVector512(ref coverageBase); + Vector512 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); + + Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + ref Vector256 sourceBase = ref Unsafe.As>(ref sourceRef); + int vectorCount = destination.Length / 2; + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); + Vector256 amountVector = CreateClampedVector256(ref amountBase); + Vector256 coverageVector = CreateClampedVector256(ref coverageBase); + Vector256 blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector); + + Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); + Vector4 blended = TOperator.Invoke(backgroundPixel, Unsafe.Add(ref sourceRef, (uint)i), Numerics.Clamp(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); + + Unsafe.Add(ref destinationRef, (uint)i) = PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F)); + } + } + + /// + protected sealed override void BlendWithCoverageFunction(Span destination, ReadOnlySpan background, Vector4 source, ReadOnlySpan amount, ReadOnlySpan coverage) + { + // The invariant source is expanded once; only amount and coverage are gathered for each vector batch. + int scalarStart = 0; + + ref Vector4 destinationRef = ref MemoryMarshal.GetReference(destination); + ref Vector4 backgroundRef = ref MemoryMarshal.GetReference(background); + ref float amountRef = ref MemoryMarshal.GetReference(amount); + ref float coverageRef = ref MemoryMarshal.GetReference(coverage); + + if (Vector512.IsHardwareAccelerated && destination.Length >= 4) + { + ref Vector512 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector512 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 4; + Vector512 sourceVector = CreateVector512(source); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector512 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 4); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 4); + Vector512 amountVector = CreateClampedVector512(ref amountBase); + Vector512 coverageVector = CreateClampedVector512(ref coverageBase); + Vector512 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); + + Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 4; + } + else if (Vector256.IsHardwareAccelerated && destination.Length >= 2) + { + ref Vector256 destinationBase = ref Unsafe.As>(ref destinationRef); + ref Vector256 backgroundBase = ref Unsafe.As>(ref backgroundRef); + int vectorCount = destination.Length / 2; + Vector256 sourceVector = CreateVector256(source); + + for (nuint i = 0; i < (uint)vectorCount; i++) + { + ref Vector256 backgroundVector = ref Unsafe.Add(ref backgroundBase, i); + ref float amountBase = ref Unsafe.Add(ref amountRef, i * 2); + ref float coverageBase = ref Unsafe.Add(ref coverageRef, i * 2); + Vector256 amountVector = CreateClampedVector256(ref amountBase); + Vector256 coverageVector = CreateClampedVector256(ref coverageBase); + Vector256 blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector); + + Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector); + } + + scalarStart = vectorCount * 2; + } + + for (int i = scalarStart; i < destination.Length; i++) + { + Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i); + Vector4 blended = TOperator.Invoke(backgroundPixel, source, Numerics.Clamp(Unsafe.Add(ref amountRef, (uint)i), 0, 1F)); + + Unsafe.Add(ref destinationRef, (uint)i) = PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F)); + } + } + + /// + /// Repeats one RGBA pixel twice to fill a 256-bit register. + /// + /// The pixel represented by ordered RGBA lanes. + /// Two consecutive copies of the pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CreateVector256(Vector4 pixel) + => Vector256.Create(pixel.X, pixel.Y, pixel.Z, pixel.W, pixel.X, pixel.Y, pixel.Z, pixel.W); + + /// + /// Repeats one RGBA pixel four times to fill a 512-bit register. + /// + /// The pixel represented by ordered RGBA lanes. + /// Four consecutive copies of the pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 CreateVector512(Vector4 pixel) + => Vector512.Create(pixel.X, pixel.Y, pixel.Z, pixel.W, pixel.X, pixel.Y, pixel.Z, pixel.W, pixel.X, pixel.Y, pixel.Z, pixel.W, pixel.X, pixel.Y, pixel.Z, pixel.W); + + /// + /// Expands and clamps two per-pixel scalar values for two packed RGBA pixels. + /// + /// The first of two contiguous scalar values. + /// Two consecutive groups containing one clamped value in each group's four lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CreateClampedVector256(ref float values) + { + Vector256 result = Vector256.Create(Vector128.Create(values), Vector128.Create(Unsafe.Add(ref values, 1))); + + // Amount and coverage share the same public 0..1 contract and therefore the same packed clamp. + return Vector256.Min(Vector256.Max(Vector256.Zero, result), Vector256.Create(1F)); + } + + /// + /// Expands and clamps four per-pixel scalar values for four packed RGBA pixels. + /// + /// The first of four contiguous scalar values. + /// Four consecutive groups containing one clamped value in each group's four lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 CreateClampedVector512(ref float values) + { + Vector512 result = Vector512.Create(values, values, values, values, Unsafe.Add(ref values, 1), Unsafe.Add(ref values, 1), Unsafe.Add(ref values, 1), Unsafe.Add(ref values, 1), Unsafe.Add(ref values, 2), Unsafe.Add(ref values, 2), Unsafe.Add(ref values, 2), Unsafe.Add(ref values, 2), Unsafe.Add(ref values, 3), Unsafe.Add(ref values, 3), Unsafe.Add(ref values, 3), Unsafe.Add(ref values, 3)); + + // Amount and coverage share the same public 0..1 contract and therefore the same packed clamp. + return Vector512.Min(Vector512.Max(Vector512.Zero, result), Vector512.Create(1F)); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs b/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs index 255bafc79..262370cd5 100644 --- a/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs @@ -37,7 +37,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "NormalSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "NormalSrcAtop" compositing equation. @@ -64,7 +75,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Normal(backdrop, source)); + } + + /// + /// Returns the result of the "NormalSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, Normal(backdrop, source)); } @@ -94,7 +120,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Normal(backdrop, source)); + } + + /// + /// Returns the result of the "NormalSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, Normal(backdrop, source)); } @@ -123,7 +164,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "NormalSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "NormalSrcOut" compositing equation. @@ -149,7 +201,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "NormalSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "NormalDest" compositing equation. @@ -177,6 +240,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "NormalDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "NormalDestAtop" compositing equation. /// @@ -202,7 +278,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Normal(source, backdrop)); + } + + /// + /// Returns the result of the "NormalDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, Normal(source, backdrop)); } @@ -232,7 +323,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Normal(source, backdrop)); + } + + /// + /// Returns the result of the "NormalDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, Normal(source, backdrop)); } @@ -261,7 +367,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "NormalDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "NormalDestOut" compositing equation. @@ -287,7 +404,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "NormalDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "NormalXor" compositing equation. @@ -313,7 +441,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "NormalXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "NormalClear" compositing equation. @@ -339,7 +478,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 NormalClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "NormalClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 NormalClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -355,7 +505,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalSrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -371,7 +521,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalSrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -387,7 +537,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalSrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -403,7 +553,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalSrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -419,7 +569,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalSrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -435,7 +585,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -451,7 +601,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -467,7 +617,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -483,7 +633,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -499,7 +649,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -515,7 +665,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -531,7 +681,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(NormalXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(NormalXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -558,7 +708,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplySrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "MultiplySrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "MultiplySrcAtop" compositing equation. @@ -585,7 +746,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplySrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the result of the "MultiplySrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, Multiply(backdrop, source)); } @@ -615,7 +791,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplySrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Multiply(backdrop, source)); + } + + /// + /// Returns the result of the "MultiplySrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, Multiply(backdrop, source)); } @@ -644,7 +835,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplySrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "MultiplySrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "MultiplySrcOut" compositing equation. @@ -670,7 +872,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplySrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "MultiplySrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplySrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "MultiplyDest" compositing equation. @@ -698,6 +911,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "MultiplyDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "MultiplyDestAtop" compositing equation. /// @@ -723,7 +949,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplyDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the result of the "MultiplyDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, Multiply(source, backdrop)); } @@ -753,7 +994,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplyDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Multiply(source, backdrop)); + } + + /// + /// Returns the result of the "MultiplyDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, Multiply(source, backdrop)); } @@ -782,7 +1038,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplyDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "MultiplyDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "MultiplyDestOut" compositing equation. @@ -808,7 +1075,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplyDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "MultiplyDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "MultiplyXor" compositing equation. @@ -834,7 +1112,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplyXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "MultiplyXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "MultiplyClear" compositing equation. @@ -860,7 +1149,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 MultiplyClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "MultiplyClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -876,7 +1176,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplySrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplySrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -892,7 +1192,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplySrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplySrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -908,7 +1208,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplySrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplySrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -924,7 +1224,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplySrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplySrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -940,7 +1240,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplySrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplySrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -956,7 +1256,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplyDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplyDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -972,7 +1272,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplyDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplyDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -988,7 +1288,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplyDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplyDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1004,7 +1304,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplyDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplyDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1020,7 +1320,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplyDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplyDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1036,7 +1336,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplyClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplyClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1052,7 +1352,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(MultiplyXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(MultiplyXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1079,7 +1379,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "AddSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "AddSrcAtop" compositing equation. @@ -1106,7 +1417,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the result of the "AddSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, Add(backdrop, source)); } @@ -1136,24 +1462,39 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); return Over(backdrop, source, Add(backdrop, source)); } /// - /// Returns the result of the "AddSrcIn" compositing equation. + /// Returns the result of the "AddSrcOver" compositing equation. /// /// The backdrop vector. /// The source vector. /// The source opacity. Range 0..1 - /// The . + /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector4 AddSrcIn(Vector4 backdrop, Vector4 source, float opacity) + public static Vector512 AddSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) { - source = Numerics.WithW(source, source * opacity); + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); - return In(backdrop, source); + return Over(backdrop, source, Add(backdrop, source)); + } + + /// + /// Returns the result of the "AddSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 AddSrcIn(Vector4 backdrop, Vector4 source, float opacity) + { + source = Numerics.WithW(source, source * opacity); + + return In(backdrop, source); } /// @@ -1165,7 +1506,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "AddSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "AddSrcOut" compositing equation. @@ -1191,7 +1543,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "AddSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "AddDest" compositing equation. @@ -1219,6 +1582,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "AddDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "AddDestAtop" compositing equation. /// @@ -1244,7 +1620,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the result of the "AddDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, Add(source, backdrop)); } @@ -1274,7 +1665,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Add(source, backdrop)); + } + + /// + /// Returns the result of the "AddDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, Add(source, backdrop)); } @@ -1303,7 +1709,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "AddDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "AddDestOut" compositing equation. @@ -1329,7 +1746,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "AddDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "AddXor" compositing equation. @@ -1355,7 +1783,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "AddXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "AddClear" compositing equation. @@ -1381,7 +1820,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 AddClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "AddClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AddClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -1397,7 +1847,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddSrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1413,7 +1863,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddSrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1429,7 +1879,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddSrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1445,7 +1895,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddSrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1461,7 +1911,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddSrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1477,7 +1927,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1493,7 +1943,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1509,7 +1959,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1525,7 +1975,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1541,7 +1991,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1557,7 +2007,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1573,7 +2023,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(AddXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(AddXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1600,7 +2050,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "SubtractSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "SubtractSrcAtop" compositing equation. @@ -1627,7 +2088,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the result of the "SubtractSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, Subtract(backdrop, source)); } @@ -1657,7 +2133,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Subtract(backdrop, source)); + } + + /// + /// Returns the result of the "SubtractSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, Subtract(backdrop, source)); } @@ -1686,7 +2177,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "SubtractSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "SubtractSrcOut" compositing equation. @@ -1712,7 +2214,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "SubtractSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "SubtractDest" compositing equation. @@ -1740,6 +2253,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "SubtractDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "SubtractDestAtop" compositing equation. /// @@ -1765,7 +2291,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the result of the "SubtractDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, Subtract(source, backdrop)); } @@ -1795,7 +2336,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Subtract(source, backdrop)); + } + + /// + /// Returns the result of the "SubtractDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, Subtract(source, backdrop)); } @@ -1824,7 +2380,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "SubtractDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "SubtractDestOut" compositing equation. @@ -1850,7 +2417,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "SubtractDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "SubtractXor" compositing equation. @@ -1876,7 +2454,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "SubtractXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "SubtractClear" compositing equation. @@ -1902,7 +2491,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 SubtractClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "SubtractClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 SubtractClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -1918,7 +2518,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractSrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1934,7 +2534,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractSrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1950,7 +2550,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractSrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1966,7 +2566,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractSrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1982,7 +2582,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractSrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -1998,7 +2598,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2014,7 +2614,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2030,7 +2630,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2046,7 +2646,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2062,7 +2662,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2078,7 +2678,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2094,7 +2694,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(SubtractXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(SubtractXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2121,7 +2721,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "ScreenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "ScreenSrcAtop" compositing equation. @@ -2148,7 +2759,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the result of the "ScreenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, Screen(backdrop, source)); } @@ -2178,7 +2804,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Screen(backdrop, source)); + } + + /// + /// Returns the result of the "ScreenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, Screen(backdrop, source)); } @@ -2207,7 +2848,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "ScreenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "ScreenSrcOut" compositing equation. @@ -2233,7 +2885,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "ScreenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "ScreenDest" compositing equation. @@ -2261,6 +2924,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "ScreenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "ScreenDestAtop" compositing equation. /// @@ -2271,7 +2947,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector4 ScreenDestAtop(Vector4 backdrop, Vector4 source, float opacity) { - source = Numerics.WithW(source, source * opacity); + source = Numerics.WithW(source, source * opacity); + + return Atop(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the result of the "ScreenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ScreenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + { + source = Avx.Blend(source, source * opacity, BlendAlphaControl); return Atop(source, backdrop, Screen(source, backdrop)); } @@ -2282,11 +2973,11 @@ internal static partial class PorterDuffFunctions /// The backdrop vector. /// The source vector. /// The source opacity. Range 0..1 - /// The . + /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 ScreenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) + public static Vector512 ScreenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, Screen(source, backdrop)); } @@ -2316,7 +3007,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Screen(source, backdrop)); + } + + /// + /// Returns the result of the "ScreenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, Screen(source, backdrop)); } @@ -2345,7 +3051,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "ScreenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "ScreenDestOut" compositing equation. @@ -2371,7 +3088,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "ScreenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "ScreenXor" compositing equation. @@ -2397,7 +3125,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "ScreenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "ScreenClear" compositing equation. @@ -2423,7 +3162,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 ScreenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "ScreenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ScreenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -2439,7 +3189,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenSrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2455,7 +3205,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenSrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2471,7 +3221,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenSrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2487,7 +3237,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenSrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2503,7 +3253,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenSrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2519,7 +3269,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2535,7 +3285,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2551,7 +3301,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2567,7 +3317,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2583,7 +3333,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2599,7 +3349,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2615,7 +3365,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(ScreenXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(ScreenXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2642,7 +3392,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "DarkenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "DarkenSrcAtop" compositing equation. @@ -2669,7 +3430,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the result of the "DarkenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, Darken(backdrop, source)); } @@ -2699,7 +3475,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Darken(backdrop, source)); + } + + /// + /// Returns the result of the "DarkenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, Darken(backdrop, source)); } @@ -2728,7 +3519,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "DarkenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "DarkenSrcOut" compositing equation. @@ -2754,7 +3556,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "DarkenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "DarkenDest" compositing equation. @@ -2782,6 +3595,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "DarkenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "DarkenDestAtop" compositing equation. /// @@ -2807,7 +3633,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the result of the "DarkenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, Darken(source, backdrop)); } @@ -2837,7 +3678,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Darken(source, backdrop)); + } + + /// + /// Returns the result of the "DarkenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, Darken(source, backdrop)); } @@ -2866,7 +3722,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "DarkenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "DarkenDestOut" compositing equation. @@ -2892,7 +3759,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "DarkenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "DarkenXor" compositing equation. @@ -2918,7 +3796,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "DarkenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "DarkenClear" compositing equation. @@ -2944,7 +3833,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 DarkenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "DarkenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DarkenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -2960,7 +3860,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenSrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2976,7 +3876,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenSrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -2992,7 +3892,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenSrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3008,7 +3908,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenSrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3024,7 +3924,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenSrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3040,7 +3940,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3056,7 +3956,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3072,7 +3972,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3088,7 +3988,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3104,7 +4004,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3120,7 +4020,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3136,7 +4036,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(DarkenXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(DarkenXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3163,7 +4063,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "LightenSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "LightenSrcAtop" compositing equation. @@ -3190,7 +4101,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the result of the "LightenSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, Lighten(backdrop, source)); } @@ -3220,7 +4146,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Lighten(backdrop, source)); + } + + /// + /// Returns the result of the "LightenSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, Lighten(backdrop, source)); } @@ -3249,7 +4190,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "LightenSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "LightenSrcOut" compositing equation. @@ -3275,7 +4227,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "LightenSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "LightenDest" compositing equation. @@ -3303,6 +4266,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "LightenDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "LightenDestAtop" compositing equation. /// @@ -3328,7 +4304,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the result of the "LightenDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, Lighten(source, backdrop)); } @@ -3358,7 +4349,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Lighten(source, backdrop)); + } + + /// + /// Returns the result of the "LightenDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, Lighten(source, backdrop)); } @@ -3384,10 +4390,21 @@ internal static partial class PorterDuffFunctions /// The backdrop vector. /// The source vector. /// The source opacity. Range 0..1 - /// The . + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LightenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "LightenDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 LightenDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + public static Vector512 LightenDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "LightenDestOut" compositing equation. @@ -3413,7 +4430,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "LightenDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "LightenXor" compositing equation. @@ -3439,7 +4467,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "LightenXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "LightenClear" compositing equation. @@ -3465,7 +4504,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 LightenClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "LightenClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 LightenClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -3481,7 +4531,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenSrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3497,7 +4547,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenSrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3513,7 +4563,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenSrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3529,7 +4579,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenSrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3545,7 +4595,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenSrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3561,7 +4611,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3577,7 +4627,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3593,7 +4643,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3609,7 +4659,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3625,7 +4675,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3641,7 +4691,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3657,7 +4707,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(LightenXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(LightenXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -3684,7 +4734,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlaySrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "OverlaySrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "OverlaySrcAtop" compositing equation. @@ -3711,7 +4772,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlaySrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the result of the "OverlaySrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, Overlay(backdrop, source)); } @@ -3741,7 +4817,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlaySrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, Overlay(backdrop, source)); + } + + /// + /// Returns the result of the "OverlaySrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, Overlay(backdrop, source)); } @@ -3770,7 +4861,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlaySrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "OverlaySrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "OverlaySrcOut" compositing equation. @@ -3796,7 +4898,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlaySrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "OverlaySrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlaySrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "OverlayDest" compositing equation. @@ -3824,6 +4937,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "OverlayDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "OverlayDestAtop" compositing equation. /// @@ -3849,7 +4975,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlayDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the result of the "OverlayDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, Overlay(source, backdrop)); } @@ -3879,7 +5020,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlayDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, Overlay(source, backdrop)); + } + + /// + /// Returns the result of the "OverlayDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, Overlay(source, backdrop)); } @@ -3908,7 +5064,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlayDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "OverlayDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "OverlayDestOut" compositing equation. @@ -3934,7 +5101,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlayDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "OverlayDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "OverlayXor" compositing equation. @@ -3960,7 +5138,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlayXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "OverlayXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "OverlayClear" compositing equation. @@ -3986,7 +5175,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 OverlayClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "OverlayClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -4002,7 +5202,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlaySrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlaySrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4018,7 +5218,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlaySrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlaySrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4034,7 +5234,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlaySrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlaySrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4050,7 +5250,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlaySrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlaySrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4066,7 +5266,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlaySrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlaySrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4082,7 +5282,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlayDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlayDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4098,7 +5298,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlayDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlayDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4114,7 +5314,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlayDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlayDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4130,7 +5330,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlayDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlayDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4146,7 +5346,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlayDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlayDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4162,7 +5362,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlayClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlayClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4178,7 +5378,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(OverlayXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(OverlayXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4205,7 +5405,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightSrc(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "HardLightSrc compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrc(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "HardLightSrcAtop" compositing equation. @@ -4232,7 +5443,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightSrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the result of the "HardLightSrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, HardLight(backdrop, source)); } @@ -4262,7 +5488,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightSrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, HardLight(backdrop, source)); + } + + /// + /// Returns the result of the "HardLightSrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, HardLight(backdrop, source)); } @@ -4291,7 +5532,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightSrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "HardLightSrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "HardLightSrcOut" compositing equation. @@ -4317,7 +5569,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightSrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "HardLightSrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightSrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "HardLightDest" compositing equation. @@ -4345,6 +5608,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "HardLightDest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "HardLightDestAtop" compositing equation. /// @@ -4370,7 +5646,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightDestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the result of the "HardLightDestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, HardLight(source, backdrop)); } @@ -4400,7 +5691,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightDestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, HardLight(source, backdrop)); + } + + /// + /// Returns the result of the "HardLightDestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, HardLight(source, backdrop)); } @@ -4429,7 +5735,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightDestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "HardLightDestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "HardLightDestOut" compositing equation. @@ -4455,7 +5772,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightDestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "HardLightDestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightDestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "HardLightXor" compositing equation. @@ -4481,7 +5809,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightXor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "HardLightXor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightXor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "HardLightClear" compositing equation. @@ -4507,7 +5846,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 HardLightClear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "HardLightClear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLightClear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// @@ -4523,7 +5873,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightSrc(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightSrc(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4539,7 +5889,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightSrcAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightSrcAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4555,7 +5905,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightSrcOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightSrcOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4571,7 +5921,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightSrcIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightSrcIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4587,7 +5937,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightSrcOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightSrcOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4603,7 +5953,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightDest(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightDest(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4619,7 +5969,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightDestAtop(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightDestAtop(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4635,7 +5985,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightDestOver(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightDestOver(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4651,7 +6001,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightDestIn(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightDestIn(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4667,7 +6017,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightDestOut(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightDestOut(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4683,7 +6033,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightClear(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightClear(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } /// @@ -4699,6 +6049,6 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(HardLightXor(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(HardLightXor(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } } diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.tt b/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.tt index 150adb33a..8837a90a3 100644 --- a/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.tt +++ b/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.tt @@ -47,7 +47,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>Src(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + => Avx.Blend(source, source * opacity, BlendAlphaControl); + + /// + /// Returns the result of the "<#=blender#>Src compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>Src(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); /// /// Returns the result of the "<#=blender#>SrcAtop" compositing equation. @@ -74,7 +85,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>SrcAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(backdrop, source, <#=blender#>(backdrop, source)); + } + + /// + /// Returns the result of the "<#=blender#>SrcAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>SrcAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(backdrop, source, <#=blender#>(backdrop, source)); } @@ -104,7 +130,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>SrcOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(backdrop, source, <#=blender#>(backdrop, source)); + } + + /// + /// Returns the result of the "<#=blender#>SrcOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>SrcOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(backdrop, source, <#=blender#>(backdrop, source)); } @@ -133,7 +174,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>SrcIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => In(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "<#=blender#>SrcIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>SrcIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "<#=blender#>SrcOut" compositing equation. @@ -159,7 +211,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>SrcOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Out(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "<#=blender#>SrcOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>SrcOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "<#=blender#>Dest" compositing equation. @@ -187,6 +250,19 @@ internal static partial class PorterDuffFunctions return backdrop; } + /// + /// Returns the result of the "<#=blender#>Dest" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>Dest(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + return backdrop; + } + /// /// Returns the result of the "<#=blender#>DestAtop" compositing equation. /// @@ -212,7 +288,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>DestAtop(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Atop(source, backdrop, <#=blender#>(source, backdrop)); + } + + /// + /// Returns the result of the "<#=blender#>DestAtop" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>DestAtop(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Atop(source, backdrop, <#=blender#>(source, backdrop)); } @@ -242,7 +333,22 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>DestOver(Vector256 backdrop, Vector256 source, Vector256 opacity) { - source = Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl); + source = Avx.Blend(source, source * opacity, BlendAlphaControl); + + return Over(source, backdrop, <#=blender#>(source, backdrop)); + } + + /// + /// Returns the result of the "<#=blender#>DestOver" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>DestOver(Vector512 backdrop, Vector512 source, Vector512 opacity) + { + source = Avx512F.BlendVariable(source, source * opacity, AlphaMask512()); return Over(source, backdrop, <#=blender#>(source, backdrop)); } @@ -271,7 +377,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>DestIn(Vector256 backdrop, Vector256 source, Vector256 opacity) - => In(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => In(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "<#=blender#>DestIn" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>DestIn(Vector512 backdrop, Vector512 source, Vector512 opacity) + => In(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "<#=blender#>DestOut" compositing equation. @@ -297,7 +414,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>DestOut(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Out(Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl), backdrop); + => Out(Avx.Blend(source, source * opacity, BlendAlphaControl), backdrop); + + /// + /// Returns the result of the "<#=blender#>DestOut" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>DestOut(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Out(Avx512F.BlendVariable(source, source * opacity, AlphaMask512()), backdrop); /// /// Returns the result of the "<#=blender#>Xor" compositing equation. @@ -323,7 +451,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>Xor(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Xor(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Xor(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "<#=blender#>Xor" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>Xor(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Xor(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); /// /// Returns the result of the "<#=blender#>Clear" compositing equation. @@ -349,7 +488,18 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 <#=blender#>Clear(Vector256 backdrop, Vector256 source, Vector256 opacity) - => Clear(backdrop, Avx.Blend(source, Avx.Multiply(source, opacity), BlendAlphaControl)); + => Clear(backdrop, Avx.Blend(source, source * opacity, BlendAlphaControl)); + + /// + /// Returns the result of the "<#=blender#>Clear" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The source opacity. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 <#=blender#>Clear(Vector512 backdrop, Vector512 source, Vector512 opacity) + => Clear(backdrop, Avx512F.BlendVariable(source, source * opacity, AlphaMask512())); <#} #> @@ -368,7 +518,7 @@ internal static partial class PorterDuffFunctions where TPixel : unmanaged, IPixel { opacity = Numerics.Clamp(opacity, 0, 1); - return TPixel.FromScaledVector4(<#=blender#><#=composer#>(backdrop.ToScaledVector4(), source.ToScaledVector4(), opacity)); + return TPixel.FromUnassociatedScaledVector4(<#=blender#><#=composer#>(backdrop.ToUnassociatedScaledVector4(), source.ToUnassociatedScaledVector4(), opacity)); } <# } #> <# diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.cs b/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.cs index ca358be31..46d97ad3d 100644 --- a/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.cs +++ b/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders; @@ -44,6 +45,16 @@ internal static partial class PorterDuffFunctions public static Vector256 Normal(Vector256 backdrop, Vector256 source) => source; + /// + /// Returns the result of the "Normal" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Normal(Vector512 backdrop, Vector512 source) + => source; + /// /// Returns the result of the "Multiply" compositing equation. /// @@ -62,7 +73,17 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Multiply(Vector256 backdrop, Vector256 source) - => Avx.Multiply(backdrop, source); + => backdrop * source; + + /// + /// Returns the result of the "Multiply" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Multiply(Vector512 backdrop, Vector512 source) + => backdrop * source; /// /// Returns the result of the "Add" compositing equation. @@ -82,7 +103,17 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Add(Vector256 backdrop, Vector256 source) - => Avx.Min(Vector256.Create(1F), Avx.Add(backdrop, source)); + => Vector256.Min(Vector256.Create(1F), backdrop + source); + + /// + /// Returns the result of the "Add" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Add(Vector512 backdrop, Vector512 source) + => Vector512.Min(Vector512.Create(1F), backdrop + source); /// /// Returns the result of the "Subtract" compositing equation. @@ -102,7 +133,17 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Subtract(Vector256 backdrop, Vector256 source) - => Avx.Max(Vector256.Zero, Avx.Subtract(backdrop, source)); + => Vector256.Max(Vector256.Zero, backdrop - source); + + /// + /// Returns the result of the "Subtract" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Subtract(Vector512 backdrop, Vector512 source) + => Vector512.Max(Vector512.Zero, backdrop - source); /// /// Returns the result of the "Screen" compositing equation. @@ -124,7 +165,20 @@ internal static partial class PorterDuffFunctions public static Vector256 Screen(Vector256 backdrop, Vector256 source) { Vector256 vOne = Vector256.Create(1F); - return SimdUtils.HwIntrinsics.MultiplyAddNegated(Avx.Subtract(vOne, backdrop), Avx.Subtract(vOne, source), vOne); + return Vector256_.MultiplyAddNegated(vOne, vOne - backdrop, vOne - source); + } + + /// + /// Returns the result of the "Screen" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Screen(Vector512 backdrop, Vector512 source) + { + Vector512 vOne = Vector512.Create(1F); + return Vector512_.MultiplyAddNegated(vOne, vOne - backdrop, vOne - source); } /// @@ -145,7 +199,17 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Darken(Vector256 backdrop, Vector256 source) - => Avx.Min(backdrop, source); + => Vector256.Min(backdrop, source); + + /// + /// Returns the result of the "Darken" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Darken(Vector512 backdrop, Vector512 source) + => Vector512.Min(backdrop, source); /// /// Returns the result of the "Lighten" compositing equation. @@ -164,7 +228,17 @@ internal static partial class PorterDuffFunctions /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Lighten(Vector256 backdrop, Vector256 source) - => Avx.Max(backdrop, source); + => Vector256.Max(backdrop, source); + + /// + /// Returns the result of the "Lighten" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Lighten(Vector512 backdrop, Vector512 source) + => Vector512.Max(backdrop, source); /// /// Returns the result of the "Overlay" compositing equation. @@ -192,7 +266,20 @@ internal static partial class PorterDuffFunctions public static Vector256 Overlay(Vector256 backdrop, Vector256 source) { Vector256 color = OverlayValueFunction(backdrop, source); - return Avx.Min(Vector256.Create(1F), Avx.Blend(color, Vector256.Zero, BlendAlphaControl)); + return Vector256.Min(Vector256.Create(1F), Avx.Blend(color, Vector256.Zero, BlendAlphaControl)); + } + + /// + /// Returns the result of the "Overlay" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Overlay(Vector512 backdrop, Vector512 source) + { + Vector512 color = OverlayValueFunction(backdrop, source); + return Vector512.Min(Vector512.Create(1F), Vector512.ConditionalSelect(AlphaMask512(), Vector512.Zero, color)); } /// @@ -221,7 +308,81 @@ internal static partial class PorterDuffFunctions public static Vector256 HardLight(Vector256 backdrop, Vector256 source) { Vector256 color = OverlayValueFunction(source, backdrop); - return Avx.Min(Vector256.Create(1F), Avx.Blend(color, Vector256.Zero, BlendAlphaControl)); + return Vector256.Min(Vector256.Create(1F), Avx.Blend(color, Vector256.Zero, BlendAlphaControl)); + } + + /// + /// Returns the result of the "HardLight" compositing equation. + /// + /// The backdrop vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HardLight(Vector512 backdrop, Vector512 source) + { + Vector512 color = OverlayValueFunction(source, backdrop); + return Vector512.Min(Vector512.Create(1F), Vector512.ConditionalSelect(AlphaMask512(), Vector512.Zero, color)); + } + + /// + /// Applies raster coverage to a Porter-Duff composition result. + /// + /// The backdrop vector. + /// The Porter-Duff composition result. + /// The coverage. Range 0..1. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 BlendWithCoverage(Vector4 backdrop, Vector4 source, float coverage) + { + Vector4 backdropAlpha = Numerics.PermuteW(backdrop); + Vector4 sourceAlpha = Numerics.PermuteW(source); + Vector4 backdropPremultiplied = Numerics.WithW(backdrop * backdropAlpha, backdropAlpha); + Vector4 sourcePremultiplied = Numerics.WithW(source * sourceAlpha, sourceAlpha); + + // Use the same fused operation as the wider paths so exact midpoints cannot change across vector widths. + Vector4 result = Vector128_.MultiplyAddEstimate((sourcePremultiplied - backdropPremultiplied).AsVector128(), Vector128.Create(coverage), backdropPremultiplied.AsVector128()).AsVector4(); + + Numerics.UnPremultiply(ref result); + return result; + } + + /// + /// Applies raster coverage to a Porter-Duff composition result. + /// + /// The backdrop vector. + /// The Porter-Duff composition result. + /// The coverage. Range 0..1. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 BlendWithCoverage(Vector256 backdrop, Vector256 source, Vector256 coverage) + { + Vector256 backdropAlpha = Avx.Permute(backdrop, ShuffleAlphaControl); + Vector256 sourceAlpha = Avx.Permute(source, ShuffleAlphaControl); + Vector256 backdropPremultiplied = Avx.Blend(backdrop * backdropAlpha, backdropAlpha, BlendAlphaControl); + Vector256 sourcePremultiplied = Avx.Blend(source * sourceAlpha, sourceAlpha, BlendAlphaControl); + Vector256 result = Vector256_.MultiplyAddEstimate(sourcePremultiplied - backdropPremultiplied, coverage, backdropPremultiplied); + + return Numerics.UnPremultiply(result, Avx.Permute(result, ShuffleAlphaControl)); + } + + /// + /// Applies raster coverage to a Porter-Duff composition result. + /// + /// The backdrop vector. + /// The Porter-Duff composition result. + /// The coverage. Range 0..1. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 BlendWithCoverage(Vector512 backdrop, Vector512 source, Vector512 coverage) + { + Vector512 backdropAlpha = Vector512_.ShuffleNative(backdrop, ShuffleAlphaControl); + Vector512 sourceAlpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 alphaMask = AlphaMask512(); + Vector512 backdropPremultiplied = Vector512.ConditionalSelect(alphaMask, backdropAlpha, backdrop * backdropAlpha); + Vector512 sourcePremultiplied = Vector512.ConditionalSelect(alphaMask, sourceAlpha, source * sourceAlpha); + Vector512 result = Vector512_.MultiplyAddEstimate(sourcePremultiplied - backdropPremultiplied, coverage, backdropPremultiplied); + + return Numerics.UnPremultiply(result, Vector512_.ShuffleNative(result, ShuffleAlphaControl)); } /// @@ -244,14 +405,32 @@ internal static partial class PorterDuffFunctions public static Vector256 OverlayValueFunction(Vector256 backdrop, Vector256 source) { Vector256 vOne = Vector256.Create(1F); - Vector256 left = Avx.Multiply(Avx.Add(backdrop, backdrop), source); + Vector256 left = (backdrop + backdrop) * source; Vector256 vOneMinusSource = Avx.Subtract(vOne, source); - Vector256 right = SimdUtils.HwIntrinsics.MultiplyAddNegated(Avx.Add(vOneMinusSource, vOneMinusSource), Avx.Subtract(vOne, backdrop), vOne); + Vector256 right = Vector256_.MultiplyAddNegated(vOne, vOneMinusSource + vOneMinusSource, vOne - backdrop); Vector256 cmp = Avx.CompareGreaterThan(backdrop, Vector256.Create(.5F)); return Avx.BlendVariable(left, right, cmp); } + /// + /// Helper function for Overlay and HardLight modes + /// + /// Backdrop color element + /// Source color element + /// Overlay value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 OverlayValueFunction(Vector512 backdrop, Vector512 source) + { + Vector512 vOne = Vector512.Create(1F); + Vector512 left = (backdrop + backdrop) * source; + + Vector512 vOneMinusSource = vOne - source; + Vector512 right = Vector512_.MultiplyAddNegated(vOne, vOneMinusSource + vOneMinusSource, vOne - backdrop); + Vector512 cmp = Avx512F.CompareGreaterThan(backdrop, Vector512.Create(.5F)); + return Vector512.ConditionalSelect(cmp, right, left); + } + /// /// Returns the result of the "Over" compositing equation. /// @@ -295,17 +474,47 @@ internal static partial class PorterDuffFunctions Vector256 sW = Avx.Permute(source, ShuffleAlphaControl); Vector256 dW = Avx.Permute(destination, ShuffleAlphaControl); - Vector256 blendW = Avx.Multiply(sW, dW); - Vector256 dstW = Avx.Subtract(dW, blendW); - Vector256 srcW = Avx.Subtract(sW, blendW); + Vector256 blendW = sW * dW; + Vector256 dstW = dW - blendW; + Vector256 srcW = sW - blendW; // calculate final alpha - Vector256 alpha = Avx.Add(dstW, sW); + Vector256 alpha = dstW + sW; // calculate final color - Vector256 color = Avx.Multiply(destination, dstW); - color = SimdUtils.HwIntrinsics.MultiplyAdd(color, source, srcW); - color = SimdUtils.HwIntrinsics.MultiplyAdd(color, blend, blendW); + Vector256 color = destination * dstW; + color = Vector256_.MultiplyAddEstimate(source, srcW, color); + color = Vector256_.MultiplyAddEstimate(blend, blendW, color); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "Over" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The amount to blend. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Over(Vector512 destination, Vector512 source, Vector512 blend) + { + // calculate weights + Vector512 sW = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 dW = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + + Vector512 blendW = sW * dW; + Vector512 dstW = dW - blendW; + Vector512 srcW = sW - blendW; + + // calculate final alpha + Vector512 alpha = dstW + sW; + + // calculate final color + Vector512 color = destination * dstW; + color = Vector512_.MultiplyAddEstimate(source, srcW, color); + color = Vector512_.MultiplyAddEstimate(blend, blendW, color); // unpremultiply return Numerics.UnPremultiply(color, alpha); @@ -354,11 +563,36 @@ internal static partial class PorterDuffFunctions // calculate weights Vector256 sW = Avx.Permute(source, ShuffleAlphaControl); - Vector256 blendW = Avx.Multiply(sW, alpha); - Vector256 dstW = Avx.Subtract(alpha, blendW); + Vector256 blendW = sW * alpha; + Vector256 dstW = alpha - blendW; + + // calculate final color + Vector256 color = Vector256_.MultiplyAddEstimate(destination, dstW, Avx.Multiply(blend, blendW)); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "Atop" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The amount to blend. Range 0..1 + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Atop(Vector512 destination, Vector512 source, Vector512 blend) + { + // calculate final alpha + Vector512 alpha = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + + // calculate weights + Vector512 sW = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 blendW = sW * alpha; + Vector512 dstW = alpha - blendW; // calculate final color - Vector256 color = SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(blend, blendW), destination, dstW); + Vector512 color = Vector512_.MultiplyAddEstimate(destination, dstW, blend * blendW); // unpremultiply return Numerics.UnPremultiply(color, alpha); @@ -392,10 +626,29 @@ internal static partial class PorterDuffFunctions public static Vector256 In(Vector256 destination, Vector256 source) { // calculate alpha - Vector256 alpha = Avx.Permute(Avx.Multiply(source, destination), ShuffleAlphaControl); + Vector256 alpha = Avx.Permute(source * destination, ShuffleAlphaControl); + + // premultiply + Vector256 color = source * alpha; + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "In" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 In(Vector512 destination, Vector512 source) + { + // calculate alpha + Vector512 alpha = Vector512_.ShuffleNative(source * destination, ShuffleAlphaControl); // premultiply - Vector256 color = Avx.Multiply(source, alpha); + Vector512 color = source * alpha; // unpremultiply return Numerics.UnPremultiply(color, alpha); @@ -429,10 +682,29 @@ internal static partial class PorterDuffFunctions public static Vector256 Out(Vector256 destination, Vector256 source) { // calculate alpha - Vector256 alpha = Avx.Permute(Avx.Multiply(source, Avx.Subtract(Vector256.Create(1F), destination)), ShuffleAlphaControl); + Vector256 alpha = Avx.Permute(source * (Vector256.Create(1F) - destination), ShuffleAlphaControl); // premultiply - Vector256 color = Avx.Multiply(source, alpha); + Vector256 color = source * alpha; + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "Out" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Out(Vector512 destination, Vector512 source) + { + // calculate alpha + Vector512 alpha = Vector512_.ShuffleNative(source * (Vector512.Create(1F) - destination), ShuffleAlphaControl); + + // premultiply + Vector512 color = source * alpha; // unpremultiply return Numerics.UnPremultiply(color, alpha); @@ -475,12 +747,37 @@ internal static partial class PorterDuffFunctions Vector256 dW = Avx.Shuffle(destination, destination, ShuffleAlphaControl); Vector256 vOne = Vector256.Create(1F); - Vector256 srcW = Avx.Subtract(vOne, dW); - Vector256 dstW = Avx.Subtract(vOne, sW); + Vector256 srcW = vOne - dW; + Vector256 dstW = vOne - sW; + + // calculate alpha + Vector256 alpha = Vector256_.MultiplyAddEstimate(sW, srcW, Avx.Multiply(dW, dstW)); + Vector256 color = Vector256_.MultiplyAddEstimate(Avx.Multiply(sW, source), srcW, Avx.Multiply(Avx.Multiply(dW, destination), dstW)); + + // unpremultiply + return Numerics.UnPremultiply(color, alpha); + } + + /// + /// Returns the result of the "XOr" compositing equation. + /// + /// The destination vector. + /// The source vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Xor(Vector512 destination, Vector512 source) + { + // calculate weights + Vector512 sW = Vector512_.ShuffleNative(source, ShuffleAlphaControl); + Vector512 dW = Vector512_.ShuffleNative(destination, ShuffleAlphaControl); + + Vector512 vOne = Vector512.Create(1F); + Vector512 srcW = vOne - dW; + Vector512 dstW = vOne - sW; // calculate alpha - Vector256 alpha = SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(dW, dstW), sW, srcW); - Vector256 color = SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(Avx.Multiply(dW, destination), dstW), Avx.Multiply(sW, source), srcW); + Vector512 alpha = Vector512_.MultiplyAddEstimate(sW, srcW, dW * dstW); + Vector512 color = Vector512_.MultiplyAddEstimate(sW * source, srcW, (dW * destination) * dstW); // unpremultiply return Numerics.UnPremultiply(color, alpha); @@ -491,4 +788,11 @@ internal static partial class PorterDuffFunctions [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector256 Clear(Vector256 backdrop, Vector256 source) => Vector256.Zero; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Clear(Vector512 backdrop, Vector512 source) => Vector512.Zero; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 AlphaMask512() + => Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); } diff --git a/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs b/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs index 4ed617438..bb519c174 100644 --- a/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs @@ -3,7 +3,7 @@ using System.Buffers; using System.Numerics; -using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats.PixelBlenders; namespace SixLabors.ImageSharp.PixelFormats; @@ -52,16 +52,276 @@ public abstract class PixelBlender Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); - Span sourceVectors = buffer.Slice(maxLength * 2, maxLength); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); - PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); - PixelOperations.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale); + this.ToBlendVector4(configuration, background[..maxLength], backgroundVectors); + this.ToBlendVector4(configuration, source[..maxLength], sourceVectors); this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + this.FromBlendVector4(configuration, destinationVectors, destination); + } + + /// + /// Blends a row against a constant source color. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + float amount) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends a row against a constant source color using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 2 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + float amount, + Span workingBuffer) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + + this.ToBlendVector4(configuration, background[..maxLength], backgroundVectors); + + this.BlendFunction(destinationVectors, backgroundVectors, this.ToBlendVector4(source), amount); + + this.FromBlendVector4(configuration, destinationVectors, destination); + } + + /// + /// Blends 2 rows together with per-pixel coverage. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount, + ReadOnlySpan coverage) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(coverage.Length, maxLength, nameof(coverage.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); + this.BlendWithCoverage( + configuration, + destination, + background, + source, + amount, + coverage, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends 2 rows together with per-pixel coverage using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount, + ReadOnlySpan coverage, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(coverage.Length, maxLength, nameof(coverage.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); + + this.ToBlendVector4(configuration, background[..maxLength], backgroundVectors); + this.ToBlendVector4(configuration, source[..maxLength], sourceVectors); + + this.BlendWithCoverageFunction(destinationVectors, backgroundVectors, sourceVectors, amount, coverage); + + this.FromBlendVector4(configuration, destinationVectors, destination); + } + + /// + /// Blends a row against a constant source color with per-pixel coverage. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + float amount, + ReadOnlySpan coverage) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(coverage.Length, maxLength, nameof(coverage.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); + this.BlendWithCoverage( + configuration, + destination, + background, + source, + amount, + coverage, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends a row against a constant source color with per-pixel coverage using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + /// Reusable temporary vector scratch with capacity for at least 2 rows. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + float amount, + ReadOnlySpan coverage, + Span workingBuffer) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(coverage.Length, maxLength, nameof(coverage.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + + this.ToBlendVector4(configuration, background[..maxLength], backgroundVectors); + + this.BlendWithCoverageFunction(destinationVectors, backgroundVectors, this.ToBlendVector4(source), amount, coverage); + + this.FromBlendVector4(configuration, destinationVectors, destination); } /// @@ -83,6 +343,27 @@ public abstract class PixelBlender ReadOnlySpan amount) => this.Blend(configuration, destination, background, source, amount); + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + Span workingBuffer) + => this.Blend(configuration, destination, background, source, amount, workingBuffer); + /// /// Blends 2 rows together /// @@ -109,18 +390,356 @@ public abstract class PixelBlender Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); - Span sourceVectors = buffer.Slice(maxLength * 2, maxLength); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends a row against a constant source color. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + ReadOnlySpan amount) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); - PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); - PixelOperations.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale); + this.ToBlendVector4(configuration, background[..maxLength], backgroundVectors); + this.ToBlendVector4(configuration, source[..maxLength], sourceVectors); this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + this.FromBlendVector4(configuration, destinationVectors, destination); } + /// + /// Blends a row against a constant source color using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 2 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + ReadOnlySpan amount, + Span workingBuffer) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + + this.ToBlendVector4(configuration, background[..maxLength], backgroundVectors); + + this.BlendFunction(destinationVectors, backgroundVectors, this.ToBlendVector4(source), amount); + + this.FromBlendVector4(configuration, destinationVectors, destination); + } + + /// + /// Blends 2 rows together with per-pixel coverage. + /// + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + ReadOnlySpan coverage) + => this.BlendWithCoverage(configuration, destination, background, source, amount, coverage); + + /// + /// Blends 2 rows together with per-pixel coverage using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + ReadOnlySpan coverage, + Span workingBuffer) + => this.BlendWithCoverage(configuration, destination, background, source, amount, coverage, workingBuffer); + + /// + /// Blends 2 rows together with per-pixel coverage. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + ReadOnlySpan coverage) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(coverage.Length, maxLength, nameof(coverage.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); + this.BlendWithCoverage( + configuration, + destination, + background, + source, + amount, + coverage, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends a row against a constant source color with per-pixel coverage. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + ReadOnlySpan amount, + ReadOnlySpan coverage) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(coverage.Length, maxLength, nameof(coverage.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); + this.BlendWithCoverage( + configuration, + destination, + background, + source, + amount, + coverage, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends 2 rows together with per-pixel coverage using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + ReadOnlySpan coverage, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(coverage.Length, maxLength, nameof(coverage.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); + + this.ToBlendVector4(configuration, background[..maxLength], backgroundVectors); + this.ToBlendVector4(configuration, source[..maxLength], sourceVectors); + + this.BlendWithCoverageFunction(destinationVectors, backgroundVectors, sourceVectors, amount, coverage); + + this.FromBlendVector4(configuration, destinationVectors, destination); + } + + /// + /// Blends a row against a constant source color with per-pixel coverage using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + /// Reusable temporary vector scratch with capacity for at least 2 rows. + public void BlendWithCoverage( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + ReadOnlySpan amount, + ReadOnlySpan coverage, + Span workingBuffer) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(coverage.Length, maxLength, nameof(coverage.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + + this.ToBlendVector4(configuration, background[..maxLength], backgroundVectors); + + this.BlendWithCoverageFunction(destinationVectors, backgroundVectors, this.ToBlendVector4(source), amount, coverage); + + this.FromBlendVector4(configuration, destinationVectors, destination); + } + + /// + /// Converts source pixels to the scaled-vector representation consumed by this blender. + /// + /// The source pixel format. + /// The configuration. + /// The source pixels. + /// The destination vectors. + protected virtual void ToBlendVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination) + where TPixelSource : unmanaged, IPixel + => PixelOperations.Instance.ToVector4(configuration, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + /// + /// Converts a source pixel to the vector representation consumed by the blend functions. + /// + /// The source pixel. + /// The source vector. + protected virtual Vector4 ToBlendVector4(TPixel source) + => source.ToUnassociatedScaledVector4(); + + /// + /// Converts blend results from this blender's scaled-vector representation to destination pixels. + /// + /// The configuration. + /// The source vectors. + /// The destination pixels. + protected virtual void FromBlendVector4( + Configuration configuration, + Span source, + Span destination) + => PixelOperations.Instance.FromVector4Destructive(configuration, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + /// /// Blend 2 rows together. /// @@ -137,6 +756,22 @@ public abstract class PixelBlender ReadOnlySpan source, float amount); + /// + /// Blend a row against a constant source color. + /// + /// destination span + /// the background span + /// the source color vector + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + protected abstract void BlendFunction( + Span destination, + ReadOnlySpan background, + Vector4 source, + float amount); + /// /// Blend 2 rows together. /// @@ -152,4 +787,124 @@ public abstract class PixelBlender ReadOnlySpan background, ReadOnlySpan source, ReadOnlySpan amount); + + /// + /// Blend a row against a constant source color. + /// + /// destination span + /// the background span + /// the source color vector + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + protected abstract void BlendFunction( + Span destination, + ReadOnlySpan background, + Vector4 source, + ReadOnlySpan amount); + + /// + /// Blend 2 rows together with per-pixel coverage. + /// + /// destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + protected virtual void BlendWithCoverageFunction( + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount, + ReadOnlySpan coverage) + { + this.BlendFunction(destination, background, source, amount); + + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.BlendWithCoverage(background[i], destination[i], Numerics.Clamp(coverage[i], 0, 1F)); + } + } + + /// + /// Blend a row against a constant source color with per-pixel coverage. + /// + /// destination span + /// the background span + /// the source color vector + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + protected virtual void BlendWithCoverageFunction( + Span destination, + ReadOnlySpan background, + Vector4 source, + float amount, + ReadOnlySpan coverage) + { + this.BlendFunction(destination, background, source, amount); + + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.BlendWithCoverage(background[i], destination[i], Numerics.Clamp(coverage[i], 0, 1F)); + } + } + + /// + /// Blend 2 rows together with per-pixel coverage. + /// + /// destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + protected virtual void BlendWithCoverageFunction( + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + ReadOnlySpan coverage) + { + this.BlendFunction(destination, background, source, amount); + + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.BlendWithCoverage(background[i], destination[i], Numerics.Clamp(coverage[i], 0, 1F)); + } + } + + /// + /// Blend a row against a constant source color with per-pixel coverage. + /// + /// destination span + /// the background span + /// the source color vector + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// A span with coverage values between 0 and 1. + protected virtual void BlendWithCoverageFunction( + Span destination, + ReadOnlySpan background, + Vector4 source, + ReadOnlySpan amount, + ReadOnlySpan coverage) + { + this.BlendFunction(destination, background, source, amount); + + for (int i = 0; i < destination.Length; i++) + { + destination[i] = PorterDuffFunctions.BlendWithCoverage(background[i], destination[i], Numerics.Clamp(coverage[i], 0, 1F)); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelConversionModifiers.cs b/src/ImageSharp/PixelFormats/PixelConversionModifiers.cs index edc04fa7c..8cee6057d 100644 --- a/src/ImageSharp/PixelFormats/PixelConversionModifiers.cs +++ b/src/ImageSharp/PixelFormats/PixelConversionModifiers.cs @@ -6,32 +6,38 @@ using SixLabors.ImageSharp.ColorProfiles.Companding; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Flags responsible to select additional operations which could be efficiently applied in +/// Specifies vector representation transformations applied by /// -/// or +/// and /// -/// knowing the pixel type. +/// during bulk pixel conversion. /// [Flags] public enum PixelConversionModifiers { /// - /// No special operation is selected + /// Preserves the pixel format's native numeric range and alpha representation. /// None = 0, /// - /// Select and instead the standard (non scaled) variants. + /// Requests the scaled numeric range represented by and . /// Scale = 1 << 0, /// - /// Enable alpha premultiplication / unpremultiplication + /// Requests associated alpha for the vector representation. + /// When combined with , associated alpha takes precedence. /// Premultiply = 1 << 1, /// - /// Enable SRGB companding (defined in ). + /// Expands sRGB color components when converting pixels to vectors and compresses linear RGB color components when converting vectors to pixels. /// SRgbCompand = 1 << 2, + + /// + /// Requests unassociated alpha for the vector representation unless is also specified. + /// + UnPremultiply = 1 << 3, } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/A8.cs b/src/ImageSharp/PixelFormats/PixelImplementations/A8.cs index 94fbf7eb7..8a17831a1 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/A8.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/A8.cs @@ -8,10 +8,11 @@ namespace SixLabors.ImageSharp.PixelFormats; /// /// Packed pixel type containing a single 8-bit normalized alpha value. -/// -/// Ranges from [0, 0, 0, 0] to [0, 0, 0, 1] in vector form. -/// /// +/// +/// and scaled vector conversions return alpha in [0, 1] with zero color components. +/// The storage layout matches DXGI_FORMAT_A8_UNORM. +/// public partial struct A8 : IPixel, IPackedVector { /// @@ -77,6 +78,38 @@ public partial struct A8 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromAssociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static A8 FromAssociatedVector4(Vector4 source) => FromVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static A8 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Abgr32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Abgr32.cs index 1190d307a..acd354b7f 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Abgr32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Abgr32.cs @@ -182,7 +182,7 @@ public partial struct Abgr32 : IPixel, IPackedVector /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / MaxBytes; + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / byte.MaxValue; /// public static PixelTypeInfo GetPixelTypeInfo() @@ -194,6 +194,47 @@ public partial struct Abgr32 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Abgr32 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Abgr32P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Abgr32P.cs new file mode 100644 index 000000000..9105a99f7 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Abgr32P.cs @@ -0,0 +1,273 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Packed pixel type containing alpha and associated blue, green, and red components as 8-bit unsigned normalized values. +/// Components are stored in alpha, blue, green, and red order from least to most significant byte. +/// +/// +/// The native component, packed, and vector representations use associated alpha. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct Abgr32P : IPixel, IPackedVector +{ + private static readonly Vector4 Half = new(0.5F); + private static readonly Vector4 MaxBytes = new(byte.MaxValue); + + /// + /// Gets or sets the alpha component. + /// + public byte A; + + /// + /// Gets or sets the associated blue component. + /// + public byte B; + + /// + /// Gets or sets the associated green component. + /// + public byte G; + + /// + /// Gets or sets the associated red component. + /// + public byte R; + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public Abgr32P(byte r, byte g, byte b) + : this(r, g, b, byte.MaxValue) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public Abgr32P(byte r, byte g, byte b, byte a) + { + this.A = a; + this.B = b; + this.G = g; + this.R = r; + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public Abgr32P(float r, float g, float b) + : this(new Vector4(r, g, b, 1F)) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public Abgr32P(float r, float g, float b, float a) + : this(new Vector4(r, g, b, a)) + { + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public Abgr32P(Vector3 vector) + : this(new Vector4(vector, 1F)) + { + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public Abgr32P(Vector4 vector) + : this() => this = FromScaledVector4(vector); + + /// + /// Initializes a new instance of the struct from a packed associated value. + /// + /// The packed associated value. + public Abgr32P(uint packed) + : this() => this.PackedValue = packed; + + /// + public uint PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(Abgr32P left, Abgr32P right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(Abgr32P left, Abgr32P right) => !left.Equals(right); + + /// + public readonly Rgba32 ToRgba32() + => Rgba32.FromScaledVector4(Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(this.R, this.G, this.B, this.A)); + + /// + public readonly Vector4 ToScaledVector4() => new Vector4(this.R, this.G, this.B, this.A) / byte.MaxValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() + { + // Divide the stored byte magnitudes before normalization so unassociation cannot move an exact byte conversion across its rounding midpoint. + return Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(this.R, this.G, this.B, this.A); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + public readonly Vector4 ToVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToUnassociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.Alpha | PixelColorType.BGR, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32P FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static Abgr32P FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32P FromUnassociatedVector4(Vector4 source) => FromUnassociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32P FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32P FromUnassociatedScaledVector4(Vector4 source) + => Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4ToAbgr32P(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Abgr32P FromAssociatedScaledVector4(Vector4 source) + { + // Rescale associated RGB when alpha rounds to a different byte so the stored channels remain associated with the alpha actually written. + return Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4ToAbgr32P(source); + } + + /// + public static Abgr32P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Abgr32P FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Abgr32P other && this.Equals(other); + + /// + public readonly bool Equals(Abgr32P other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() => $"Abgr32P({this.R}, {this.G}, {this.B}, {this.A})"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Abgr32P Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + // Each converted component occupies one 32-bit lane. Reinterpreting those lanes as bytes exposes their low bytes at offsets 0, 4, 8, and 12. + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return new Abgr32P(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs index b74f5db71..90ae88263 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Argb32.cs @@ -175,7 +175,7 @@ public partial struct Argb32 : IPixel, IPackedVector /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / MaxBytes; + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / byte.MaxValue; /// public static PixelTypeInfo GetPixelTypeInfo() @@ -187,6 +187,47 @@ public partial struct Argb32 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Argb32 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Argb32P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Argb32P.cs new file mode 100644 index 000000000..7c866c8fe --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Argb32P.cs @@ -0,0 +1,273 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Packed pixel type containing alpha and associated red, green, and blue components as 8-bit unsigned normalized values. +/// Components are stored in alpha, red, green, and blue order from least to most significant byte. +/// +/// +/// The native component, packed, and vector representations use associated alpha. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct Argb32P : IPixel, IPackedVector +{ + private static readonly Vector4 Half = new(0.5F); + private static readonly Vector4 MaxBytes = new(byte.MaxValue); + + /// + /// Gets or sets the alpha component. + /// + public byte A; + + /// + /// Gets or sets the associated red component. + /// + public byte R; + + /// + /// Gets or sets the associated green component. + /// + public byte G; + + /// + /// Gets or sets the associated blue component. + /// + public byte B; + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public Argb32P(byte r, byte g, byte b) + : this(r, g, b, byte.MaxValue) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public Argb32P(byte r, byte g, byte b, byte a) + { + this.A = a; + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public Argb32P(float r, float g, float b) + : this(new Vector4(r, g, b, 1F)) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public Argb32P(float r, float g, float b, float a) + : this(new Vector4(r, g, b, a)) + { + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public Argb32P(Vector3 vector) + : this(new Vector4(vector, 1F)) + { + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public Argb32P(Vector4 vector) + : this() => this = FromScaledVector4(vector); + + /// + /// Initializes a new instance of the struct from a packed associated value. + /// + /// The packed associated value. + public Argb32P(uint packed) + : this() => this.PackedValue = packed; + + /// + public uint PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(Argb32P left, Argb32P right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(Argb32P left, Argb32P right) => !left.Equals(right); + + /// + public readonly Rgba32 ToRgba32() + => Rgba32.FromScaledVector4(Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(this.R, this.G, this.B, this.A)); + + /// + public readonly Vector4 ToScaledVector4() => new Vector4(this.R, this.G, this.B, this.A) / byte.MaxValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() + { + // Divide the stored byte magnitudes before normalization so unassociation cannot move an exact byte conversion across its rounding midpoint. + return Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(this.R, this.G, this.B, this.A); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + public readonly Vector4 ToVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToUnassociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.Alpha | PixelColorType.RGB, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32P FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static Argb32P FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32P FromUnassociatedVector4(Vector4 source) => FromUnassociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32P FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32P FromUnassociatedScaledVector4(Vector4 source) + => Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4ToArgb32P(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Argb32P FromAssociatedScaledVector4(Vector4 source) + { + // Rescale associated RGB when alpha rounds to a different byte so the stored channels remain associated with the alpha actually written. + return Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4ToArgb32P(source); + } + + /// + public static Argb32P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Argb32P FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Argb32P other && this.Equals(other); + + /// + public readonly bool Equals(Argb32P other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() => $"Argb32P({this.R}, {this.G}, {this.B}, {this.A})"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Argb32P Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + // Each converted component occupies one 32-bit lane. Reinterpreting those lanes as bytes exposes their low bytes at offsets 0, 4, 8, and 12. + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return new Argb32P(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgr24.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgr24.cs index 944cdf7bf..c64047518 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgr24.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgr24.cs @@ -97,6 +97,48 @@ public partial struct Bgr24 : IPixel /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr24 FromAssociatedVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Bgr24 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs index 87055bf22..338c55fe3 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgr565.cs @@ -7,14 +7,12 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing unsigned normalized values ranging from 0 to 1. +/// Packed pixel type containing three unsigned normalized values. /// The x and z components use 5 bits, and the y component uses 6 bits. -/// -/// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form. -/// /// /// -/// Initializes a new instance of the struct. +/// and scaled vector conversions return color components in [0, 1] with an implicit +/// alpha of 1. The storage layout matches DXGI_FORMAT_B5G6R5_UNORM. /// /// /// The vector containing the components for the packed value. @@ -75,6 +73,48 @@ public partial struct Bgr565(Vector3 vector) : IPixel, IPackedVector public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgr565 FromAssociatedVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } + /// public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra32.cs index 903d9dc8c..e8405dd2b 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra32.cs @@ -124,7 +124,7 @@ public partial struct Bgra32 : IPixel, IPackedVector /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / MaxBytes; + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / byte.MaxValue; /// public static PixelTypeInfo GetPixelTypeInfo() @@ -136,6 +136,47 @@ public partial struct Bgra32 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Bgra32 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra32P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra32P.cs new file mode 100644 index 000000000..6ce3138f7 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra32P.cs @@ -0,0 +1,273 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Packed pixel type containing associated blue, green, red, and alpha components as 8-bit unsigned normalized values. +/// Components are stored in blue, green, red, and alpha order from least to most significant byte. +/// +/// +/// The native component, packed, and vector representations use associated alpha. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct Bgra32P : IPixel, IPackedVector +{ + private static readonly Vector4 Half = new(0.5F); + private static readonly Vector4 MaxBytes = new(byte.MaxValue); + + /// + /// Gets or sets the associated blue component. + /// + public byte B; + + /// + /// Gets or sets the associated green component. + /// + public byte G; + + /// + /// Gets or sets the associated red component. + /// + public byte R; + + /// + /// Gets or sets the alpha component. + /// + public byte A; + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public Bgra32P(byte r, byte g, byte b) + : this(r, g, b, byte.MaxValue) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public Bgra32P(byte r, byte g, byte b, byte a) + { + this.B = b; + this.G = g; + this.R = r; + this.A = a; + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public Bgra32P(float r, float g, float b) + : this(new Vector4(r, g, b, 1F)) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public Bgra32P(float r, float g, float b, float a) + : this(new Vector4(r, g, b, a)) + { + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public Bgra32P(Vector3 vector) + : this(new Vector4(vector, 1F)) + { + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public Bgra32P(Vector4 vector) + : this() => this = FromScaledVector4(vector); + + /// + /// Initializes a new instance of the struct from a packed associated value. + /// + /// The packed associated value. + public Bgra32P(uint packed) + : this() => this.PackedValue = packed; + + /// + public uint PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(Bgra32P left, Bgra32P right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(Bgra32P left, Bgra32P right) => !left.Equals(right); + + /// + public readonly Rgba32 ToRgba32() + => Rgba32.FromScaledVector4(Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(this.R, this.G, this.B, this.A)); + + /// + public readonly Vector4 ToScaledVector4() => new Vector4(this.R, this.G, this.B, this.A) / byte.MaxValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() + { + // Divide the stored byte magnitudes before normalization so unassociation cannot move an exact byte conversion across its rounding midpoint. + return Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(this.R, this.G, this.B, this.A); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + public readonly Vector4 ToVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToUnassociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.BGR | PixelColorType.Alpha, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32P FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static Bgra32P FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32P FromUnassociatedVector4(Vector4 source) => FromUnassociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32P FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32P FromUnassociatedScaledVector4(Vector4 source) + => Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4ToBgra32P(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra32P FromAssociatedScaledVector4(Vector4 source) + { + // Rescale associated RGB when alpha rounds to a different byte so the stored channels remain associated with the alpha actually written. + return Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4ToBgra32P(source); + } + + /// + public static Bgra32P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Bgra32P FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Bgra32P other && this.Equals(other); + + /// + public readonly bool Equals(Bgra32P other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() => $"Bgra32P({this.R}, {this.G}, {this.B}, {this.A})"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Bgra32P Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + // Each converted component occupies one 32-bit lane. Reinterpreting those lanes as bytes exposes their low bytes at offsets 0, 4, 8, and 12. + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return new Bgra32P(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs index 55971210c..17711835a 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra4444.cs @@ -7,11 +7,12 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing unsigned normalized values, ranging from 0 to 1, using 4 bits each for x, y, z, and w. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four unsigned normalized values using 4 bits per component. /// +/// +/// and scaled vector conversions return all components in [0, 1]. The storage layout matches +/// DXGI_FORMAT_B4G4R4A4_UNORM. +/// public partial struct Bgra4444 : IPixel, IPackedVector { /// @@ -88,6 +89,47 @@ public partial struct Bgra4444 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra4444 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Bgra4444 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs index 4c94dea5f..22a2c1f99 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Bgra5551.cs @@ -7,12 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing unsigned normalized values ranging from 0 to 1. -/// The x , y and z components use 5 bits, and the w component uses 1 bit. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four unsigned normalized values. +/// The x, y, and z components use 5 bits, and the w component uses 1 bit. /// +/// +/// and scaled vector conversions return all components in [0, 1]. The storage layout matches +/// DXGI_FORMAT_B5G5R5A1_UNORM. +/// public partial struct Bgra5551 : IPixel, IPackedVector { /// @@ -86,6 +87,47 @@ public partial struct Bgra5551 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Bgra5551 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Bgra5551 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs index 680a7ee0b..c0372ab97 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Byte4.cs @@ -8,11 +8,12 @@ using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 8-bit unsigned integer values, ranging from 0 to 255. -/// -/// Ranges from [0, 0, 0, 0] to [255, 255, 255, 255] in vector form. -/// +/// Packed pixel type containing four 8-bit unsigned integer values. /// +/// +/// returns components in [0, 255]. Scaled vector conversions map that range to +/// [0, 1]. The storage layout matches DXGI_FORMAT_R8G8B8A8_UINT. +/// public partial struct Byte4 : IPixel, IPackedVector { private static readonly Vector4 MaxBytes = Vector128.Create(255f).AsVector4(); @@ -88,6 +89,58 @@ public partial struct Byte4 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() + { + Vector4 vector = this.ToAssociatedScaledVector4(); + + // Native components use [0, 255], so association occurs in scaled [0, 1] space before mapping the result back. + return vector * MaxBytes; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Byte4 FromAssociatedVector4(Vector4 source) + { + // Map every native component, including alpha, to scaled [0, 1] space before unassociating. + source /= MaxBytes; + return FromAssociatedScaledVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Byte4 FromScaledVector4(Vector4 source) => FromVector4(source * 255f); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs index 00deadb12..cbb0cf086 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfSingle.cs @@ -7,11 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing a single 16 bit floating point value. -/// -/// Ranges from [-1, 0, 0, 1] to [1, 0, 0, 1] in vector form. -/// +/// Packed pixel type containing a single IEEE 754 binary16 floating-point value. /// +/// +/// returns the stored IEEE 754 binary16 value directly. Scaled vector conversions normalize +/// the finite range [-65504, 65504] to [0, 1]. The packed representation is binary-compatible with +/// DXGI_FORMAT_R16_FLOAT. +/// public partial struct HalfSingle : IPixel, IPackedVector { /// @@ -53,9 +55,8 @@ public partial struct HalfSingle : IPixel, IPackedVector [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Vector4 ToScaledVector4() { - float single = this.ToSingle() + 1F; - single /= 2F; - return new Vector4(single, 0, 0, 1F); + float scaled = HalfTypeHelper.ToScaled(this.ToSingle()); + return new Vector4(scaled, 0, 0, 1F); } /// @@ -72,16 +73,53 @@ public partial struct HalfSingle : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HalfSingle FromScaledVector4(Vector4 source) + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromAssociatedScaledVector4(Vector4 source) { - float scaled = source.X; - scaled *= 2F; - scaled--; - return new HalfSingle { PackedValue = HalfTypeHelper.Pack(scaled) }; + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromAssociatedVector4(Vector4 source) + { + // This format has no native alpha component: X uses the binary16 finite range, while W remains the source opacity. + source.X = HalfTypeHelper.ToScaled(source.X); + return FromAssociatedScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfSingle FromScaledVector4(Vector4 source) + => new() { PackedValue = HalfTypeHelper.Pack(HalfTypeHelper.FromScaled(source.X)) }; + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfSingle FromVector4(Vector4 source) => new() { PackedValue = HalfTypeHelper.Pack(source.X) }; diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs index 03d4dee89..ad6c7aefa 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector2.cs @@ -7,11 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two 16-bit floating-point values. -/// -/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. -/// +/// Packed pixel type containing two IEEE 754 binary16 floating-point values. /// +/// +/// and return the stored IEEE 754 binary16 values directly. Scaled +/// vector conversions normalize the finite range [-65504, 65504] to [0, 1]. The packed representation is +/// binary-compatible with DXGI_FORMAT_R16G16_FLOAT. +/// public partial struct HalfVector2 : IPixel, IPackedVector { /// @@ -60,9 +62,7 @@ public partial struct HalfVector2 : IPixel, IPackedVector [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Vector4 ToScaledVector4() { - Vector2 scaled = this.ToVector2(); - scaled += Vector2.One; - scaled /= 2F; + Vector2 scaled = HalfTypeHelper.ToScaled(this.ToVector2()); return new Vector4(scaled, 0F, 1F); } @@ -84,13 +84,56 @@ public partial struct HalfVector2 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector2 FromAssociatedVector4(Vector4 source) + { + // This format has no native alpha component: XY use the binary16 finite range, while W remains the source opacity. + Vector2 scaled = HalfTypeHelper.ToScaled(new Vector2(source.X, source.Y)); + source.X = scaled.X; + source.Y = scaled.Y; + return FromAssociatedScaledVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfVector2 FromScaledVector4(Vector4 source) { - Vector2 scaled = new Vector2(source.X, source.Y) * 2F; - scaled -= Vector2.One; - return new HalfVector2 { PackedValue = Pack(scaled.X, scaled.Y) }; + Vector2 native = HalfTypeHelper.FromScaled(new Vector2(source.X, source.Y)); + return new HalfVector2 { PackedValue = Pack(native.X, native.Y) }; } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs index d0b57d788..42531937c 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs @@ -7,11 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 16-bit floating-point values. -/// -/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four IEEE 754 binary16 floating-point values. /// +/// +/// returns the stored IEEE 754 binary16 values directly. Scaled vector conversions normalize +/// the finite range [-65504, 65504] to [0, 1]. The packed representation is binary-compatible with +/// DXGI_FORMAT_R16G16B16A16_FLOAT. +/// public partial struct HalfVector4 : IPixel, IPackedVector { /// @@ -63,13 +65,7 @@ public partial struct HalfVector4 : IPixel, IPackedVector /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToScaledVector4() - { - Vector4 scaled = this.ToVector4(); - scaled += Vector4.One; - scaled /= 2f; - return scaled; - } + public readonly Vector4 ToScaledVector4() => HalfTypeHelper.ToScaled(this.ToVector4()); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -89,15 +85,62 @@ public partial struct HalfVector4 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); - /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HalfVector4 FromScaledVector4(Vector4 source) + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() { - source *= 2f; - source -= Vector4.One; - return FromVector4(source); + Vector4 vector = this.ToAssociatedScaledVector4(); + + // Association is defined in scaled color space, so map the associated result back to the native binary16 range. + return HalfTypeHelper.FromScaled(vector); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromAssociatedVector4(Vector4 source) + { + // Restore scaled opacity before unassociating the color channels. + source = HalfTypeHelper.ToScaled(source); + return FromAssociatedScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4 FromScaledVector4(Vector4 source) => FromVector4(HalfTypeHelper.FromScaled(source)); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HalfVector4 FromVector4(Vector4 source) => new() { PackedValue = Pack(source) }; diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs new file mode 100644 index 000000000..50fc27255 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs @@ -0,0 +1,270 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Packed pixel type containing four associated 16-bit floating-point values. +/// +/// +/// returns the stored associated IEEE 754 binary16 values directly. Scaled vector conversions +/// normalize the finite range [-65504, 65504] to [0, 1] while preserving associated alpha. The packed +/// representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. +/// +public partial struct HalfVector4P : IPixel, IPackedVector +{ + /// + /// Initializes a new instance of the struct. + /// + /// The associated x-component. + /// The associated y-component. + /// The associated z-component. + /// The alpha component. + public HalfVector4P(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the associated component values. + public HalfVector4P(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public ulong PackedValue { get; set; } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(HalfVector4P left, HalfVector4P right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(HalfVector4P left, HalfVector4P right) => !left.Equals(right); + + /// + public readonly Rgba32 ToRgba32() + { + return Rgba32.FromScaledVector4(this.ToUnassociatedScaledVector4()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => HalfTypeHelper.ToScaled(this.ToVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.UnPremultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + public readonly Vector4 ToVector4() => new( + HalfTypeHelper.Unpack((ushort)this.PackedValue), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x10)), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x20)), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x30))); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() + { + Vector4 vector = this.ToUnassociatedScaledVector4(); + return HalfTypeHelper.FromScaled(vector); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + public static HalfVector4P FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static HalfVector4P FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4P FromUnassociatedVector4(Vector4 source) + { + source = HalfTypeHelper.ToScaled(source); + return FromUnassociatedScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4P FromAssociatedVector4(Vector4 source) + { + source = HalfTypeHelper.ToScaled(source); + return FromAssociatedScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4P FromUnassociatedScaledVector4(Vector4 source) => PackAssociatedScaledVector4(Associate(source)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HalfVector4P FromAssociatedScaledVector4(Vector4 source) => PackAssociatedScaledVector4(Reassociate(source)); + + /// + public static HalfVector4P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static HalfVector4P FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is HalfVector4P other && this.Equals(other); + + /// + public readonly bool Equals(HalfVector4P other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"HalfVector4P({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##}, {vector.W:#0.##})"); + } + + /// + /// Converts an unassociated scaled vector to the associated representation of a half-precision destination. + /// + /// The unassociated scaled vector. + /// The associated scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 Associate(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + + // RGB must use the scaled alpha that the binary16 representation can reproduce. + source.W = QuantizeScaledAlpha(source.W); + Numerics.Premultiply(ref source); + return source; + } + + /// + /// Reassociates a scaled vector with the alpha value the destination stores. + /// + /// The associated scaled vector. + /// The reassociated scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 Reassociate(Vector4 source) + { + float alpha = source.W; + + if (alpha <= 0) + { + return Vector4.Zero; + } + + float storedAlpha = QuantizeScaledAlpha(alpha); + + // Associated RGB scales by the same ratio as alpha. Applying that ratio directly avoids the extra division and multiplication of an unpremultiply/premultiply round trip and preserves exact midpoints when alpha needs no quantization. + source *= storedAlpha / alpha; + source.W = storedAlpha; + Numerics.ClampRgbToAlpha(ref source); + return source; + } + + /// + /// Packs an associated scaled vector into the native binary16 representation. + /// + /// The associated scaled vector. + /// The packed pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static HalfVector4P PackAssociatedScaledVector4(Vector4 source) + { + source = HalfTypeHelper.FromScaled(source); + return new HalfVector4P { PackedValue = Pack(source) }; + } + + /// + /// Quantizes scaled alpha through the native binary16 representation. + /// + /// The scaled alpha value. + /// The scaled value represented by the stored binary16 component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float QuantizeScaledAlpha(float alpha) + { + float nativeAlpha = HalfTypeHelper.FromScaled(Numerics.Clamp(alpha, 0F, 1F)); + return HalfTypeHelper.ToScaled(HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha))); + } + + /// + /// Packs native half-precision components into a 64-bit value. + /// + /// The native component values. + /// The packed value. + private static ulong Pack(Vector4 vector) + { + ulong x = HalfTypeHelper.Pack(vector.X); + ulong y = (ulong)HalfTypeHelper.Pack(vector.Y) << 0x10; + ulong z = (ulong)HalfTypeHelper.Pack(vector.Z) << 0x20; + ulong w = (ulong)HalfTypeHelper.Pack(vector.W) << 0x30; + return x | y | z | w; + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/L16.cs b/src/ImageSharp/PixelFormats/PixelImplementations/L16.cs index c5893f770..1ab8e9650 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/L16.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/L16.cs @@ -77,6 +77,48 @@ public partial struct L16 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L16 FromAssociatedVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static L16 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/L8.cs b/src/ImageSharp/PixelFormats/PixelImplementations/L8.cs index 008eed459..9908e0306 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/L8.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/L8.cs @@ -79,6 +79,48 @@ public partial struct L8 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static L8 FromAssociatedVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static L8 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/La16.cs b/src/ImageSharp/PixelFormats/PixelImplementations/La16.cs index 6c3fa7829..3a15f24d5 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/La16.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/La16.cs @@ -106,6 +106,47 @@ public partial struct La16 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La16 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static La16 FromScaledVector4(Vector4 source) => Pack(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/La32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/La32.cs index c99f6fe60..ae3190d96 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/La32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/La32.cs @@ -103,6 +103,47 @@ public partial struct La32 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static La32 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static La32 FromScaledVector4(Vector4 source) => Pack(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs index da2ab2440..6039bd9d7 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte2.cs @@ -7,15 +7,20 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two 8-bit signed normalized values, ranging from −1 to 1. -/// -/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. -/// +/// Packed pixel type containing two 8-bit signed normalized values. /// +/// +/// and return components in the native signed-normalized range +/// [-1, 1]. Scaled vector conversions return components in [0, 1]. +/// The packed two's-complement codes -128 and -127 both represent -1, +/// matching DXGI_FORMAT_R8G8_SNORM. +/// public partial struct NormalizedByte2 : IPixel, IPackedVector { private const float MaxPos = 127f; + private const float ScaledMagnitude = MaxPos * 2F; private static readonly Vector2 Half = new(MaxPos); + private static readonly Vector2 Minimum = new(-MaxPos); private static readonly Vector2 MinusOne = new(-1f); /// @@ -67,9 +72,14 @@ public partial struct NormalizedByte2 : IPixel, IPackedVector> 8)); + + // SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -128 cannot escape the scaled range. + scaled = Vector2.Max(scaled, Minimum); + scaled += Half; + scaled /= ScaledMagnitude; return new Vector4(scaled, 0f, 1f); } @@ -87,6 +97,49 @@ public partial struct NormalizedByte2 : IPixel, IPackedVector public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte2 FromAssociatedVector4(Vector4 source) + { + // Only the stored color components use the native [-1, 1] encoding; W remains the normalized source alpha. + source.X = (source.X + 1F) / 2F; + source.Y = (source.Y + 1F) / 2F; + return FromAssociatedScaledVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NormalizedByte2 FromScaledVector4(Vector4 source) @@ -158,9 +211,15 @@ public partial struct NormalizedByte2 : IPixel, IPackedVector /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector2 ToVector2() => new( - (sbyte)((this.PackedValue >> 0) & 0xFF) / MaxPos, - (sbyte)((this.PackedValue >> 8) & 0xFF) / MaxPos); + public readonly Vector2 ToVector2() + { + Vector2 vector = new( + (sbyte)(this.PackedValue & 0xFF), + (sbyte)(this.PackedValue >> 8)); + + // DirectX SNORM maps both -128 and -127 to -1. + return Vector2.Max(vector, Minimum) / MaxPos; + } /// public override readonly bool Equals(object? obj) => obj is NormalizedByte2 other && this.Equals(other); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs index 1fb386725..2f91718cf 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4.cs @@ -8,15 +8,20 @@ using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 8-bit signed normalized values, ranging from −1 to 1. -/// -/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four 8-bit signed normalized values. /// +/// +/// returns components in the native signed-normalized range [-1, 1]. +/// Scaled vector conversions return components in [0, 1]. +/// The packed two's-complement codes -128 and -127 both represent -1, +/// matching DXGI_FORMAT_R8G8B8A8_SNORM. +/// public partial struct NormalizedByte4 : IPixel, IPackedVector { private const float MaxPos = 127f; + private const float ScaledMagnitude = MaxPos * 2F; private static readonly Vector4 Half = Vector128.Create(MaxPos).AsVector4(); + private static readonly Vector4 Minimum = -Half; private static readonly Vector4 MinusOne = Vector128.Create(-1f).AsVector4(); /// @@ -70,19 +75,30 @@ public partial struct NormalizedByte4 : IPixel, IPackedVector> 0) & 0xFF), + (sbyte)((this.PackedValue >> 8) & 0xFF), + (sbyte)((this.PackedValue >> 16) & 0xFF), + (sbyte)((this.PackedValue >> 24) & 0xFF)); + + // SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -128 cannot escape the scaled range. + return (Vector4.Max(scaled, Minimum) + Half) / ScaledMagnitude; } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToVector4() => new( - (sbyte)((this.PackedValue >> 0) & 0xFF) / MaxPos, - (sbyte)((this.PackedValue >> 8) & 0xFF) / MaxPos, - (sbyte)((this.PackedValue >> 16) & 0xFF) / MaxPos, - (sbyte)((this.PackedValue >> 24) & 0xFF) / MaxPos); + public readonly Vector4 ToVector4() + { + Vector4 vector = new( + (sbyte)((this.PackedValue >> 0) & 0xFF), + (sbyte)((this.PackedValue >> 8) & 0xFF), + (sbyte)((this.PackedValue >> 16) & 0xFF), + (sbyte)((this.PackedValue >> 24) & 0xFF)); + + // DirectX SNORM maps both -128 and -127 to -1. + return Vector4.Max(vector, Minimum) / MaxPos; + } /// public static PixelTypeInfo GetPixelTypeInfo() @@ -94,6 +110,61 @@ public partial struct NormalizedByte4 : IPixel, IPackedVector public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() + { + Vector4 vector = this.ToAssociatedScaledVector4(); + + // Native components use an affine [-1, 1] encoding, so direct multiplication would use the wrong zero point. + vector *= 2F; + vector -= Vector4.One; + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4 FromAssociatedVector4(Vector4 source) + { + // Map the affine native encoding to logical [0, 1] space before unassociating. + source += Vector4.One; + source /= 2F; + return FromAssociatedScaledVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NormalizedByte4 FromScaledVector4(Vector4 source) diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4P.cs new file mode 100644 index 000000000..1172aafb9 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedByte4P.cs @@ -0,0 +1,318 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Packed pixel type containing four associated 8-bit signed normalized values ranging from -1 to 1. +/// +/// +/// returns associated components in the native signed-normalized range [-1, 1]. +/// Scaled vector conversions return associated components in [0, 1]. +/// The packed two's-complement codes -128 and -127 both represent -1, +/// matching DXGI_FORMAT_R8G8B8A8_SNORM. +/// +public partial struct NormalizedByte4P : IPixel, IPackedVector +{ + private const float MaxPos = 127F; + private const float ScaledMagnitude = MaxPos * 2F; + private static readonly Vector4 Half = Vector128.Create(MaxPos).AsVector4(); + private static readonly Vector4 Minimum = -Half; + private static readonly Vector4 MinusOne = Vector128.Create(-1F).AsVector4(); + + /// + /// Initializes a new instance of the struct. + /// + /// The associated x-component. + /// The associated y-component. + /// The associated z-component. + /// The alpha component. + public NormalizedByte4P(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the associated component values. + public NormalizedByte4P(Vector4 vector) => this.PackedValue = Pack(vector); + + /// + public uint PackedValue { get; set; } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(NormalizedByte4P left, NormalizedByte4P right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(NormalizedByte4P left, NormalizedByte4P right) => !left.Equals(right); + + /// + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(ToUnassociatedScaledVector4(this)); + + /// + public readonly Vector4 ToScaledVector4() + { + // Offset the exact signed components before division. Mapping an already normalized value through (value + 1) / 2 loses precision near -1 through cancellation. + Vector4 scaled = new( + (sbyte)((this.PackedValue >> 0) & 0xFF), + (sbyte)((this.PackedValue >> 8) & 0xFF), + (sbyte)((this.PackedValue >> 16) & 0xFF), + (sbyte)((this.PackedValue >> 24) & 0xFF)); + + // SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -128 cannot escape the scaled range. + return (Vector4.Max(scaled, Minimum) + Half) / ScaledMagnitude; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => ToUnassociatedScaledVector4(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + public readonly Vector4 ToVector4() + { + Vector4 vector = new( + (sbyte)((this.PackedValue >> 0) & 0xFF), + (sbyte)((this.PackedValue >> 8) & 0xFF), + (sbyte)((this.PackedValue >> 16) & 0xFF), + (sbyte)((this.PackedValue >> 24) & 0xFF)); + + // DirectX SNORM maps both -128 and -127 to -1. + return Vector4.Max(vector, Minimum) / MaxPos; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() + { + // Association is defined in the common scaled domain. Signed-native W is an affine encoding of alpha, so it cannot be used as a divisor directly. + Vector4 vector = this.ToUnassociatedScaledVector4(); + vector *= 2F; + vector -= Vector4.One; + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + public static NormalizedByte4P FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static NormalizedByte4P FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4P FromUnassociatedVector4(Vector4 source) + { + // Convert the signed-native range to the common scaled domain before associating because native W is not opacity. + source += Vector4.One; + source /= 2F; + return FromUnassociatedScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4P FromAssociatedVector4(Vector4 source) + { + // Reassociation must also operate in scaled space so RGB follows the quantized scaled alpha rather than the signed-native W component. + source += Vector4.One; + source /= 2F; + return FromAssociatedScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4P FromUnassociatedScaledVector4(Vector4 source) => PackAssociatedScaledVector4(Associate(source)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedByte4P FromAssociatedScaledVector4(Vector4 source) => PackAssociatedScaledVector4(Reassociate(source)); + + /// + /// Packs an associated scaled vector into signed-normalized storage. + /// + /// The associated scaled vector. + /// The packed pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static NormalizedByte4P PackAssociatedScaledVector4(Vector4 source) + { + // Signed-normalized storage uses the native [-1, 1] range even though association is defined in scaled opacity space. + source *= 2F; + source -= Vector4.One; + return new() { PackedValue = Pack(source) }; + } + + /// + public static NormalizedByte4P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static NormalizedByte4P FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is NormalizedByte4P other && this.Equals(other); + + /// + public readonly bool Equals(NormalizedByte4P other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() + { + Vector4 vector = this.ToVector4(); + return FormattableString.Invariant($"NormalizedByte4P({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##}, {vector.W:#0.##})"); + } + + /// + /// Converts an unassociated scaled vector to the associated representation of a signed-normalized-byte destination. + /// + /// The unassociated scaled vector. + /// The associated scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 Associate(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + + // Reproduce the signed-normalized packer's alpha quantization, then associate RGB with the exact alpha that will be stored. + float nativeAlpha = Numerics.Clamp((source.W * 2F) - 1F, -1F, 1F); + float storedAlpha = MathF.Round(nativeAlpha * MaxPos); + source.W = (storedAlpha + MaxPos) / ScaledMagnitude; + Numerics.Premultiply(ref source); + return source; + } + + /// + /// Converts the stored associated components to an unassociated scaled vector. + /// + /// The associated pixel. + /// The unassociated scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 ToUnassociatedScaledVector4(NormalizedByte4P source) + { + // Offset signed storage into exact nonnegative byte magnitudes before division so the quotient retains the destination's byte-rounding midpoint. + Vector4 vector = new( + (sbyte)(source.PackedValue >> 0), + (sbyte)(source.PackedValue >> 8), + (sbyte)(source.PackedValue >> 16), + (sbyte)(source.PackedValue >> 24)); + + // Clamp the duplicate SNORM minimum encoding before converting it to the nonnegative associated domain. + vector = Vector4.Max(vector, Minimum) + Half; + + if (vector.W == 0F) + { + // Numerics.UnPremultiply preserves RGB when alpha is zero. Normalize the stored components because they already are the unassociated value in this case. + return vector / ScaledMagnitude; + } + + Numerics.UnPremultiply(ref vector); + vector.W /= ScaledMagnitude; + return vector; + } + + /// + /// Reassociates a scaled vector with the alpha value the destination stores. + /// + /// The associated scaled vector. + /// The reassociated scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 Reassociate(Vector4 source) + { + float alpha = source.W; + + if (alpha <= 0) + { + return Vector4.Zero; + } + + float nativeAlpha = Numerics.Clamp((alpha * 2F) - 1F, -1F, 1F); + float storedAlpha = (MathF.Round(nativeAlpha * MaxPos) + MaxPos) / ScaledMagnitude; + + // Associated RGB scales by the same ratio as alpha. Applying that ratio directly avoids the extra division and multiplication of an unpremultiply/premultiply round trip and preserves exact midpoints when alpha needs no quantization. + source *= storedAlpha / alpha; + source.W = storedAlpha; + Numerics.ClampRgbToAlpha(ref source); + return source; + } + + /// + /// Packs native signed normalized components into a 32-bit value. + /// + /// The native component values. + /// The packed value. + private static uint Pack(Vector4 vector) + { + vector = Numerics.Clamp(vector, MinusOne, Vector4.One) * Half; + + uint byte4 = ((uint)Convert.ToInt16(MathF.Round(vector.X)) & 0xFF) << 0; + uint byte3 = ((uint)Convert.ToInt16(MathF.Round(vector.Y)) & 0xFF) << 8; + uint byte2 = ((uint)Convert.ToInt16(MathF.Round(vector.Z)) & 0xFF) << 16; + uint byte1 = ((uint)Convert.ToInt16(MathF.Round(vector.W)) & 0xFF) << 24; + + return byte4 | byte3 | byte2 | byte1; + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs index 0942c3a76..3da0f7f0b 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort2.cs @@ -7,15 +7,19 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two 16-bit signed normalized values, ranging from −1 to 1. -/// -/// Ranges from [-1, -1, 0, 1] to [1, 1, 0, 1] in vector form. -/// +/// Packed pixel type containing two 16-bit signed normalized values. /// +/// +/// and return components in the native signed-normalized range +/// [-1, 1]. Scaled vector conversions return components in [0, 1]. +/// The packed two's-complement codes -32768 and -32767 both represent -1, +/// matching DXGI_FORMAT_R16G16_SNORM. +/// public partial struct NormalizedShort2 : IPixel, IPackedVector { // Largest two byte positive number 0xFFFF >> 1; private const float MaxPos = 0x7FFF; + private const float ScaledMagnitude = MaxPos * 2F; private static readonly Vector2 Max = new(MaxPos); private static readonly Vector2 Min = Vector2.Negate(Max); @@ -69,9 +73,14 @@ public partial struct NormalizedShort2 : IPixel, IPackedVector [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Vector4 ToScaledVector4() { - Vector2 scaled = this.ToVector2(); - scaled += Vector2.One; - scaled /= 2f; + Vector2 scaled = new( + (short)(this.PackedValue & 0xFFFF), + (short)(this.PackedValue >> 0x10)); + + // SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -32768 cannot escape the scaled range. + scaled = Vector2.Max(scaled, Min); + scaled += Max; + scaled /= ScaledMagnitude; return new Vector4(scaled, 0f, 1f); } @@ -89,6 +98,49 @@ public partial struct NormalizedShort2 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort2 FromAssociatedVector4(Vector4 source) + { + // Only the stored color components use the native [-1, 1] encoding; W remains the normalized source alpha. + source.X = (source.X + 1F) / 2F; + source.Y = (source.Y + 1F) / 2F; + return FromAssociatedScaledVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NormalizedShort2 FromScaledVector4(Vector4 source) @@ -160,9 +212,15 @@ public partial struct NormalizedShort2 : IPixel, IPackedVector /// /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector2 ToVector2() => new( - (short)(this.PackedValue & 0xFFFF) / MaxPos, - (short)(this.PackedValue >> 0x10) / MaxPos); + public readonly Vector2 ToVector2() + { + Vector2 vector = new( + (short)(this.PackedValue & 0xFFFF), + (short)(this.PackedValue >> 0x10)); + + // DirectX SNORM maps both -32768 and -32767 to -1. + return Vector2.Max(vector, Min) / MaxPos; + } /// public override readonly bool Equals(object? obj) => obj is NormalizedShort2 other && this.Equals(other); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs index 2b33fec27..7acac0b7d 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/NormalizedShort4.cs @@ -8,15 +8,19 @@ using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 16-bit signed normalized values, ranging from −1 to 1. -/// -/// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four 16-bit signed normalized values. /// +/// +/// returns components in the native signed-normalized range [-1, 1]. +/// Scaled vector conversions return components in [0, 1]. +/// The packed two's-complement codes -32768 and -32767 both represent -1, +/// matching DXGI_FORMAT_R16G16B16A16_SNORM. +/// public partial struct NormalizedShort4 : IPixel, IPackedVector { // Largest two byte positive number 0xFFFF >> 1; private const float MaxPos = 0x7FFF; + private const float ScaledMagnitude = MaxPos * 2F; private static readonly Vector4 Max = Vector128.Create(MaxPos).AsVector4(); private static readonly Vector4 Min = Vector4.Negate(Max); @@ -71,19 +75,30 @@ public partial struct NormalizedShort4 : IPixel, IPackedVector [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Vector4 ToScaledVector4() { - Vector4 scaled = this.ToVector4(); - scaled += Vector4.One; - scaled /= 2f; - return scaled; + // Offset the exact signed components before division. Mapping an already normalized value through (value + 1) / 2 loses precision near -1 through cancellation. + Vector4 scaled = new( + (short)((this.PackedValue >> 0x00) & 0xFFFF), + (short)((this.PackedValue >> 0x10) & 0xFFFF), + (short)((this.PackedValue >> 0x20) & 0xFFFF), + (short)((this.PackedValue >> 0x30) & 0xFFFF)); + + // SNORM reserves both minimum two's-complement codes for -1. Clamp before offsetting so raw -32768 cannot escape the scaled range. + return (Vector4.Max(scaled, Min) + Max) / ScaledMagnitude; } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToVector4() => new( - (short)((this.PackedValue >> 0x00) & 0xFFFF) / MaxPos, - (short)((this.PackedValue >> 0x10) & 0xFFFF) / MaxPos, - (short)((this.PackedValue >> 0x20) & 0xFFFF) / MaxPos, - (short)((this.PackedValue >> 0x30) & 0xFFFF) / MaxPos); + public readonly Vector4 ToVector4() + { + Vector4 vector = new( + (short)((this.PackedValue >> 0x00) & 0xFFFF), + (short)((this.PackedValue >> 0x10) & 0xFFFF), + (short)((this.PackedValue >> 0x20) & 0xFFFF), + (short)((this.PackedValue >> 0x30) & 0xFFFF)); + + // DirectX SNORM maps both -32768 and -32767 to -1. + return Vector4.Max(vector, Min) / MaxPos; + } /// public static PixelTypeInfo GetPixelTypeInfo() @@ -95,6 +110,61 @@ public partial struct NormalizedShort4 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() + { + Vector4 vector = this.ToAssociatedScaledVector4(); + + // Native components use an affine [-1, 1] encoding, so direct multiplication would use the wrong zero point. + vector *= 2F; + vector -= Vector4.One; + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NormalizedShort4 FromAssociatedVector4(Vector4 source) + { + // Map the affine native encoding to logical [0, 1] space before unassociating. + source += Vector4.One; + source /= 2F; + return FromAssociatedScaledVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NormalizedShort4 FromScaledVector4(Vector4 source) diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/A8.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/A8.PixelOperations.cs index da0e6ec60..da6762912 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/A8.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/A8.PixelOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +13,20 @@ public partial struct A8 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + // A8 stores alpha without color components, so association cannot alter any emitted or consumed vector component. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) => this.FromUnassociatedVector4Destructive(configuration, source, destination); + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) => this.FromUnassociatedScaledVector4Destructive(configuration, source, destination); + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Abgr32P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Abgr32P.PixelOperations.cs new file mode 100644 index 000000000..746fbf85f --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Abgr32P.PixelOperations.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Abgr32P +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(source, destination); + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToAssociatedVector4(source, destination); + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // Native and scaled vectors have the same range for this format, so the byte-specialized converter is valid for both contracts. + Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4(source, destination); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // The converter rescales RGB when alpha rounds so the channels remain associated with the byte alpha actually stored. + Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4(source, destination); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + // This byte format has the same normalized native and scaled ranges, so the unmodified converter satisfies both contracts. + Vector4Converters.AssociatedRgbaCompatible.ToAssociatedVector4(source, destination); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4(source, destination); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4(source, destination); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Argb32P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Argb32P.PixelOperations.cs new file mode 100644 index 000000000..80dbd3c31 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Argb32P.PixelOperations.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Argb32P +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(source, destination); + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToAssociatedVector4(source, destination); + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // Native and scaled vectors have the same range for this format, so the byte-specialized converter is valid for both contracts. + Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4(source, destination); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // The converter rescales RGB when alpha rounds so the channels remain associated with the byte alpha actually stored. + Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4(source, destination); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + // This byte format has the same normalized native and scaled ranges, so the unmodified converter satisfies both contracts. + Vector4Converters.AssociatedRgbaCompatible.ToAssociatedVector4(source, destination); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4(source, destination); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4(source, destination); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr565.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr565.PixelOperations.cs index 87175fe21..737f6999f 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr565.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgr565.PixelOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +13,14 @@ public partial struct Bgr565 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra32P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra32P.PixelOperations.cs new file mode 100644 index 000000000..606990d66 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Bgra32P.PixelOperations.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Bgra32P +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(source, destination); + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToAssociatedVector4(source, destination); + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // Native and scaled vectors have the same range for this format, so the byte-specialized converter is valid for both contracts. + Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4(source, destination); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // The converter rescales RGB when alpha rounds so the channels remain associated with the byte alpha actually stored. + Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4(source, destination); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + // This byte format has the same normalized native and scaled ranges, so the unmodified converter satisfies both contracts. + Vector4Converters.AssociatedRgbaCompatible.ToAssociatedVector4(source, destination); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4(source, destination); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4(source, destination); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Byte4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Byte4.PixelOperations.cs index c012547c9..7f0919919 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Byte4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Byte4.PixelOperations.cs @@ -1,6 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +15,93 @@ public partial struct Byte4 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + // Protected hooks are same-instance extension points. Cross-format reuse must request the representation through public modifier dispatch. + private const PixelConversionModifiers UnassociatedScaledModifiers = PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply; + private const PixelConversionModifiers AssociatedScaledModifiers = PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply; + + private static readonly Vector4 NativeMaximum = new(byte.MaxValue); + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Byte4 and Rgba32 have the same packed layout. Reusing the optimized Rgba32 unpacker avoids four scalar byte + // extractions per pixel; the affine transform then restores Byte4's native [0, 255] vector range. + PixelOperations.Instance.ToVector4(configuration, MemoryMarshal.Cast(source), destination, UnassociatedScaledModifiers); + Vector4Converters.MultiplyThenAdd(destination, NativeMaximum, Vector4.Zero); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Association is performed in normalized color space before mapping all four components back to Byte4's native range. + PixelOperations.Instance.ToVector4(configuration, MemoryMarshal.Cast(source), destination, AssociatedScaledModifiers); + Vector4Converters.MultiplyThenAdd(destination, NativeMaximum, Vector4.Zero); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + PixelOperations.Instance.ToVector4(configuration, MemoryMarshal.Cast(source), destination, UnassociatedScaledModifiers); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + PixelOperations.Instance.ToVector4(configuration, MemoryMarshal.Cast(source), destination, AssociatedScaledModifiers); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + Vector4Converters.AddThenDivide(source, Vector4.Zero, NativeMaximum); + PixelOperations.Instance.FromVector4Destructive(configuration, source, MemoryMarshal.Cast(destination), UnassociatedScaledModifiers); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Native alpha is normalized with RGB so the Rgba32 bulk path can unassociate in a single, consistent coordinate space. + Vector4Converters.AddThenDivide(source, Vector4.Zero, NativeMaximum); + PixelOperations.Instance.FromVector4Destructive(configuration, source, MemoryMarshal.Cast(destination), AssociatedScaledModifiers); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + PixelOperations.Instance.FromVector4Destructive(configuration, source, MemoryMarshal.Cast(destination), UnassociatedScaledModifiers); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + PixelOperations.Instance.FromVector4Destructive(configuration, source, MemoryMarshal.Cast(destination), AssociatedScaledModifiers); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.cs index a5e4e3048..bcfad0117 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Abgr32.PixelOperations.Generated.cs @@ -38,6 +38,7 @@ public partial struct Abgr32 source.CopyTo(destination.Slice(0, source.Length)); } + /// public override void FromVector4Destructive( Configuration configuration, @@ -45,7 +46,20 @@ public partial struct Abgr32 Span destination, PixelConversionModifiers modifiers) { - Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + Guard.DestinationShouldNotBeTooShort(sourceVectors, destination, nameof(destination)); + + destination = destination[..sourceVectors.Length]; + Vector4Converters.ApplyBackwardConversionModifiers(sourceVectors, modifiers.Remove(PixelConversionModifiers.Scale)); + + Span destinationBytes = MemoryMarshal.Cast(destination); + + // The SIMD saturating conversion produces RGBA byte order. Reusing the destination + // buffer and shuffling it in place avoids the row-sized Rgba32 temporary used by + // the generic Rgba-compatible path for non-Rgba32 formats. + SimdUtils.NormalizedFloatToByteSaturate( + MemoryMarshal.Cast(sourceVectors), + destinationBytes); + PixelConverter.FromRgba32.ToAbgr32(destinationBytes, destinationBytes); } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.cs index 30fed8d1c..3ef93de9f 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Argb32.PixelOperations.Generated.cs @@ -38,6 +38,7 @@ public partial struct Argb32 source.CopyTo(destination.Slice(0, source.Length)); } + /// public override void FromVector4Destructive( Configuration configuration, @@ -45,7 +46,20 @@ public partial struct Argb32 Span destination, PixelConversionModifiers modifiers) { - Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + Guard.DestinationShouldNotBeTooShort(sourceVectors, destination, nameof(destination)); + + destination = destination[..sourceVectors.Length]; + Vector4Converters.ApplyBackwardConversionModifiers(sourceVectors, modifiers.Remove(PixelConversionModifiers.Scale)); + + Span destinationBytes = MemoryMarshal.Cast(destination); + + // The SIMD saturating conversion produces RGBA byte order. Reusing the destination + // buffer and shuffling it in place avoids the row-sized Rgba32 temporary used by + // the generic Rgba-compatible path for non-Rgba32 formats. + SimdUtils.NormalizedFloatToByteSaturate( + MemoryMarshal.Cast(sourceVectors), + destinationBytes); + PixelConverter.FromRgba32.ToArgb32(destinationBytes, destinationBytes); } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.cs index e3ff3a7fe..773b8c1b8 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgr24.PixelOperations.Generated.cs @@ -7,7 +7,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats.Utils; - namespace SixLabors.ImageSharp.PixelFormats; /// @@ -45,7 +44,7 @@ public partial struct Bgr24 Span destination, PixelConversionModifiers modifiers) { - Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale)); } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.cs index ae6be4401..6afcb6432 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Bgra32.PixelOperations.Generated.cs @@ -38,6 +38,7 @@ public partial struct Bgra32 source.CopyTo(destination.Slice(0, source.Length)); } + /// public override void FromVector4Destructive( Configuration configuration, @@ -45,7 +46,20 @@ public partial struct Bgra32 Span destination, PixelConversionModifiers modifiers) { - Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale)); + Guard.DestinationShouldNotBeTooShort(sourceVectors, destination, nameof(destination)); + + destination = destination[..sourceVectors.Length]; + Vector4Converters.ApplyBackwardConversionModifiers(sourceVectors, modifiers.Remove(PixelConversionModifiers.Scale)); + + Span destinationBytes = MemoryMarshal.Cast(destination); + + // The SIMD saturating conversion produces RGBA byte order. Reusing the destination + // buffer and shuffling it in place avoids the row-sized Rgba32 temporary used by + // the generic Rgba-compatible path for non-Rgba32 formats. + SimdUtils.NormalizedFloatToByteSaturate( + MemoryMarshal.Cast(sourceVectors), + destinationBytes); + PixelConverter.FromRgba32.ToBgra32(destinationBytes, destinationBytes); } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.cs index fd6465a22..5c8aeea76 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/Rgb24.PixelOperations.Generated.cs @@ -7,7 +7,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats.Utils; - namespace SixLabors.ImageSharp.PixelFormats; /// @@ -45,7 +44,7 @@ public partial struct Rgb24 Span destination, PixelConversionModifiers modifiers) { - Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(PixelConversionModifiers.Scale)); } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/_Common.ttinclude b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/_Common.ttinclude index ae29223de..abf077d15 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/_Common.ttinclude +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Generated/_Common.ttinclude @@ -147,10 +147,20 @@ using SixLabors.ImageSharp.PixelFormats.Utils; void GenerateRgba32CompatibleVector4ConversionMethods(string pixelType, bool hasAlpha) { - string removeTheseModifiers = "PixelConversionModifiers.Scale"; + string removeFromModifiers = "PixelConversionModifiers.Scale"; + string removeToModifiers = "PixelConversionModifiers.Scale"; + if (!hasAlpha) { - removeTheseModifiers += " | PixelConversionModifiers.Premultiply"; + // Premultiplication cannot change an alpha-less source, but premultiplied input + // must still be unassociated before its alpha channel is discarded. + removeToModifiers += " | PixelConversionModifiers.Premultiply"; + } + + if (hasAlpha) + { + GenerateRgba32Compatible32BitVector4ConversionMethods(pixelType, removeFromModifiers); + return; } #> @@ -161,7 +171,46 @@ using SixLabors.ImageSharp.PixelFormats.Utils; Span<<#=pixelType#>> destination, PixelConversionModifiers modifiers) { - Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(<#=removeTheseModifiers#>)); + Vector4Converters.RgbaCompatible.FromVector4(configuration, this, sourceVectors, destination, modifiers.Remove(<#=removeFromModifiers#>)); + } + + /// + public override void ToVector4( + Configuration configuration, + ReadOnlySpan<<#=pixelType#>> source, + Span destination, + PixelConversionModifiers modifiers) + { + Vector4Converters.RgbaCompatible.ToVector4(configuration, this, source, destination, modifiers.Remove(<#=removeToModifiers#>)); + } +<#+ + } + + void GenerateRgba32Compatible32BitVector4ConversionMethods(string pixelType, string removeTheseModifiers) + { +#> + + /// + public override void FromVector4Destructive( + Configuration configuration, + Span sourceVectors, + Span<<#=pixelType#>> destination, + PixelConversionModifiers modifiers) + { + Guard.DestinationShouldNotBeTooShort(sourceVectors, destination, nameof(destination)); + + destination = destination[..sourceVectors.Length]; + Vector4Converters.ApplyBackwardConversionModifiers(sourceVectors, modifiers.Remove(<#=removeTheseModifiers#>)); + + Span destinationBytes = MemoryMarshal.Cast<<#=pixelType#>, byte>(destination); + + // The SIMD saturating conversion produces RGBA byte order. Reusing the destination + // buffer and shuffling it in place avoids the row-sized Rgba32 temporary used by + // the generic Rgba-compatible path for non-Rgba32 formats. + SimdUtils.NormalizedFloatToByteSaturate( + MemoryMarshal.Cast(sourceVectors), + destinationBytes); + PixelConverter.FromRgba32.To<#=pixelType#>(destinationBytes, destinationBytes); } /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs index 770f3a1de..07428f484 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfSingle.PixelOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +13,14 @@ public partial struct HalfSingle /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs index 160ab9bd0..e167c4eb7 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector2.PixelOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +13,14 @@ public partial struct HalfVector2 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs index 703138454..9eae7d5cb 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs @@ -1,6 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +15,91 @@ public partial struct HalfVector4 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange); + private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint); + private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange); + private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum); + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // The half-vector layouts are identical, so the shared expansion kernel can process the source without copying it. + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination[..source.Length]); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Association uses normalized opacity, not the native binary16 alpha value. + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); + Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + Numerics.Premultiply(destination); + Vector4Converters.MultiplyThenAdd(destination, ScaledToNativeMultiplier, ScaledToNativeOffset); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); + Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + this.ToUnassociatedScaledVector4(configuration, source, destination); + Numerics.Premultiply(destination[..source.Length]); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // DirectX half vectors are not normalized formats, so values outside the nominal color range must reach storage unchanged. + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // Restore normalized opacity before unassociating, then return the result to the native binary16 range. + Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + Numerics.UnPremultiply(source); + Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Numerics.UnPremultiply(source); + Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs new file mode 100644 index 000000000..52b2dbf37 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs @@ -0,0 +1,375 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct HalfVector4P +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange); + private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint); + private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange); + private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum); + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + this.ToUnassociatedScaledVector4(configuration, source, destination); + Vector4Converters.MultiplyThenAdd(destination[..source.Length], ScaledToNativeMultiplier, ScaledToNativeOffset); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination[..source.Length]); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + this.ToAssociatedScaledVector4(configuration, source, destination); + Numerics.UnPremultiply(destination[..source.Length]); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); + Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + Associate(source); + PackAssociatedScaled(source, destination[..source.Length]); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + Reassociate(source); + PackAssociatedScaled(source, destination[..source.Length]); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Associate(source); + PackAssociatedScaled(source, destination[..source.Length]); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Reassociate(source); + PackAssociatedScaled(source, destination[..source.Length]); + } + + /// + /// Associates scaled vectors with the alpha values representable by native binary16 storage. + /// + /// The vectors to convert in place. + private static void Associate(Span source) + { + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorsPerRegister = Vector512.Count / Vector128.Count; + + for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) + { + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Associate(vector); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorsPerRegister = Vector256.Count / Vector128.Count; + + for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) + { + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Associate(vector); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i < source.Length; i++) + { + ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Associate(vector); + } + + return; + } + + for (; i < source.Length; i++) + { + Unsafe.Add(ref sourceBase, (uint)i) = HalfVector4P.Associate(Unsafe.Add(ref sourceBase, (uint)i)); + } + } + + /// + /// Reassociates scaled vectors with the alpha values representable by native binary16 storage. + /// + /// The vectors to convert in place. + private static void Reassociate(Span source) + { + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorsPerRegister = Vector512.Count / Vector128.Count; + + for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) + { + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Reassociate(vector); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorsPerRegister = Vector256.Count / Vector128.Count; + + for (; i <= source.Length - vectorsPerRegister; i += vectorsPerRegister) + { + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Reassociate(vector); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i < source.Length; i++) + { + ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + vector = Reassociate(vector); + } + + return; + } + + for (; i < source.Length; i++) + { + Unsafe.Add(ref sourceBase, (uint)i) = HalfVector4P.Reassociate(Unsafe.Add(ref sourceBase, (uint)i)); + } + } + + /// + /// Converts an unassociated scaled vector to associated scaled components. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Associate(Vector128 source) + { + source = ClampUnit(source); + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 storedAlpha = QuantizeScaledAlpha(alpha); + Vector128 result = source * storedAlpha; + return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); + } + + /// + /// Converts unassociated scaled vectors to associated scaled components. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Associate(Vector256 source) + { + source = ClampUnit(source); + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 storedAlpha = QuantizeScaledAlpha(alpha); + Vector256 result = source * storedAlpha; + return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); + } + + /// + /// Converts unassociated scaled vectors to associated scaled components. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Associate(Vector512 source) + { + source = ClampUnit(source); + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 storedAlpha = QuantizeScaledAlpha(alpha); + Vector512 result = source * storedAlpha; + 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, storedAlpha, result); + } + + /// + /// Reassociates an associated scaled vector after alpha quantization. + /// + /// The associated vectors. + /// The reassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Reassociate(Vector128 source) + { + Vector128 zero = Vector128.Zero; + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 storedAlpha = QuantizeScaledAlpha(alpha); + Vector128 result = source * (storedAlpha / alpha); + result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); + result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Reassociates associated scaled vectors after alpha quantization. + /// + /// The associated vectors. + /// The reassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Reassociate(Vector256 source) + { + Vector256 zero = Vector256.Zero; + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 storedAlpha = QuantizeScaledAlpha(alpha); + Vector256 result = source * (storedAlpha / alpha); + result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); + result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Reassociates associated scaled vectors after alpha quantization. + /// + /// The associated vectors. + /// The reassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Reassociate(Vector512 source) + { + Vector512 zero = Vector512.Zero; + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 storedAlpha = QuantizeScaledAlpha(alpha); + Vector512 result = source * (storedAlpha / alpha); + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Quantizes scaled alpha through the native binary16 representation. + /// + /// The scaled alpha lanes. + /// The scaled alpha values represented by binary16 storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 QuantizeScaledAlpha(Vector128 alpha) + { + Vector128 native = (ClampUnit(alpha) * Vector128.Create(HalfTypeHelper.FiniteRange)) + Vector128.Create(HalfTypeHelper.FiniteMinimum); + return (HalfTypeHelper.RoundToHalf(native) * Vector128.Create(HalfTypeHelper.InverseFiniteRange)) + Vector128.Create(HalfTypeHelper.ScaledMidpoint); + } + + /// + /// Quantizes scaled alpha through the native binary16 representation. + /// + /// The scaled alpha lanes. + /// The scaled alpha values represented by binary16 storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 QuantizeScaledAlpha(Vector256 alpha) + { + Vector256 native = (ClampUnit(alpha) * Vector256.Create(HalfTypeHelper.FiniteRange)) + Vector256.Create(HalfTypeHelper.FiniteMinimum); + return (HalfTypeHelper.RoundToHalf(native) * Vector256.Create(HalfTypeHelper.InverseFiniteRange)) + Vector256.Create(HalfTypeHelper.ScaledMidpoint); + } + + /// + /// Quantizes scaled alpha through the native binary16 representation. + /// + /// The scaled alpha lanes. + /// The scaled alpha values represented by binary16 storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 QuantizeScaledAlpha(Vector512 alpha) + { + Vector512 native = (ClampUnit(alpha) * Vector512.Create(HalfTypeHelper.FiniteRange)) + Vector512.Create(HalfTypeHelper.FiniteMinimum); + return (HalfTypeHelper.RoundToHalf(native) * Vector512.Create(HalfTypeHelper.InverseFiniteRange)) + Vector512.Create(HalfTypeHelper.ScaledMidpoint); + } + + /// + /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ClampUnit(Vector128 source) + { + Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One); + return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source); + } + + /// + /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ClampUnit(Vector256 source) + { + Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One); + return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source); + } + + /// + /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ClampUnit(Vector512 source) + { + Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One); + return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source); + } + + /// + /// Maps associated scaled vectors to native components and packs them as binary16 values. + /// + /// The associated scaled vectors. + /// The destination pixels. + private static void PackAssociatedScaled(Span source, Span destination) + { + Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination)); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs index c9714c217..b79d2a5b2 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +13,14 @@ public partial struct L16 /// /// Provides optimized overrides for bulk operations. /// - internal partial class PixelOperations : PixelOperations; + internal partial class PixelOperations : PixelOperations + { + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L8.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L8.PixelOperations.cs index e7b463fe0..fb7945fdf 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L8.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L8.PixelOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +13,14 @@ public partial struct L8 /// /// Provides optimized overrides for bulk operations. /// - internal partial class PixelOperations : PixelOperations; + internal partial class PixelOperations : PixelOperations + { + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte2.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte2.PixelOperations.cs index 36f4a0bf5..5568fc742 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte2.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte2.PixelOperations.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +14,25 @@ public partial struct NormalizedByte2 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + private static readonly Vector4 NativeOffset = new(1F, 1F, 0F, 0F); + private static readonly Vector4 NativeDivisor = new(2F, 2F, 1F, 1F); + + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // Only X and Y use affine native coordinates. Preserve W as normalized alpha for the scaled unassociation step. + Vector4Converters.AddThenDivide(source, NativeOffset, NativeDivisor); + this.FromAssociatedScaledVector4Destructive(configuration, source, destination); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4.PixelOperations.cs index e67321e4f..d467b802e 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4.PixelOperations.cs @@ -1,6 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +15,89 @@ public partial struct NormalizedByte4 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + private static readonly Vector4 NativeScale = new(2F); + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + NormalizedByte4P.PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination[..source.Length], false); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // The packed P and non-P layouts are identical, so the shared SIMD unpacker supplies normalized values. Association + // happens in [0, 1] before the result is mapped back to the signed-native [-1, 1] coordinate system. + NormalizedByte4P.PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination, true); + Numerics.Premultiply(destination); + Vector4Converters.MultiplyThenAdd(destination, NativeScale, -Vector4.One); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + NormalizedByte4P.PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination[..source.Length], true); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + NormalizedByte4P.PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination, true); + Numerics.Premultiply(destination); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + NormalizedByte4P.PixelOperations.Pack(source, MemoryMarshal.Cast(destination), false); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Convert all native components together so alpha and RGB enter unassociation in the same normalized coordinate system. + Vector4Converters.AddThenDivide(source, Vector4.One, NativeScale); + Numerics.UnPremultiply(source); + NormalizedByte4P.PixelOperations.Pack(source, MemoryMarshal.Cast(destination), true); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + NormalizedByte4P.PixelOperations.Pack(source, MemoryMarshal.Cast(destination), true); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + Numerics.UnPremultiply(source); + NormalizedByte4P.PixelOperations.Pack(source, MemoryMarshal.Cast(destination), true); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs new file mode 100644 index 000000000..de2f8f17b --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedByte4P.PixelOperations.cs @@ -0,0 +1,814 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct NormalizedByte4P +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + private const byte RestorePackedPixelOrder = 0b_11_01_10_00; + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ToUnassociatedVector4(source, destination[..source.Length], false); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ToVector4(source, destination[..source.Length], false); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + Associate(source, false); + Pack(source, destination, true); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + Reassociate(source, false); + Pack(source, destination, true); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ToUnassociatedVector4(source, destination[..source.Length], true); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ToVector4(source, destination[..source.Length], true); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + Associate(source, true); + Pack(source, destination, true); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + Reassociate(source, true); + Pack(source, destination, true); + } + + /// + /// Converts packed signed-normalized pixels to native or scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + /// Whether to produce scaled vectors. + internal static void ToVector4(ReadOnlySpan source, Span destination, bool scaled) + { + ref byte sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + + // Each packed component occupies one byte, so the destination Vector4 lane count also defines the source byte stride. + int componentsPerPixel = Vector128.Count; + int i = 0; + + // Portable widening advances one integer width at a time, so assemble the wider vectors from ordered 128-bit halves. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + + for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)).AsSByte(); + Vector128 lowerShorts = Vector128.WidenLower(packed); + Vector128 upperShorts = Vector128.WidenUpper(packed); + Vector256 lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts)); + Vector256 upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts)); + Vector512 integers = Vector512.Create(lowerIntegers, upperIntegers); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Vector512.ConvertToSingle(integers), scaled); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + + for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel))); + Vector128 shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsSByte()); + Vector256 integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToVector4(Vector256.ConvertToSingle(integers), scaled); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i < source.Length; i++) + { + uint packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel))); + Vector128 shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsSByte()); + Vector128 integers = Vector128.WidenLower(shorts); + Unsafe.Add(ref destinationBase, (uint)i) = ToVector4(Vector128.ConvertToSingle(integers), scaled).AsVector4(); + } + + return; + } + + ref NormalizedByte4P pixelBase = ref Unsafe.As(ref sourceBase); + + for (; i < source.Length; i++) + { + NormalizedByte4P pixel = Unsafe.Add(ref pixelBase, (uint)i); + Unsafe.Add(ref destinationBase, (uint)i) = scaled ? pixel.ToScaledVector4() : pixel.ToVector4(); + } + } + + /// + /// Converts signed-normalized storage to native or scaled vectors. + /// + /// The signed storage components. + /// Whether to produce scaled vectors. + /// The converted vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ToVector4(Vector512 source, bool scaled) + { + // Both minimum two's-complement SNORM encodings represent -1. + source = Vector512.Max(source, Vector512.Create(-MaxPos)); + + if (scaled) + { + // Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound. + return (source + Vector512.Create(MaxPos)) / Vector512.Create(ScaledMagnitude); + } + + return source / Vector512.Create(MaxPos); + } + + /// + /// Converts signed-normalized storage to native or scaled vectors. + /// + /// The signed storage components. + /// Whether to produce scaled vectors. + /// The converted vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ToVector4(Vector256 source, bool scaled) + { + // Both minimum two's-complement SNORM encodings represent -1. + source = Vector256.Max(source, Vector256.Create(-MaxPos)); + + if (scaled) + { + // Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound. + return (source + Vector256.Create(MaxPos)) / Vector256.Create(ScaledMagnitude); + } + + return source / Vector256.Create(MaxPos); + } + + /// + /// Converts signed-normalized storage to native or scaled vectors. + /// + /// The signed storage components. + /// Whether to produce scaled vectors. + /// The converted vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ToVector4(Vector128 source, bool scaled) + { + // Both minimum two's-complement SNORM encodings represent -1. + source = Vector128.Max(source, Vector128.Create(-MaxPos)); + + if (scaled) + { + // Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound. + return (source + Vector128.Create(MaxPos)) / Vector128.Create(ScaledMagnitude); + } + + return source / Vector128.Create(MaxPos); + } + + /// + /// Converts packed associated pixels directly to unassociated native or scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + /// Whether to produce scaled vectors. + private static void ToUnassociatedVector4(ReadOnlySpan source, Span destination, bool scaled) + { + ref byte sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + + // Each packed component occupies one byte, so the destination Vector4 lane count also defines the source byte stride. + int componentsPerPixel = Vector128.Count; + int i = 0; + + // Portable widening advances one integer width at a time, so assemble the wider vectors from ordered 128-bit halves. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + + for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)).AsSByte(); + Vector128 lowerShorts = Vector128.WidenLower(packed); + Vector128 upperShorts = Vector128.WidenUpper(packed); + Vector256 lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts)); + Vector256 upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts)); + Vector512 integers = Vector512.Create(lowerIntegers, upperIntegers); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector512.ConvertToSingle(integers), scaled); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + + for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel))); + Vector128 shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsSByte()); + Vector256 integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector256.ConvertToSingle(integers), scaled); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i < source.Length; i++) + { + uint packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel))); + Vector128 shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsSByte()); + Vector128 integers = Vector128.WidenLower(shorts); + Unsafe.Add(ref destinationBase, (uint)i) = ToUnassociatedVector4(Vector128.ConvertToSingle(integers), scaled).AsVector4(); + } + + return; + } + + ref NormalizedByte4P pixelBase = ref Unsafe.As(ref sourceBase); + + for (; i < source.Length; i++) + { + NormalizedByte4P pixel = Unsafe.Add(ref pixelBase, (uint)i); + Unsafe.Add(ref destinationBase, (uint)i) = scaled ? pixel.ToUnassociatedScaledVector4() : pixel.ToUnassociatedVector4(); + } + } + + /// + /// Converts four associated signed-byte vectors to unassociated native or scaled vectors. + /// + /// The signed storage components. + /// Whether to produce scaled vectors. + /// The unassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ToUnassociatedVector4(Vector512 source, bool scaled) + { + // Clamp the duplicate SNORM minimum encoding before converting it to the nonnegative associated domain. + source = Vector512.Max(source, Vector512.Create(-MaxPos)); + source += Vector512.Create(MaxPos); + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + Vector512 normalizeMask = Vector512.Equals(alpha, Vector512.Zero) | alphaMask; + Vector512 divisor = Vector512.ConditionalSelect(normalizeMask, Vector512.Create(ScaledMagnitude), alpha); + Vector512 result = source / divisor; + + // Unassociation is performed in scaled space; map the result back only after alpha has served as a true opacity value. + return scaled ? result : (result * Vector512.Create(2F)) - Vector512.One; + } + + /// + /// Converts two associated signed-byte vectors to unassociated native or scaled vectors. + /// + /// The signed storage components. + /// Whether to produce scaled vectors. + /// The unassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ToUnassociatedVector4(Vector256 source, bool scaled) + { + // Clamp the duplicate SNORM minimum encoding before converting it to the nonnegative associated domain. + source = Vector256.Max(source, Vector256.Create(-MaxPos)); + source += Vector256.Create(MaxPos); + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + Vector256 normalizeMask = Vector256.Equals(alpha, Vector256.Zero) | alphaMask; + Vector256 divisor = Vector256.ConditionalSelect(normalizeMask, Vector256.Create(ScaledMagnitude), alpha); + Vector256 result = source / divisor; + + // Unassociation is performed in scaled space; map the result back only after alpha has served as a true opacity value. + return scaled ? result : (result * Vector256.Create(2F)) - Vector256.One; + } + + /// + /// Converts an associated signed-byte vector to an unassociated native or scaled vector. + /// + /// The signed storage components. + /// Whether to produce a scaled vector. + /// The unassociated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ToUnassociatedVector4(Vector128 source, bool scaled) + { + // Clamp the duplicate SNORM minimum encoding before converting it to the nonnegative associated domain. + source = Vector128.Max(source, Vector128.Create(-MaxPos)); + source += Vector128.Create(MaxPos); + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); + Vector128 normalizeMask = Vector128.Equals(alpha, Vector128.Zero) | alphaMask; + Vector128 divisor = Vector128.ConditionalSelect(normalizeMask, Vector128.Create(ScaledMagnitude), alpha); + Vector128 result = source / divisor; + + // Unassociation is performed in scaled space; map the result back only after alpha has served as a true opacity value. + return scaled ? result : (result * Vector128.Create(2F)) - Vector128.One; + } + + /// + /// Converts unassociated native or scaled vectors to the destination's associated scaled representation. + /// + /// The vectors to convert in place. + /// Whether the source contains scaled vectors. + private static void Associate(Span source, bool scaled) + { + // Quantize alpha through signed-normalized destination storage before associating RGB. The kernels replace W separately + // so alpha is stored unchanged instead of being multiplied by itself. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector512 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length / (uint)pixelsPerVector); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = Associate(sourceBase, scaled); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + // SIMD widths are powers of two, so masking finds the start of the unprocessed remainder without division. + source = source[(source.Length & ~(pixelsPerVector - 1))..]; + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length / (uint)pixelsPerVector); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = Associate(sourceBase, scaled); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + // SIMD widths are powers of two, so masking finds the start of the unprocessed remainder without division. + source = source[(source.Length & ~(pixelsPerVector - 1))..]; + } + + if (Vector128.IsHardwareAccelerated) + { + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector128 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = Associate(sourceBase, scaled); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + return; + } + + ref Vector4 tailBase = ref MemoryMarshal.GetReference(source); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Vector4 vector = Unsafe.Add(ref tailBase, i); + + if (!scaled) + { + // Signed-native W is an affine encoding rather than opacity, so association must happen after mapping to scaled space. + vector += Vector4.One; + vector /= 2F; + } + + // Qualify the containing pixel type because this nested class also declares SIMD helpers named Associate. + Unsafe.Add(ref tailBase, i) = NormalizedByte4P.Associate(vector); + } + } + + /// + /// Converts four unassociated native or scaled vectors to the destination's associated scaled representation. + /// + /// The unassociated vectors. + /// Whether the source contains scaled vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Associate(Vector512 source, bool scaled) + { + Vector512 zero = Vector512.Zero; + Vector512 one = Vector512.One; + + if (!scaled) + { + // Signed-native W is an affine encoding rather than opacity, so association must happen after mapping to scaled space. + source = (source + one) / Vector512.Create(2F); + } + + source = Vector512.Min(Vector512.Max(source, zero), one); + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 nativeAlpha = Vector512.Min(Vector512.Max((alpha * Vector512.Create(2F)) - one, -one), one); + Vector512 storedAlpha = Vector512_.RoundToNearestInteger(nativeAlpha * Vector512.Create(MaxPos)); + storedAlpha += Vector512.Create(MaxPos); + storedAlpha /= Vector512.Create(ScaledMagnitude); + Vector512 result = source * storedAlpha; + 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, storedAlpha, result); + } + + /// + /// Converts two unassociated native or scaled vectors to the destination's associated scaled representation. + /// + /// The unassociated vectors. + /// Whether the source contains scaled vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Associate(Vector256 source, bool scaled) + { + Vector256 zero = Vector256.Zero; + Vector256 one = Vector256.One; + + if (!scaled) + { + // Signed-native W is an affine encoding rather than opacity, so association must happen after mapping to scaled space. + source = (source + one) / Vector256.Create(2F); + } + + source = Vector256.Min(Vector256.Max(source, zero), one); + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 nativeAlpha = Vector256.Min(Vector256.Max((alpha * Vector256.Create(2F)) - one, -one), one); + Vector256 storedAlpha = Vector256_.RoundToNearestInteger(nativeAlpha * Vector256.Create(MaxPos)); + storedAlpha += Vector256.Create(MaxPos); + storedAlpha /= Vector256.Create(ScaledMagnitude); + Vector256 result = source * storedAlpha; + Vector256 alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + return Vector256.ConditionalSelect(alphaMask, storedAlpha, result); + } + + /// + /// Converts an unassociated native or scaled vector to the destination's associated scaled representation. + /// + /// The unassociated vector. + /// Whether the source contains a scaled vector. + /// The associated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Associate(Vector128 source, bool scaled) + { + Vector128 zero = Vector128.Zero; + Vector128 one = Vector128.One; + + if (!scaled) + { + // Signed-native W is an affine encoding rather than opacity, so association must happen after mapping to scaled space. + source = (source + one) / Vector128.Create(2F); + } + + source = Vector128.Min(Vector128.Max(source, zero), one); + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 nativeAlpha = Vector128.Min(Vector128.Max((alpha * Vector128.Create(2F)) - one, -one), one); + Vector128 storedAlpha = Vector128_.RoundToNearestInteger(nativeAlpha * Vector128.Create(MaxPos)); + storedAlpha += Vector128.Create(MaxPos); + storedAlpha /= Vector128.Create(ScaledMagnitude); + Vector128 result = source * storedAlpha; + Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); + return Vector128.ConditionalSelect(alphaMask, storedAlpha, result); + } + + /// + /// Reassociates native or scaled vectors with the scaled alpha values the destination stores. + /// + /// The vectors to convert in place. + /// Whether the source contains scaled vectors. + private static void Reassociate(Span source, bool scaled) + { + // Preserve straight color across destination-alpha quantization by scaling RGB by storedAlpha / inputAlpha. The kernels + // replace W, clamp RGB to stored alpha, and clear zero-alpha vectors so the stored result remains valid associated color. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector512 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length / (uint)pixelsPerVector); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = Reassociate(sourceBase, scaled); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + // SIMD widths are powers of two, so masking finds the start of the unprocessed remainder without division. + source = source[(source.Length & ~(pixelsPerVector - 1))..]; + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length / (uint)pixelsPerVector); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = Reassociate(sourceBase, scaled); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + // SIMD widths are powers of two, so masking finds the start of the unprocessed remainder without division. + source = source[(source.Length & ~(pixelsPerVector - 1))..]; + } + + if (Vector128.IsHardwareAccelerated) + { + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector128 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = Reassociate(sourceBase, scaled); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + return; + } + + ref Vector4 tailBase = ref MemoryMarshal.GetReference(source); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Vector4 vector = Unsafe.Add(ref tailBase, i); + + if (!scaled) + { + // Convert native components together so RGB and alpha enter reassociation in the same scaled coordinate system. + vector += Vector4.One; + vector /= 2F; + } + + // Qualify the containing pixel type because this nested class also declares SIMD helpers named Reassociate. + Unsafe.Add(ref tailBase, i) = NormalizedByte4P.Reassociate(vector); + } + } + + /// + /// Reassociates four native or scaled vectors with the scaled alpha values the destination stores. + /// + /// The associated vectors. + /// Whether the source contains scaled vectors. + /// The reassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Reassociate(Vector512 source, bool scaled) + { + Vector512 zero = Vector512.Zero; + Vector512 one = Vector512.One; + + if (!scaled) + { + // Convert native components together so RGB and alpha enter reassociation in the same scaled coordinate system. + source = (source + one) / Vector512.Create(2F); + } + + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 nativeAlpha = Vector512.Min(Vector512.Max((alpha * Vector512.Create(2F)) - one, -one), one); + Vector512 storedAlpha = Vector512_.RoundToNearestInteger(nativeAlpha * Vector512.Create(MaxPos)); + storedAlpha += Vector512.Create(MaxPos); + storedAlpha /= Vector512.Create(ScaledMagnitude); + Vector512 result = source * (storedAlpha / alpha); + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Reassociates two native or scaled vectors with the scaled alpha values the destination stores. + /// + /// The associated vectors. + /// Whether the source contains scaled vectors. + /// The reassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Reassociate(Vector256 source, bool scaled) + { + Vector256 zero = Vector256.Zero; + Vector256 one = Vector256.One; + + if (!scaled) + { + // Convert native components together so RGB and alpha enter reassociation in the same scaled coordinate system. + source = (source + one) / Vector256.Create(2F); + } + + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 nativeAlpha = Vector256.Min(Vector256.Max((alpha * Vector256.Create(2F)) - one, -one), one); + Vector256 storedAlpha = Vector256_.RoundToNearestInteger(nativeAlpha * Vector256.Create(MaxPos)); + storedAlpha += Vector256.Create(MaxPos); + storedAlpha /= Vector256.Create(ScaledMagnitude); + Vector256 result = source * (storedAlpha / alpha); + Vector256 alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + result = Vector256.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Reassociates a native or scaled vector with the scaled alpha value the destination stores. + /// + /// The associated vector. + /// Whether the source contains a scaled vector. + /// The reassociated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Reassociate(Vector128 source, bool scaled) + { + Vector128 zero = Vector128.Zero; + Vector128 one = Vector128.One; + + if (!scaled) + { + // Convert native components together so RGB and alpha enter reassociation in the same scaled coordinate system. + source = (source + one) / Vector128.Create(2F); + } + + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 nativeAlpha = Vector128.Min(Vector128.Max((alpha * Vector128.Create(2F)) - one, -one), one); + Vector128 storedAlpha = Vector128_.RoundToNearestInteger(nativeAlpha * Vector128.Create(MaxPos)); + storedAlpha += Vector128.Create(MaxPos); + storedAlpha /= Vector128.Create(ScaledMagnitude); + Vector128 result = source * (storedAlpha / alpha); + Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); + result = Vector128.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Packs native or scaled vectors into signed-normalized storage. + /// + /// The source vectors. + /// The destination pixels. + /// Whether the source contains scaled vectors. + internal static void Pack(Span source, Span destination, bool scaled) + { + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source); + ref NormalizedByte4P destinationBase = ref MemoryMarshal.GetReference(destination); + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + + for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) + { + Vector512 vector = Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + Vector512 integers = ConvertToPackedInt32(vector, scaled); + Vector256 shorts = Vector256_.PackSignedSaturate(integers.GetLower(), integers.GetUpper()); + Vector128 packed = Vector128_.PackSignedSaturate(shorts.GetLower(), shorts.GetUpper()); + + if (Avx2.IsSupported) + { + // AVX2 narrows each 128-bit lane independently, placing the middle two pixels out of order. + packed = Vector128_.ShuffleNative(packed.AsSingle(), RestorePackedPixelOrder).AsSByte(); + } + + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = packed; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + + for (; i <= source.Length - pixelsPerVector; i += pixelsPerVector) + { + Vector256 vector = Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + Vector256 integers = ConvertToPackedInt32(vector, scaled); + Vector128 shorts = Vector128_.PackSignedSaturate(integers.GetLower(), integers.GetUpper()); + Vector128 packed = Vector128_.PackSignedSaturate(shorts, shorts); + Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)) = packed.AsUInt64().GetElement(0); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i < source.Length; i++) + { + Vector128 vector = Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)i)); + Vector128 integers = ConvertToPackedInt32(vector, scaled); + Vector128 shorts = Vector128_.PackSignedSaturate(integers, integers); + Vector128 packed = Vector128_.PackSignedSaturate(shorts, shorts); + Unsafe.Add(ref destinationBase, (uint)i).PackedValue = packed.AsUInt32().GetElement(0); + } + + return; + } + + for (; i < source.Length; i++) + { + Vector4 vector = Unsafe.Add(ref sourceBase, (uint)i); + Unsafe.Add(ref destinationBase, (uint)i) = scaled ? FromScaledVector4(vector) : FromVector4(vector); + } + } + + /// + /// Converts four vectors to signed integers using the destination's packing contract. + /// + /// The source vectors. + /// Whether the source contains scaled vectors. + /// The packed integer components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ConvertToPackedInt32(Vector512 source, bool scaled) + { + Vector512 one = Vector512.One; + + if (scaled) + { + source *= Vector512.Create(2F); + source -= one; + } + + source = Vector512.Min(Vector512.Max(source, -one), one) * Vector512.Create(MaxPos); + return Vector512_.ConvertToInt32RoundToEven(source); + } + + /// + /// Converts two vectors to signed integers using the destination's packing contract. + /// + /// The source vectors. + /// Whether the source contains scaled vectors. + /// The packed integer components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConvertToPackedInt32(Vector256 source, bool scaled) + { + Vector256 one = Vector256.One; + + if (scaled) + { + source *= Vector256.Create(2F); + source -= one; + } + + source = Vector256.Min(Vector256.Max(source, -one), one) * Vector256.Create(MaxPos); + return Vector256_.ConvertToInt32RoundToEven(source); + } + + /// + /// Converts a vector to signed integers using the destination's packing contract. + /// + /// The source vector. + /// Whether the source contains a scaled vector. + /// The packed integer components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ConvertToPackedInt32(Vector128 source, bool scaled) + { + Vector128 one = Vector128.One; + + if (scaled) + { + source *= Vector128.Create(2F); + source -= one; + } + + source = Vector128.Min(Vector128.Max(source, -one), one) * Vector128.Create(MaxPos); + return Vector128_.ConvertToInt32RoundToEven(source); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort2.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort2.PixelOperations.cs index 99636cb37..f35f2a2da 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort2.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort2.PixelOperations.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +14,25 @@ public partial struct NormalizedShort2 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + private static readonly Vector4 NativeOffset = new(1F, 1F, 0F, 0F); + private static readonly Vector4 NativeDivisor = new(2F, 2F, 1F, 1F); + + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // Only X and Y use affine native coordinates. Preserve W as normalized alpha for the scaled unassociation step. + Vector4Converters.AddThenDivide(source, NativeOffset, NativeDivisor); + this.FromAssociatedScaledVector4Destructive(configuration, source, destination); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort4.PixelOperations.cs index ad6aa8d8a..8a60d68b4 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/NormalizedShort4.PixelOperations.cs @@ -1,6 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +15,89 @@ public partial struct NormalizedShort4 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + private static readonly Vector4 NativeScale = new(2F); + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + SignedShort4PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination[..source.Length], true, false); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Association is defined in scaled color space, not around the signed-native zero point. The shared signed-short SIMD + // kernel handles unpacking before the result is mapped back to the format's native [-1, 1] range. + SignedShort4PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination, true, true); + Numerics.Premultiply(destination); + Vector4Converters.MultiplyThenAdd(destination, NativeScale, -Vector4.One); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + SignedShort4PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination[..source.Length], true, true); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + SignedShort4PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination, true, true); + Numerics.Premultiply(destination); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + SignedShort4PixelOperations.FromVector4(source, MemoryMarshal.Cast(destination), true, false); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Convert native RGB and alpha together before unassociation so the subsequent scaled pack retains the scalar contract. + Vector4Converters.AddThenDivide(source, Vector4.One, NativeScale); + Numerics.UnPremultiply(source); + SignedShort4PixelOperations.FromVector4(source, MemoryMarshal.Cast(destination), true, true); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + SignedShort4PixelOperations.FromVector4(source, MemoryMarshal.Cast(destination), true, true); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + Numerics.UnPremultiply(source); + SignedShort4PixelOperations.FromVector4(source, MemoryMarshal.Cast(destination), true, true); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rg32.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rg32.PixelOperations.cs index 7bca1c781..5be973404 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rg32.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rg32.PixelOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +13,14 @@ public partial struct Rg32 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb48.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb48.PixelOperations.cs index c9ed730c4..31816ceb0 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb48.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb48.PixelOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +13,14 @@ public partial struct Rgb48 /// /// Provides optimized overrides for bulk operations. /// - internal partial class PixelOperations : PixelOperations; + internal partial class PixelOperations : PixelOperations + { + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb96.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb96.PixelOperations.cs new file mode 100644 index 000000000..990fd64bd --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb96.PixelOperations.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// The alpha component is always fully opaque. +/// +public partial struct Rgb96 +{ + /// + /// Provides optimized overrides for bulk operations. + /// + internal class PixelOperations : PixelOperations + { + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba32P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba32P.PixelOperations.cs new file mode 100644 index 000000000..19338f12f --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgba32P.PixelOperations.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct Rgba32P +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(source, destination); + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToAssociatedVector4(source, destination); + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // Native and scaled vectors have the same range for this format, so the byte-specialized converter is valid for both contracts. + Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4(source, destination); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // The converter rescales RGB when alpha rounds so the channels remain associated with the byte alpha actually stored. + Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4(source, destination); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + // This byte format has the same normalized native and scaled ranges, so the unmodified converter satisfies both contracts. + Vector4Converters.AssociatedRgbaCompatible.ToAssociatedVector4(source, destination); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4(source, destination); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4(source, destination); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs new file mode 100644 index 000000000..f3c01e082 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalf.PixelOperations.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct RgbaHalf +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : PixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // RgbaHalf and RgbaHalfP have the same four-half layout. Sharing the binary16 expansion kernel keeps this path + // vectorized without changing the unassociated meaning of the source components. + RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination[..source.Length]); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // Association is fused with half expansion so the conversion reads and writes each span once. + RgbaHalfP.PixelOperations.UnpackAssociated(MemoryMarshal.Cast(source), destination[..source.Length]); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToAssociatedVector4(configuration, source, destination); + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // The layouts are identical, so the shared packer can write RgbaHalf storage without an intermediate buffer. + RgbaHalfP.PixelOperations.Pack(source, MemoryMarshal.Cast(destination[..source.Length])); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + // Unassociation is fused with binary16 packing so processors do not pay for another pass over their vector buffer. + RgbaHalfP.PixelOperations.PackFromAssociated(source, MemoryMarshal.Cast(destination[..source.Length])); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromUnassociatedVector4Destructive(configuration, source, destination); + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromAssociatedVector4Destructive(configuration, source, destination); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs new file mode 100644 index 000000000..825ebeec9 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs @@ -0,0 +1,795 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Provides optimized overrides for bulk operations. +/// +public partial struct RgbaHalfP +{ + /// + /// Provides optimized bulk operations for . + /// + internal class PixelOperations : AssociatedAlphaPixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + UnpackUnassociated(source, destination[..source.Length]); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Unpack(source, destination[..source.Length]); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => this.ToAssociatedVector4(configuration, source, destination); + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + PackUnassociated(source, destination[..source.Length]); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + PackAssociated(source, destination[..source.Length]); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromUnassociatedVector4Destructive(configuration, source, destination); + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + => this.FromAssociatedVector4Destructive(configuration, source, destination); + + /// + /// Expands binary16 components without changing their unit-range representation. + /// + /// The packed source pixels. + /// The destination vectors. + internal static void Unpack(ReadOnlySpan source, Span destination) + { + ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); + Vector512.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector512.Count)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); + Vector256.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); + Vector128.StoreUnsafe(lower, ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(upper, ref destinationBase, (nuint)(i + Vector128.Count)); + } + + if (i < componentCount) + { + // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. + ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); + Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); + Vector128.StoreUnsafe(HalfTypeHelper.Unpack(packed).Lower, ref destinationBase, (nuint)i); + } + + return; + } + + ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); + ref Vector4 vectorBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Unsafe.Add(ref vectorBase, (uint)pixelIndex) = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToVector4(); + } + } + + /// + /// Expands unassociated binary16 components and associates RGB in the same pass. + /// + /// The packed source pixels. + /// The destination vectors. + internal static void UnpackAssociated(ReadOnlySpan source, Span destination) + { + ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); + Vector512.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector512.Count)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); + Vector256.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); + Vector128.StoreUnsafe(Associate(lower), ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(Associate(upper), ref destinationBase, (nuint)(i + Vector128.Count)); + } + + if (i < componentCount) + { + // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. + ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); + Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); + Vector128.StoreUnsafe(Associate(HalfTypeHelper.Unpack(packed).Lower), ref destinationBase, (nuint)i); + } + + return; + } + + ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); + ref Vector4 vectorBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Vector4 vector = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToVector4(); + Numerics.Premultiply(ref vector); + Unsafe.Add(ref vectorBase, (uint)pixelIndex) = vector; + } + } + + /// + /// Expands associated binary16 components and unassociates RGB in the same pass. + /// + /// The packed source pixels. + /// The destination vectors. + internal static void UnpackUnassociated(ReadOnlySpan source, Span destination) + { + ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref float destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector512 lower, Vector512 upper) = HalfTypeHelper.Unpack(packed); + Vector512.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector512.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector512.Count)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector256 lower, Vector256 upper) = HalfTypeHelper.Unpack(packed); + Vector256.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector256.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector128 lower, Vector128 upper) = HalfTypeHelper.Unpack(packed); + Vector128.StoreUnsafe(Unassociate(lower), ref destinationBase, (nuint)i); + Vector128.StoreUnsafe(Unassociate(upper), ref destinationBase, (nuint)(i + Vector128.Count)); + } + + if (i < componentCount) + { + // Four binary16 components form one pixel, so the only possible remainder is one complete pixel. + ulong remainder = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i))); + Vector128 packed = Vector128.CreateScalarUnsafe(remainder).AsUInt16(); + Vector128.StoreUnsafe(Unassociate(HalfTypeHelper.Unpack(packed).Lower), ref destinationBase, (nuint)i); + } + + return; + } + + ref RgbaHalfP pixelBase = ref Unsafe.As(ref sourceBase); + ref Vector4 vectorBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Unsafe.Add(ref vectorBase, (uint)pixelIndex) = Unsafe.Add(ref pixelBase, (uint)pixelIndex).ToUnassociatedVector4(); + } + } + + /// + /// Packs unassociated unit-range vectors directly into binary16 storage. + /// + /// The source vectors. + /// The destination pixels. + internal static void Pack(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 lower = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512 upper = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256 upper = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 upper = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); + Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + + if (i < componentCount) + { + // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. + Vector128 vector = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Vector4 vector = Numerics.Clamp(Unsafe.Add(ref vectorBase, (uint)pixelIndex), Vector4.Zero, Vector4.One); + Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W); + } + } + + /// + /// Packs vectors directly into IEEE 754 binary16 storage without applying unit-range color constraints. + /// + /// The source vectors. + /// The destination pixels. + internal static void PackUnclamped(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 lower = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + Vector512 upper = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + Vector256 upper = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + Vector128 upper = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)); + Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + + if (i < componentCount) + { + // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. + Vector128 vector = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex); + Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W); + } + } + + /// + /// Unassociates vectors and packs unassociated unit-range binary16 storage in the same pass. + /// + /// The associated source vectors. + /// The destination pixels. + internal static void PackFromAssociated(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 lower = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector512 upper = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)))); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector256 upper = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)))); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector128 upper = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)))); + Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + + if (i < componentCount) + { + // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. + Vector128 vector = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Vector4 vector = Unsafe.Add(ref vectorBase, (uint)pixelIndex); + Numerics.UnPremultiply(ref vector); + vector = Numerics.Clamp(vector, Vector4.Zero, Vector4.One); + Unsafe.Add(ref pixelBase, (uint)pixelIndex) = new RgbaHalfP(vector.X, vector.Y, vector.Z, vector.W); + } + } + + /// + /// Associates unassociated vectors with their stored binary16 alpha and packs them in one pass. + /// + /// The unassociated source vectors. + /// The destination pixels. + internal static void PackUnassociated(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + // Alpha is rounded to the exact binary16 value that will be stored before RGB is associated with it. + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 lower = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512 upper = AssociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256 upper = AssociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 upper = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); + Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + + if (i < componentCount) + { + // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. + Vector128 vector = AssociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Unsafe.Add(ref pixelBase, (uint)pixelIndex) = RgbaHalfP.FromUnassociatedVector4(Unsafe.Add(ref vectorBase, (uint)pixelIndex)); + } + } + + /// + /// Reassociates vectors with their stored binary16 alpha and packs them in one pass. + /// + /// The associated source vectors. + /// The destination pixels. + internal static void PackAssociated(Span source, Span destination) + { + ref float sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref ushort destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + int componentCount = source.Length * Vector128.Count; + int i = 0; + + // Scaling RGB by storedAlpha / inputAlpha preserves straight color when binary16 rounds the alpha channel. + if (Vector512.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector512.Count; i += Vector512.Count) + { + Vector512 lower = ReassociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector512 upper = ReassociateForStorage(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector256.Count; i += Vector256.Count) + { + Vector256 lower = ReassociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector256 upper = ReassociateForStorage(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= componentCount - Vector128.Count; i += Vector128.Count) + { + Vector128 lower = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 upper = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); + Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); + } + + if (i < componentCount) + { + // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. + Vector128 vector = ReassociateForStorage(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 packed = HalfTypeHelper.Pack(vector, vector); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); + } + + return; + } + + ref Vector4 vectorBase = ref Unsafe.As(ref sourceBase); + ref RgbaHalfP pixelBase = ref Unsafe.As(ref destinationBase); + + for (int pixelIndex = 0; pixelIndex < source.Length; pixelIndex++) + { + Unsafe.Add(ref pixelBase, (uint)pixelIndex) = RgbaHalfP.FromAssociatedVector4(Unsafe.Add(ref vectorBase, (uint)pixelIndex)); + } + } + + /// + /// Associates RGB with alpha while preserving each alpha lane. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Associate(Vector128 source) + { + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 result = source * alpha; + return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), alpha, result); + } + + /// + /// Associates RGB with alpha while preserving each alpha lane. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Associate(Vector256 source) + { + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 result = source * alpha; + return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), alpha, result); + } + + /// + /// Associates RGB with alpha while preserving each alpha lane. + /// + /// The unassociated vectors. + /// The associated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Associate(Vector512 source) + { + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 result = source * alpha; + 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); + } + + /// + /// Unassociates RGB while preserving each alpha lane. + /// + /// The associated vectors. + /// The unassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Unassociate(Vector128 source) + { + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + return Numerics.UnPremultiply(source, alpha); + } + + /// + /// Unassociates RGB while preserving each alpha lane. + /// + /// The associated vectors. + /// The unassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Unassociate(Vector256 source) + { + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + return Numerics.UnPremultiply(source, alpha); + } + + /// + /// Unassociates RGB while preserving each alpha lane. + /// + /// The associated vectors. + /// The unassociated vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Unassociate(Vector512 source) + { + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + return Numerics.UnPremultiply(source, alpha); + } + + /// + /// Clamps vectors to the unit range represented by the pixel format. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ClampUnit(Vector128 source) + { + Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One); + + // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. + return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source); + } + + /// + /// Clamps vectors to the unit range represented by the pixel format. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ClampUnit(Vector256 source) + { + Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One); + + // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. + return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source); + } + + /// + /// Clamps vectors to the unit range represented by the pixel format. + /// + /// The vectors to clamp. + /// The clamped vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ClampUnit(Vector512 source) + { + Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One); + + // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. + return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source); + } + + /// + /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. + /// + /// The unassociated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AssociateForStorage(Vector128 source) + { + source = ClampUnit(source); + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); + Vector128 result = source * storedAlpha; + return Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); + } + + /// + /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. + /// + /// The unassociated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 AssociateForStorage(Vector256 source) + { + source = ClampUnit(source); + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); + Vector256 result = source * storedAlpha; + return Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); + } + + /// + /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. + /// + /// The unassociated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 AssociateForStorage(Vector512 source) + { + source = ClampUnit(source); + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); + Vector512 result = source * storedAlpha; + 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, storedAlpha, result); + } + + /// + /// Reassociates vectors with the alpha value binary16 storage can reproduce. + /// + /// The associated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ReassociateForStorage(Vector128 source) + { + Vector128 zero = Vector128.Zero; + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 clampedAlpha = ClampUnit(alpha); + Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); + Vector128 result = source * (storedAlpha / alpha); + result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); + result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Reassociates vectors with the alpha value binary16 storage can reproduce. + /// + /// The associated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ReassociateForStorage(Vector256 source) + { + Vector256 zero = Vector256.Zero; + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 clampedAlpha = ClampUnit(alpha); + Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); + Vector256 result = source * (storedAlpha / alpha); + result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); + result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); + } + + /// + /// Reassociates vectors with the alpha value binary16 storage can reproduce. + /// + /// The associated vectors. + /// The associated vectors ready for packing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ReassociateForStorage(Vector512 source) + { + Vector512 zero = Vector512.Zero; + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 clampedAlpha = ClampUnit(alpha); + Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); + Vector512 result = source * (storedAlpha / alpha); + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); + } + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs index 5f9b51af9..d4b868871 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs @@ -23,9 +23,16 @@ public partial struct RgbaVector ReadOnlySpan sourcePixels, Span destinationPixels) { + Guard.DestinationShouldNotBeTooShort(sourcePixels, destinationPixels, nameof(destinationPixels)); + + destinationPixels = destinationPixels[..sourcePixels.Length]; Span destinationVectors = MemoryMarshal.Cast(destinationPixels); - PixelOperations.Instance.ToVector4(configuration, sourcePixels, destinationVectors, PixelConversionModifiers.Scale); + // Cross-format conversion uses public dispatch so associated source pixels are unassociated before entering RgbaVector storage. + PixelOperations.Instance.ToVector4(configuration, sourcePixels, destinationVectors, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + // RgbaVector.FromScaledVector4 clamps scaled input, so the optimized bulk path must preserve that behavior after unassociating. + Numerics.Clamp(MemoryMarshal.Cast(destinationVectors), 0F, 1F); } /// @@ -38,7 +45,11 @@ public partial struct RgbaVector Guard.DestinationShouldNotBeTooShort(sourceVectors, destinationPixels, nameof(destinationPixels)); Vector4Converters.ApplyBackwardConversionModifiers(sourceVectors, modifiers); - MemoryMarshal.Cast(sourceVectors).CopyTo(destinationPixels); + + // RgbaVector.FromVector4 and FromScaledVector4 both clamp to the representable [0, 1] range. Preserve that scalar + // contract before the zero-copy representation cast so bulk processor output cannot retain HDR or negative values. + Numerics.Clamp(MemoryMarshal.Cast(sourceVectors), 0F, 1F); + MemoryMarshal.Cast(sourceVectors).CopyTo(destinationPixels[..sourceVectors.Length]); } /// @@ -50,6 +61,7 @@ public partial struct RgbaVector { Guard.DestinationShouldNotBeTooShort(sourcePixels, destinationVectors, nameof(destinationVectors)); + destinationVectors = destinationVectors[..sourcePixels.Length]; MemoryMarshal.Cast(sourcePixels).CopyTo(destinationVectors); Vector4Converters.ApplyForwardConversionModifiers(destinationVectors, modifiers); } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs index d8225f18a..4d6b066ad 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short2.PixelOperations.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +14,26 @@ public partial struct Short2 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + private static readonly Vector4 NativeOffset = new(-MinNeg, -MinNeg, 0F, 0F); + private static readonly Vector4 NativeRange = new(Range, Range, 1F, 1F); + + // Alpha is implicitly one, so both outward representations already contain associated color components. + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + // X and Y use signed-native coordinates while incoming W remains normalized alpha. Map only the stored components before + // the scaled bulk path unassociates RGB; transforming W would change the opacity that must be removed. + Vector4Converters.AddThenDivide(source, NativeOffset, NativeRange); + this.FromAssociatedScaledVector4Destructive(configuration, source, destination); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs index 3d7043b0c..83c3ce053 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Short4.PixelOperations.cs @@ -1,6 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats.Utils; + namespace SixLabors.ImageSharp.PixelFormats; /// @@ -11,5 +15,90 @@ public partial struct Short4 /// /// Provides optimized overrides for bulk operations. /// - internal class PixelOperations : PixelOperations; + internal class PixelOperations : PixelOperations + { + private static readonly Vector4 NativeOffset = new(-MinNeg); + private static readonly Vector4 NativeMagnitude = new(Range); + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + SignedShort4PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination[..source.Length], false, false); + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Signed integer components use an affine native range. Expand to scaled vectors, associate there, then restore the + // native zero point so RGB and alpha remain in the same coordinate system. + SignedShort4PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination, false, true); + Numerics.Premultiply(destination); + Vector4Converters.MultiplyThenAdd(destination, NativeMagnitude, -NativeOffset); + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + SignedShort4PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination[..source.Length], false, true); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + SignedShort4PixelOperations.ToVector4(MemoryMarshal.Cast(source), destination, false, true); + Numerics.Premultiply(destination); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + SignedShort4PixelOperations.FromVector4(source, MemoryMarshal.Cast(destination), false, false); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Normalize all four components before unassociating, then use the scaled packing path to preserve Short4's affine mapping. + Vector4Converters.AddThenDivide(source, NativeOffset, NativeMagnitude); + Numerics.UnPremultiply(source); + SignedShort4PixelOperations.FromVector4(source, MemoryMarshal.Cast(destination), false, true); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + SignedShort4PixelOperations.FromVector4(source, MemoryMarshal.Cast(destination), false, true); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + Numerics.UnPremultiply(source); + SignedShort4PixelOperations.FromVector4(source, MemoryMarshal.Cast(destination), false, true); + } + } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs index e7c97269e..a88823069 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rg32.cs @@ -7,11 +7,12 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing two 16-bit unsigned normalized values ranging from 0 to 1. -/// -/// Ranges from [0, 0, 0, 1] to [1, 1, 0, 1] in vector form. -/// +/// Packed pixel type containing two 16-bit unsigned normalized values. /// +/// +/// , , and scaled vector conversions return x and y in [0, 1], z +/// as 0, and implicit alpha as 1. The packed storage layout matches DXGI_FORMAT_R16G16_UNORM. +/// public partial struct Rg32 : IPixel, IPackedVector { private static readonly Vector2 Max = new(ushort.MaxValue); @@ -84,6 +85,48 @@ public partial struct Rg32 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rg32 FromAssociatedVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rg32 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb24.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb24.cs index 190407296..b8c2d2098 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb24.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb24.cs @@ -108,6 +108,48 @@ public partial struct Rgb24 : IPixel /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb24 FromAssociatedVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rgb24 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs index ed6f57abb..0a240b369 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs @@ -91,6 +91,48 @@ public partial struct Rgb48 : IPixel /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb48 FromAssociatedVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rgb48 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs new file mode 100644 index 000000000..4b940a6a5 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -0,0 +1,245 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Pixel type containing three 32-bit unsigned normalized values ranging from 0 to 4294967295. +/// The color components are stored in red, green, blue. +/// +/// Ranges from [0, 0, 0] to [1, 1, 1] in vector form. +/// +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct Rgb96 : IPixel, IEquatable +{ + private const float InvMax = 1.0f / uint.MaxValue; + + // Use double here because at this magnitude a float cannot represent all 32-bit + // integer values exactly. A float only has 24 bits of precision, so around + // uint.MaxValue it can only represent multiples of 256 and will round + // 4294967295 up to 4294967296. Double has 53 bits of precision and can + // represent all uint values exactly, avoiding precision loss before scaling. + private const double Max = uint.MaxValue; + + /// + /// Gets the red component. + /// + public uint R; + + /// + /// Gets the green component. + /// + public uint G; + + /// + /// Gets the blue component. + /// + public uint B; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb96(uint r, uint g, uint b) + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + /// The on the right side of the operand. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgb96 left, Rgb96 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgb96 left, Rgb96 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + this.R * InvMax, + this.G * InvMax, + this.B * InvMax, + 1.0f); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromAssociatedVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + return new Rgb96( + (uint)Math.Round(source.X * Max), + (uint)Math.Round(source.Y * Max), + (uint)Math.Round(source.Z * Max)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromAbgr32(Abgr32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromArgb32(Argb32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgr24(Bgr24 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgra32(Bgra32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromL8(L8 source) + { + uint rgb = ColorNumerics.From8BitTo32Bit(source.PackedValue); + return new Rgb96(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromL16(L16 source) + { + uint rgb = ColorNumerics.From16BitTo32Bit(source.PackedValue); + return new(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromLa16(La16 source) + { + uint rgb = ColorNumerics.From8BitTo32Bit((byte)source.PackedValue); + return new(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromLa32(La32 source) + { + uint rgb = ColorNumerics.From16BitTo32Bit(source.L); + return new(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgb24(Rgb24 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgba32(Rgba32 source) => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgb48(Rgb48 source) => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgba64(Rgba64 source) => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( + PixelComponentInfo.Create(3, 32, 32, 32), + PixelColorType.RGB, + PixelAlphaRepresentation.None); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromRgb96(this); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + + /// + public override readonly string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})"); + + /// + public override readonly bool Equals(object? obj) => obj is Rgb96 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs index cdee22964..340e24777 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba1010102.cs @@ -7,12 +7,13 @@ using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed vector type containing 4 unsigned normalized values ranging from 0 to 1. +/// Packed pixel type containing four unsigned normalized values. /// The x, y and z components use 10 bits, and the w component uses 2 bits. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// /// +/// +/// and scaled vector conversions return all components in [0, 1]. The storage layout matches +/// DXGI_FORMAT_R10G10B10A2_UNORM. +/// public partial struct Rgba1010102 : IPixel, IPackedVector { private static readonly Vector4 Multiplier = new(1023F, 1023F, 1023F, 3F); @@ -86,6 +87,47 @@ public partial struct Rgba1010102 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba1010102 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rgba1010102 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs new file mode 100644 index 000000000..86fab4f61 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs @@ -0,0 +1,241 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Pixel type containing four 32-bit unsigned normalized values ranging from 0 to 4294967295. +/// The color components are stored in red, green, blue and alpha. +/// +/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. +/// +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct Rgba128 : IPixel, IEquatable +{ + private const float InvMax = 1.0f / uint.MaxValue; + + // Use double here because at this magnitude a float cannot represent all 32-bit + // integer values exactly. A float only has 24 bits of precision, so around + // uint.MaxValue it can only represent multiples of 256 and will round + // 4294967295 up to 4294967296. Double has 53 bits of precision and can + // represent all uint values exactly, avoiding precision loss before scaling. + private const double Max = uint.MaxValue; + + /// + /// Gets the red component. + /// + public uint R; + + /// + /// Gets the green component. + /// + public uint G; + + /// + /// Gets the blue component. + /// + public uint B; + + /// + /// Gets the alpha channel. + /// + public uint A; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba128(uint r, uint g, uint b, uint a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + /// The on the right side of the operand. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgba128 left, Rgba128 right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgba128 left, Rgba128 right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new( + this.R * InvMax, + this.G * InvMax, + this.B * InvMax, + this.A * InvMax); + + /// + public static PixelOperations CreatePixelOperations() => new(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba128 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba128 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba128 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba128 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static Rgba128 FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + public static Rgba128 FromVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + return new Rgba128( + (uint)Math.Round(source.X * Max), + (uint)Math.Round(source.Y * Max), + (uint)Math.Round(source.Z * Max), + (uint)Math.Round(source.W * Max)); + } + + /// + public static Rgba128 FromAbgr32(Abgr32 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A)); + + /// + public static Rgba128 FromArgb32(Argb32 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A)); + + /// + public static Rgba128 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba128 FromBgr24(Bgr24 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), uint.MaxValue); + + /// + public static Rgba128 FromBgra32(Bgra32 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A)); + + /// + public static Rgba128 FromL8(L8 source) + { + uint rgb = ColorNumerics.From8BitTo32Bit(source.PackedValue); + return new Rgba128(rgb, rgb, rgb, rgb); + } + + /// + public static Rgba128 FromL16(L16 source) + { + uint rgb = ColorNumerics.From16BitTo32Bit(source.PackedValue); + return new(rgb, rgb, rgb, rgb); + } + + /// + public static Rgba128 FromLa16(La16 source) + { + uint rgb = ColorNumerics.From8BitTo32Bit((byte)source.PackedValue); + return new(rgb, rgb, rgb, rgb); + } + + /// + public static Rgba128 FromLa32(La32 source) + { + uint rgb = ColorNumerics.From16BitTo32Bit(source.L); + return new(rgb, rgb, rgb, rgb); + } + + /// + public static Rgba128 FromRgb24(Rgb24 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), uint.MaxValue); + + /// + public static Rgba128 FromRgba32(Rgba32 source) + => new(ColorNumerics.From8BitTo32Bit(source.R), ColorNumerics.From8BitTo32Bit(source.G), ColorNumerics.From8BitTo32Bit(source.B), ColorNumerics.From8BitTo32Bit(source.A)); + + /// + public static Rgba128 FromRgb48(Rgb48 source) + => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B), uint.MaxValue); + + /// + public static Rgba128 FromRgba64(Rgba64 source) + => new(ColorNumerics.From16BitTo32Bit(source.R), ColorNumerics.From16BitTo32Bit(source.G), ColorNumerics.From16BitTo32Bit(source.B), ColorNumerics.From16BitTo32Bit(source.A)); + + /// + public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 32, 32, 32, 32), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public readonly Rgba32 ToRgba32() => Rgba32.FromRgba128(this); + + /// + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); + + /// + public override readonly string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})"); + + /// + public override readonly bool Equals(object? obj) => obj is Rgba128 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B && rgb.A == this.A; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Rgba128 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B) && this.A.Equals(other.A); +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs index 199754c69..689d31793 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs @@ -220,7 +220,7 @@ public partial struct Rgba32 : IPixel, IPackedVector /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / MaxBytes; + public readonly Vector4 ToVector4() => new Vector4(this.R, this.G, this.B, this.A) / byte.MaxValue; /// public static PixelTypeInfo GetPixelTypeInfo() @@ -232,6 +232,47 @@ public partial struct Rgba32 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rgba32 FromScaledVector4(Vector4 source) => FromVector4(source); @@ -314,6 +355,36 @@ public partial struct Rgba32 : IPixel, IPackedVector A = ColorNumerics.From16BitTo8Bit(source.A) }; + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The pixel value as Rgba32. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgb96(Rgb96 source) + => new() + { + R = ColorNumerics.From32BitTo8Bit(source.R), + G = ColorNumerics.From32BitTo8Bit(source.G), + B = ColorNumerics.From32BitTo8Bit(source.B), + A = byte.MaxValue + }; + + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + /// The pixel value as Rgba32. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgba128(Rgba128 source) + => new() + { + R = ColorNumerics.From32BitTo8Bit(source.R), + G = ColorNumerics.From32BitTo8Bit(source.G), + B = ColorNumerics.From32BitTo8Bit(source.B), + A = ColorNumerics.From32BitTo8Bit(source.A), + }; + /// /// Converts the value of this instance to a hexadecimal string. /// diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32P.cs new file mode 100644 index 000000000..c7bb17ce8 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32P.cs @@ -0,0 +1,273 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Packed pixel type containing associated red, green, blue, and alpha components as 8-bit unsigned normalized values. +/// Components are stored in red, green, blue, and alpha order from least to most significant byte. +/// +/// +/// The native component, packed, and vector representations use associated alpha. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct Rgba32P : IPixel, IPackedVector +{ + private static readonly Vector4 Half = new(0.5F); + private static readonly Vector4 MaxBytes = new(byte.MaxValue); + + /// + /// Gets or sets the associated red component. + /// + public byte R; + + /// + /// Gets or sets the associated green component. + /// + public byte G; + + /// + /// Gets or sets the associated blue component. + /// + public byte B; + + /// + /// Gets or sets the alpha component. + /// + public byte A; + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public Rgba32P(byte r, byte g, byte b) + : this(r, g, b, byte.MaxValue) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public Rgba32P(byte r, byte g, byte b, byte a) + { + this.R = r; + this.G = g; + this.B = b; + this.A = a; + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public Rgba32P(float r, float g, float b) + : this(new Vector4(r, g, b, 1F)) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public Rgba32P(float r, float g, float b, float a) + : this(new Vector4(r, g, b, a)) + { + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public Rgba32P(Vector3 vector) + : this(new Vector4(vector, 1F)) + { + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public Rgba32P(Vector4 vector) + : this() => this = FromScaledVector4(vector); + + /// + /// Initializes a new instance of the struct from a packed associated value. + /// + /// The packed associated value. + public Rgba32P(uint packed) + : this() => this.PackedValue = packed; + + /// + public uint PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(Rgba32P left, Rgba32P right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(Rgba32P left, Rgba32P right) => !left.Equals(right); + + /// + public readonly Rgba32 ToRgba32() + => Rgba32.FromScaledVector4(Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(this.R, this.G, this.B, this.A)); + + /// + public readonly Vector4 ToScaledVector4() => new Vector4(this.R, this.G, this.B, this.A) / byte.MaxValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() + { + // Divide the stored byte magnitudes before normalization so unassociation cannot move an exact byte conversion across its rounding midpoint. + return Vector4Converters.AssociatedRgbaCompatible.ToUnassociatedVector4(this.R, this.G, this.B, this.A); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + public readonly Vector4 ToVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToUnassociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 8, 8, 8, 8), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32P FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static Rgba32P FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32P FromUnassociatedVector4(Vector4 source) => FromUnassociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32P FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32P FromUnassociatedScaledVector4(Vector4 source) + => Vector4Converters.AssociatedRgbaCompatible.FromUnassociatedVector4ToRgba32P(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32P FromAssociatedScaledVector4(Vector4 source) + { + // Rescale associated RGB when alpha rounds to a different byte so the stored channels remain associated with the alpha actually written. + return Vector4Converters.AssociatedRgbaCompatible.FromAssociatedVector4ToRgba32P(source); + } + + /// + public static Rgba32P FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static Rgba32P FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is Rgba32P other && this.Equals(other); + + /// + public readonly bool Equals(Rgba32P other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() => $"Rgba32P({this.R}, {this.G}, {this.B}, {this.A})"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Rgba32P Pack(Vector4 vector) + { + vector *= MaxBytes; + vector += Half; + vector = Numerics.Clamp(vector, Vector4.Zero, MaxBytes); + + // Each converted component occupies one 32-bit lane. Reinterpreting those lanes as bytes exposes their low bytes at offsets 0, 4, 8, and 12. + Vector128 result = Vector128.ConvertToInt32(vector.AsVector128()).AsByte(); + return new Rgba32P(result.GetElement(0), result.GetElement(4), result.GetElement(8), result.GetElement(12)); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs index c73daaf86..300f2357f 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba64.cs @@ -8,11 +8,12 @@ using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.PixelFormats; /// -/// Packed pixel type containing four 16-bit unsigned normalized values ranging from 0 to 65535. -/// -/// Ranges from [0, 0, 0, 0] to [1, 1, 1, 1] in vector form. -/// +/// Packed pixel type containing four 16-bit unsigned normalized values. /// +/// +/// Component fields expose storage values in [0, 65535]. and scaled vector conversions +/// return them in [0, 1]. The storage layout matches DXGI_FORMAT_R16G16B16A16_UNORM. +/// [StructLayout(LayoutKind.Sequential)] public partial struct Rgba64 : IPixel, IPackedVector { @@ -212,6 +213,47 @@ public partial struct Rgba64 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba64 FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rgba64 FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs new file mode 100644 index 000000000..573bfca55 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalf.cs @@ -0,0 +1,227 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Packed pixel type containing four 16-bit floating-point values typically ranging from 0 to 1. +/// The color components are stored in red, green, blue, and alpha order. +/// +/// +/// and scaled vector conversions return the same component values in the nominal color range +/// [0, 1]. The packed representation is binary-compatible with DXGI_FORMAT_R16G16B16A16_FLOAT. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct RgbaHalf : IPixel, IPackedVector +{ + /// + /// Gets or sets the red component. + /// + public Half R; + + /// + /// Gets or sets the green component. + /// + public Half G; + + /// + /// Gets or sets the blue component. + /// + public Half B; + + /// + /// Gets or sets the alpha component. + /// + public Half A; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + public RgbaHalf(float r, float g, float b) + : this(r, g, b, 1F) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The alpha component. + public RgbaHalf(float r, float g, float b, float a) + { + this.R = (Half)r; + this.G = (Half)g; + this.B = (Half)b; + this.A = (Half)a; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector containing the component values. + public RgbaHalf(Vector4 vector) + : this(vector.X, vector.Y, vector.Z, vector.W) + { + } + + /// + public ulong PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(RgbaHalf left, RgbaHalf right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(RgbaHalf left, RgbaHalf right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new((float)this.R, (float)this.G, (float)this.B, (float)this.A); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Unassociated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromScaledVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalf FromVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + return new RgbaHalf(source); + } + + /// + public static RgbaHalf FromAbgr32(Abgr32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromArgb32(Argb32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromBgr24(Bgr24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromBgra32(Bgra32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromL8(L8 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromL16(L16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromLa16(La16 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromLa32(La32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromRgb24(Rgb24 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromRgba32(Rgba32 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromRgb48(Rgb48 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalf FromRgba64(Rgba64 source) => FromScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is RgbaHalf other && this.Equals(other); + + /// + public readonly bool Equals(RgbaHalf other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() => FormattableString.Invariant($"RgbaHalf({(float)this.R:#0.##}, {(float)this.G:#0.##}, {(float)this.B:#0.##}, {(float)this.A:#0.##})"); +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs new file mode 100644 index 000000000..75fe4907c --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaHalfP.cs @@ -0,0 +1,242 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.PixelFormats; + +/// +/// Packed pixel type containing four associated 16-bit floating-point values typically ranging from 0 to 1. +/// The color components are stored in red, green, blue, and alpha order. +/// +/// +/// and scaled vector conversions return the same associated component values in the nominal +/// color range [0, 1]. The packed representation is binary-compatible with +/// DXGI_FORMAT_R16G16B16A16_FLOAT. +/// +[StructLayout(LayoutKind.Sequential)] +public partial struct RgbaHalfP : IPixel, IPackedVector +{ + /// + /// Gets or sets the associated red component. + /// + public Half R; + + /// + /// Gets or sets the associated green component. + /// + public Half G; + + /// + /// Gets or sets the associated blue component. + /// + public Half B; + + /// + /// Gets or sets the alpha component. + /// + public Half A; + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + public RgbaHalfP(float r, float g, float b) + : this(r, g, b, 1F) + { + } + + /// + /// Initializes a new instance of the struct from associated components. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + public RgbaHalfP(float r, float g, float b, float a) + { + this.R = (Half)r; + this.G = (Half)g; + this.B = (Half)b; + this.A = (Half)a; + } + + /// + /// Initializes a new instance of the struct from an associated vector. + /// + /// The associated vector. + public RgbaHalfP(Vector4 vector) + : this() => this = FromAssociatedScaledVector4(vector); + + /// + public ulong PackedValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + readonly get => Unsafe.As(ref Unsafe.AsRef(in this)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => Unsafe.As(ref this) = value; + } + + /// + /// Compares two values for equality. + /// + /// The left value. + /// The right value. + /// when the values are equal. + public static bool operator ==(RgbaHalfP left, RgbaHalfP right) => left.Equals(right); + + /// + /// Compares two values for inequality. + /// + /// The left value. + /// The right value. + /// when the values are not equal. + public static bool operator !=(RgbaHalfP left, RgbaHalfP right) => !left.Equals(right); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromScaledVector4(this.ToUnassociatedScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => new((float)this.R, (float)this.G, (float)this.B, (float)this.A); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.UnPremultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToUnassociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16, 16), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Associated); + + /// + public static PixelOperations CreatePixelOperations() => new PixelOperations(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromScaledVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromVector4(Vector4 source) => FromAssociatedVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromUnassociatedScaledVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + + // RGB must be associated with the alpha value that binary16 storage can reproduce, not the higher-precision input alpha. + source.W = HalfTypeHelper.Unpack(HalfTypeHelper.Pack(source.W)); + Numerics.Premultiply(ref source); + return new RgbaHalfP(source.X, source.Y, source.Z, source.W); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromAssociatedScaledVector4(Vector4 source) + { + float alpha = source.W; + + if (alpha <= 0F) + { + return default; + } + + float storedAlpha = HalfTypeHelper.Unpack(HalfTypeHelper.Pack(Numerics.Clamp(alpha, 0F, 1F))); + + // Preserve the represented straight color when binary16 rounds alpha, then restore the associated RGB <= alpha invariant. + source *= storedAlpha / alpha; + source.W = storedAlpha; + Numerics.ClampRgbToAlpha(ref source); + return new RgbaHalfP(source.X, source.Y, source.Z, source.W); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromUnassociatedVector4(Vector4 source) => FromUnassociatedScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaHalfP FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + + /// + public static RgbaHalfP FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public static RgbaHalfP FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + /// + public override readonly bool Equals(object? obj) => obj is RgbaHalfP other && this.Equals(other); + + /// + public readonly bool Equals(RgbaHalfP other) => this.PackedValue.Equals(other.PackedValue); + + /// + public override readonly int GetHashCode() => this.PackedValue.GetHashCode(); + + /// + public override readonly string ToString() => FormattableString.Invariant($"RgbaHalfP({(float)this.R:#0.##}, {(float)this.G:#0.##}, {(float)this.B:#0.##}, {(float)this.A:#0.##})"); +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs index 3f9cab32f..b1704d526 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/RgbaVector.cs @@ -106,6 +106,47 @@ public partial struct RgbaVector : IPixel /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaVector FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RgbaVector FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs index 39c853a5e..d6ab2ac1a 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Short2.cs @@ -8,10 +8,12 @@ namespace SixLabors.ImageSharp.PixelFormats; /// /// Packed pixel type containing two 16-bit signed integer values. -/// -/// Ranges from [-32767, -32767, 0, 1] to [32767, 32767, 0, 1] in vector form. -/// /// +/// +/// and return stored components in [-32768, 32767]. +/// Scaled vector conversions map the full stored range to [0, 1]. The packed storage layout matches +/// DXGI_FORMAT_R16G16_SINT. +/// public partial struct Short2 : IPixel, IPackedVector { // Largest two byte positive number 0xFFFF >> 1; @@ -20,6 +22,9 @@ public partial struct Short2 : IPixel, IPackedVector // Two's complement private const float MinNeg = ~(int)MaxPos; + // Scaled conversions cover every signed 16-bit code, including the asymmetric minimum value. + private const float Range = MaxPos - MinNeg; + private static readonly Vector2 Max = new(MaxPos); private static readonly Vector2 Min = new(MinNeg); @@ -73,8 +78,8 @@ public partial struct Short2 : IPixel, IPackedVector public readonly Vector4 ToScaledVector4() { Vector2 scaled = this.ToVector2(); - scaled += new Vector2(32767f); - scaled /= 65534F; + scaled -= Min; + scaled /= Range; return new Vector4(scaled, 0f, 1f); } @@ -92,12 +97,55 @@ public partial struct Short2 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromAssociatedScaledVector4(Vector4 source) + { + // The destination has implicit alpha one, but associated input must be restored before its alpha is discarded. + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short2 FromAssociatedVector4(Vector4 source) + { + // Only the stored color components use the signed native encoding; W remains the scaled source alpha. + source.X = (source.X - MinNeg) / Range; + source.Y = (source.Y - MinNeg) / Range; + return FromAssociatedScaledVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Short2 FromScaledVector4(Vector4 source) { - Vector2 scaled = new Vector2(source.X, source.Y) * 65534F; - scaled -= new Vector2(32767F); + Vector2 scaled = new Vector2(source.X, source.Y) * Range; + scaled += Min; return new Short2 { PackedValue = Pack(scaled) }; } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs index b6cece2be..fc3d3367d 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Short4.cs @@ -8,10 +8,11 @@ namespace SixLabors.ImageSharp.PixelFormats; /// /// Packed pixel type containing four 16-bit signed integer values. -/// -/// Ranges from [-37267, -37267, -37267, -37267] to [37267, 37267, 37267, 37267] in vector form. -/// /// +/// +/// returns stored components in [-32768, 32767]. Scaled vector conversions map the full +/// stored range to [0, 1]. The packed storage layout matches DXGI_FORMAT_R16G16B16A16_SINT. +/// public partial struct Short4 : IPixel, IPackedVector { // Largest two byte positive number 0xFFFF >> 1; @@ -20,6 +21,9 @@ public partial struct Short4 : IPixel, IPackedVector // Two's complement private const float MinNeg = ~(int)MaxPos; + // Scaled conversions cover every signed 16-bit code, including the asymmetric minimum value. + private const float Range = MaxPos - MinNeg; + private static readonly Vector4 Max = new(MaxPos); private static readonly Vector4 Min = new(MinNeg); @@ -75,8 +79,8 @@ public partial struct Short4 : IPixel, IPackedVector public readonly Vector4 ToScaledVector4() { Vector4 scaled = this.ToVector4(); - scaled += new Vector4(32767f); - scaled /= 65534f; + scaled -= Min; + scaled /= Range; return scaled; } @@ -99,12 +103,67 @@ public partial struct Short4 : IPixel, IPackedVector /// public static PixelOperations CreatePixelOperations() => new PixelOperations(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() + { + Vector4 vector = this.ToAssociatedScaledVector4(); + + // Native components use the full asymmetric signed range, so scaled zero must map to -32768 rather than -32767. + vector *= Range; + vector += Min; + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Short4 FromAssociatedVector4(Vector4 source) + { + // Map the full signed native encoding to scaled [0, 1] space before unassociating. + source -= Min; + source /= Range; + return FromAssociatedScaledVector4(source); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Short4 FromScaledVector4(Vector4 source) { - source *= 65534F; - source -= new Vector4(32767F); + source *= Range; + source += Min; return FromVector4(source); } diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.cs b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.cs index 712d43f76..854b515dc 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.cs @@ -57,7 +57,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Argb32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Argb32.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -123,7 +123,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Abgr32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Abgr32.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -189,7 +189,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Bgr24.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Bgr24.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -255,7 +255,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Bgra32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Bgra32.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -321,7 +321,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = L8.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = L8.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -387,7 +387,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = L16.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = L16.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -453,7 +453,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = La16.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = La16.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -519,7 +519,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = La32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = La32.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -585,7 +585,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Rgb24.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Rgb24.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -651,7 +651,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Rgba32.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Rgba32.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -717,7 +717,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Rgb48.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Rgb48.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -783,7 +783,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Rgba64.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Rgba64.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } @@ -849,7 +849,7 @@ public partial class PixelOperations for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = Bgra5551.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = Bgra5551.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt index b2697cb4e..769f3b44b 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.Generated.tt @@ -69,7 +69,7 @@ for (nuint i = 0; i < (uint)source.Length; i++) { - Unsafe.Add(ref destinationBase, i) = <#=pixelType#>.FromScaledVector4(Unsafe.Add(ref sourceBase, i).ToScaledVector4()); + Unsafe.Add(ref destinationBase, i) = <#=pixelType#>.FromUnassociatedScaledVector4(Unsafe.Add(ref sourceBase, i).ToUnassociatedScaledVector4()); } } diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.PixelBenders.cs b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.PixelBenders.cs index bf1391990..fe7de51c9 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.PixelBenders.cs +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.PixelBenders.cs @@ -34,182 +34,182 @@ public partial class PixelOperations case PixelAlphaCompositionMode.Clear: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyClear.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddClear.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractClear.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenClear.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenClear.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenClear.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayClear.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightClear.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyClear; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddClear; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractClear; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenClear; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenClear; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenClear; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayClear; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightClear; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalClear.Instance; + default: return DefaultPixelBlenders.NormalClear; } case PixelAlphaCompositionMode.Xor: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyXor.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddXor.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractXor.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenXor.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenXor.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenXor.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayXor.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightXor.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyXor; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddXor; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractXor; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenXor; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenXor; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenXor; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayXor; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightXor; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalXor.Instance; + default: return DefaultPixelBlenders.NormalXor; } case PixelAlphaCompositionMode.Src: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrc.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrc.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrc.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrc.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrc.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrc.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrc.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrc.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrc; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrc; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrc; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrc; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrc; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrc; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrc; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrc; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalSrc.Instance; + default: return DefaultPixelBlenders.NormalSrc; } case PixelAlphaCompositionMode.SrcAtop: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcAtop.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcAtop.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcAtop.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcAtop.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcAtop.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcAtop.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcAtop.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcAtop.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcAtop; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcAtop; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcAtop; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcAtop; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcAtop; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcAtop; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcAtop; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcAtop; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalSrcAtop.Instance; + default: return DefaultPixelBlenders.NormalSrcAtop; } case PixelAlphaCompositionMode.SrcIn: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcIn.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcIn.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcIn.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcIn.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcIn.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcIn.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcIn.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcIn.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcIn; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcIn; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcIn; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcIn; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcIn; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcIn; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcIn; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcIn; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalSrcIn.Instance; + default: return DefaultPixelBlenders.NormalSrcIn; } case PixelAlphaCompositionMode.SrcOut: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcOut.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcOut.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcOut.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcOut.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcOut.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcOut.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcOut.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcOut.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcOut; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcOut; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcOut; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcOut; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcOut; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcOut; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcOut; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcOut; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalSrcOut.Instance; + default: return DefaultPixelBlenders.NormalSrcOut; } case PixelAlphaCompositionMode.Dest: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDest.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDest.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDest.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDest.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDest.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDest.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDest.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDest.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDest; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDest; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDest; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDest; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDest; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDest; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDest; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDest; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalDest.Instance; + default: return DefaultPixelBlenders.NormalDest; } case PixelAlphaCompositionMode.DestAtop: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestAtop.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestAtop.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestAtop.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestAtop.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestAtop.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestAtop.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestAtop.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestAtop.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestAtop; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestAtop; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestAtop; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestAtop; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestAtop; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestAtop; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestAtop; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestAtop; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalDestAtop.Instance; + default: return DefaultPixelBlenders.NormalDestAtop; } case PixelAlphaCompositionMode.DestIn: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestIn.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestIn.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestIn.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestIn.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestIn.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestIn.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestIn.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestIn.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestIn; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestIn; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestIn; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestIn; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestIn; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestIn; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestIn; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestIn; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalDestIn.Instance; + default: return DefaultPixelBlenders.NormalDestIn; } case PixelAlphaCompositionMode.DestOut: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestOut.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestOut.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestOut.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestOut.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestOut.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestOut.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestOut.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestOut.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestOut; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestOut; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestOut; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestOut; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestOut; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestOut; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestOut; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestOut; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalDestOut.Instance; + default: return DefaultPixelBlenders.NormalDestOut; } case PixelAlphaCompositionMode.DestOver: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestOver.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestOver.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestOver.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestOver.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestOver.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestOver.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestOver.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestOver.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplyDestOver; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddDestOver; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractDestOver; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenDestOver; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenDestOver; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenDestOver; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlayDestOver; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightDestOver; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalDestOver.Instance; + default: return DefaultPixelBlenders.NormalDestOver; } case PixelAlphaCompositionMode.SrcOver: default: switch (colorMode) { - case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcOver.Instance; - case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcOver.Instance; - case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcOver.Instance; - case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcOver.Instance; - case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcOver.Instance; - case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcOver.Instance; - case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcOver.Instance; - case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcOver.Instance; + case PixelColorBlendingMode.Multiply: return DefaultPixelBlenders.MultiplySrcOver; + case PixelColorBlendingMode.Add: return DefaultPixelBlenders.AddSrcOver; + case PixelColorBlendingMode.Subtract: return DefaultPixelBlenders.SubtractSrcOver; + case PixelColorBlendingMode.Screen: return DefaultPixelBlenders.ScreenSrcOver; + case PixelColorBlendingMode.Darken: return DefaultPixelBlenders.DarkenSrcOver; + case PixelColorBlendingMode.Lighten: return DefaultPixelBlenders.LightenSrcOver; + case PixelColorBlendingMode.Overlay: return DefaultPixelBlenders.OverlaySrcOver; + case PixelColorBlendingMode.HardLight: return DefaultPixelBlenders.HardLightSrcOver; case PixelColorBlendingMode.Normal: - default: return DefaultPixelBlenders.NormalSrcOver.Instance; + default: return DefaultPixelBlenders.NormalSrcOver; } } } diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs index ea970a718..87957a995 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs @@ -5,14 +5,19 @@ using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles.Companding; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.PixelFormats; /// -/// A stateless class implementing Strategy Pattern for batched pixel-data conversion operations -/// for pixel buffers of type . +/// Provides bulk conversion operations for pixel buffers of type . /// +/// +/// The default conversion behavior is suitable for pixel formats that store unassociated alpha. +/// Pixel formats that store associated alpha should derive their operations from +/// . +/// /// The pixel format. public partial class PixelOperations where TPixel : unmanaged, IPixel @@ -26,6 +31,90 @@ public partial class PixelOperations public static PixelOperations Instance => LazyInstance.Value; #pragma warning restore CA1000 // Do not declare static members on generic types + /// + /// Converts pixels to unassociated vectors in the pixel format's native numeric range. + /// + /// The configuration. + /// The source pixels. + /// The destination vectors. + protected virtual void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Utils.Vector4Converters.Default.ToVector4(source, destination, PixelConversionModifiers.UnPremultiply); + + /// + /// Converts pixels to associated vectors in the pixel format's native numeric range. + /// + /// The configuration. + /// The source pixels. + /// The destination vectors. + protected virtual void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + => Utils.Vector4Converters.Default.ToVector4(source, destination, PixelConversionModifiers.Premultiply); + + /// + /// Converts pixels to the unassociated scaled vector representation used by color operations. + /// + /// The configuration. + /// The source pixels. + /// The destination vectors. + protected virtual void ToUnassociatedScaledVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination) + => Utils.Vector4Converters.Default.ToVector4(source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + /// + /// Converts pixels to associated scaled vectors. + /// + /// The configuration. + /// The source pixels. + /// The destination vectors. + protected virtual void ToAssociatedScaledVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination) + => Utils.Vector4Converters.Default.ToVector4(source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + + /// + /// Converts unassociated vectors in the pixel format's native numeric range to pixels. + /// + /// The configuration. + /// The source vectors. Implementations may modify this buffer during conversion. + /// The destination pixels. + protected virtual void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + => Utils.Vector4Converters.Default.FromVector4(source, destination, PixelConversionModifiers.UnPremultiply); + + /// + /// Converts associated vectors in the pixel format's native numeric range to pixels. + /// + /// The configuration. + /// The source vectors. Implementations may modify this buffer during conversion. + /// The destination pixels. + protected virtual void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + => Utils.Vector4Converters.Default.FromVector4(source, destination, PixelConversionModifiers.Premultiply); + + /// + /// Converts unassociated scaled vectors to pixels. + /// + /// The configuration. + /// The source vectors. Implementations may modify this buffer during conversion. + /// The destination pixels. + protected virtual void FromUnassociatedScaledVector4Destructive( + Configuration configuration, + Span source, + Span destination) + => Utils.Vector4Converters.Default.FromVector4(source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + /// + /// Converts associated scaled vectors to pixels. + /// + /// The configuration. + /// The source vectors. Implementations may modify this buffer during conversion. + /// The destination pixels. + protected virtual void FromAssociatedScaledVector4Destructive( + Configuration configuration, + Span source, + Span destination) + => Utils.Vector4Converters.Default.FromVector4(source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + /// /// Gets the pixel type info for the given . /// @@ -33,17 +122,16 @@ public partial class PixelOperations public PixelTypeInfo GetPixelTypeInfo() => TPixel.GetPixelTypeInfo(); /// - /// Bulk version of converting 'sourceVectors.Length' pixels into 'destinationColors'. - /// The method is DESTRUCTIVE altering the contents of . + /// Converts vectors to pixels using the numeric range, alpha representation, and companding specified by . + /// The contents of are not preserved. /// /// - /// The destructive behavior is a design choice for performance reasons. - /// In a typical use case the contents of are abandoned after the conversion. + /// Callers must not rely on the contents of after this method returns. /// - /// A to configure internal operations - /// The to the source vectors. - /// The to the destination colors. - /// The to apply during the conversion + /// The configuration. + /// The source vectors. Their contents may be modified during conversion. + /// The destination pixels. + /// The representation of the source vectors and the transformations applied before storing the pixels. public virtual void FromVector4Destructive( Configuration configuration, Span sourceVectors, @@ -51,20 +139,63 @@ public partial class PixelOperations PixelConversionModifiers modifiers) { Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(sourceVectors, destination, nameof(destination)); - Utils.Vector4Converters.Default.FromVector4(sourceVectors, destination, modifiers); + bool associated = modifiers.IsDefined(PixelConversionModifiers.Premultiply); + bool scaled = modifiers.IsDefined(PixelConversionModifiers.Scale); + + if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand)) + { + // Transfer functions operate on straight color components, so restore straight RGB before compressing associated input. + if (associated) + { + Numerics.UnPremultiply(sourceVectors); + } + + SRgbCompanding.Compress(sourceVectors); + + if (scaled) + { + this.FromUnassociatedScaledVector4Destructive(configuration, sourceVectors, destination); + } + else + { + this.FromUnassociatedVector4Destructive(configuration, sourceVectors, destination); + } + + return; + } + + if (scaled) + { + if (associated) + { + this.FromAssociatedScaledVector4Destructive(configuration, sourceVectors, destination); + } + else + { + this.FromUnassociatedScaledVector4Destructive(configuration, sourceVectors, destination); + } + } + else if (associated) + { + this.FromAssociatedVector4Destructive(configuration, sourceVectors, destination); + } + else + { + this.FromUnassociatedVector4Destructive(configuration, sourceVectors, destination); + } } /// /// Bulk version of converting 'sourceVectors.Length' pixels into 'destinationColors'. - /// The method is DESTRUCTIVE altering the contents of . + /// The contents of are not preserved. /// /// - /// The destructive behavior is a design choice for performance reasons. - /// In a typical use case the contents of are abandoned after the conversion. + /// Callers must not rely on the contents of after this method returns. /// /// A to configure internal operations - /// The to the source vectors. + /// The source vectors. Their contents may be modified during conversion. /// The to the destination colors. public void FromVector4Destructive( Configuration configuration, @@ -73,12 +204,12 @@ public partial class PixelOperations => this.FromVector4Destructive(configuration, sourceVectors, destination, PixelConversionModifiers.None); /// - /// Bulk version of converting 'sourceColors.Length' pixels into 'destinationVectors'. + /// Converts pixels to vectors using the numeric range, alpha representation, and companding specified by . /// - /// A to configure internal operations - /// The to the source colors. - /// The to the destination vectors. - /// The to apply during the conversion + /// The configuration. + /// The source pixels. + /// The destination vectors. + /// The requested vector representation and the transformations applied after reading the pixels. public virtual void ToVector4( Configuration configuration, ReadOnlySpan source, @@ -86,8 +217,53 @@ public partial class PixelOperations PixelConversionModifiers modifiers) { Guard.NotNull(configuration, nameof(configuration)); + Guard.DestinationShouldNotBeTooShort(source, destinationVectors, nameof(destinationVectors)); + + bool associated = modifiers.IsDefined(PixelConversionModifiers.Premultiply); + bool scaled = modifiers.IsDefined(PixelConversionModifiers.Scale); - Utils.Vector4Converters.Default.ToVector4(source, destinationVectors, modifiers); + if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand)) + { + // Expand straight RGB first because a transfer function applied to associated components would make color depend on alpha. + if (scaled) + { + this.ToUnassociatedScaledVector4(configuration, source, destinationVectors); + } + else + { + this.ToUnassociatedVector4(configuration, source, destinationVectors); + } + + Span converted = destinationVectors[..source.Length]; + SRgbCompanding.Expand(converted); + + if (associated) + { + Numerics.Premultiply(converted); + } + + return; + } + + if (scaled) + { + if (associated) + { + this.ToAssociatedScaledVector4(configuration, source, destinationVectors); + } + else + { + this.ToUnassociatedScaledVector4(configuration, source, destinationVectors); + } + } + else if (associated) + { + this.ToAssociatedVector4(configuration, source, destinationVectors); + } + else + { + this.ToUnassociatedVector4(configuration, source, destinationVectors); + } } /// @@ -103,10 +279,10 @@ public partial class PixelOperations => this.ToVector4(configuration, source, destinationVectors, PixelConversionModifiers.None); /// - /// Bulk operation that copies the to in - /// format. + /// Bulk operation that converts pixels from format to + /// destination pixels. /// - /// The destination pixel type. + /// The source pixel type. /// A to configure internal operations. /// The to the source pixels. /// The to the destination pixels. @@ -121,13 +297,14 @@ public partial class PixelOperations using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength); Span vectorSpan = tempVectors.GetSpan(); + for (int i = 0; i < numberOfSlices; i++) { int start = i * sliceLength; ReadOnlySpan s = source.Slice(start, sliceLength); Span d = destination.Slice(start, sliceLength); - PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale); - this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } int endOfCompleteSlices = numberOfSlices * sliceLength; @@ -137,8 +314,8 @@ public partial class PixelOperations ReadOnlySpan s = source[endOfCompleteSlices..]; Span d = destination[endOfCompleteSlices..]; vectorSpan = vectorSpan[..remainder]; - PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale); - this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } @@ -159,6 +336,13 @@ public partial class PixelOperations Guard.NotNull(configuration, nameof(configuration)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + if (typeof(TPixel) == typeof(TDestinationPixel)) + { + // An identical destination stores the same representation, including associated alpha, so copying preserves every packed component without a lossy representation round trip. + MemoryMarshal.Cast(source).CopyTo(destination); + return; + } + PixelOperations.Instance.From(configuration, source, destination); } diff --git a/src/ImageSharp/PixelFormats/Utils/PixelConverter.cs b/src/ImageSharp/PixelFormats/Utils/PixelConverter.cs index 50a68906e..c25cc301c 100644 --- a/src/ImageSharp/PixelFormats/Utils/PixelConverter.cs +++ b/src/ImageSharp/PixelFormats/Utils/PixelConverter.cs @@ -33,7 +33,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToArgb32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -44,7 +44,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgra32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -55,7 +55,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToAbgr32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -66,7 +66,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgb24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4Slice3(source, dest, default); + => SimdUtils.Shuffle4Slice3(source, dest); /// /// Converts a representing a collection of @@ -77,7 +77,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgr24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle3012)); + => SimdUtils.Shuffle4Slice3(source, dest); } /// @@ -96,7 +96,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgba32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -107,7 +107,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgra32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -118,7 +118,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToAbgr32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -129,7 +129,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgb24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle0321)); + => SimdUtils.Shuffle4Slice3(source, dest); /// /// Converts a representing a collection of @@ -140,7 +140,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgr24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle0123)); + => SimdUtils.Shuffle4Slice3(source, dest); } /// @@ -159,7 +159,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToArgb32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -170,7 +170,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgba32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -181,7 +181,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToAbgr32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -192,7 +192,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgb24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle3012)); + => SimdUtils.Shuffle4Slice3(source, dest); /// /// Converts a representing a collection of @@ -203,7 +203,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgr24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4Slice3(source, dest, default); + => SimdUtils.Shuffle4Slice3(source, dest); } /// @@ -222,7 +222,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToArgb32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -233,7 +233,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgba32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -244,7 +244,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgra32(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4(source, dest, default); + => SimdUtils.Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -255,7 +255,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgb24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle0123)); + => SimdUtils.Shuffle4Slice3(source, dest); /// /// Converts a representing a collection of @@ -266,7 +266,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgr24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle4Slice3(source, dest, new DefaultShuffle4Slice3(SimdUtils.Shuffle.MMShuffle0321)); + => SimdUtils.Shuffle4Slice3(source, dest); } /// @@ -285,7 +285,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgba32(ReadOnlySpan source, Span dest) - => SimdUtils.Pad3Shuffle4(source, dest, default); + => SimdUtils.Pad3Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -296,7 +296,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToArgb32(ReadOnlySpan source, Span dest) - => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle2103)); + => SimdUtils.Pad3Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -307,7 +307,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgra32(ReadOnlySpan source, Span dest) - => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle3012)); + => SimdUtils.Pad3Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -318,7 +318,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToAbgr32(ReadOnlySpan source, Span dest) - => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle0123)); + => SimdUtils.Pad3Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -329,7 +329,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgr24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle3(source, dest, new DefaultShuffle3(SimdUtils.Shuffle.MMShuffle3012)); + => SimdUtils.Shuffle3(source, dest); } /// @@ -348,7 +348,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToArgb32(ReadOnlySpan source, Span dest) - => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle0123)); + => SimdUtils.Pad3Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -359,7 +359,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgba32(ReadOnlySpan source, Span dest) - => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle3012)); + => SimdUtils.Pad3Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -370,7 +370,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToBgra32(ReadOnlySpan source, Span dest) - => SimdUtils.Pad3Shuffle4(source, dest, default); + => SimdUtils.Pad3Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -381,7 +381,7 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToAbgr32(ReadOnlySpan source, Span dest) - => SimdUtils.Pad3Shuffle4(source, dest, new DefaultPad3Shuffle4(SimdUtils.Shuffle.MMShuffle2103)); + => SimdUtils.Pad3Shuffle4(source, dest); /// /// Converts a representing a collection of @@ -392,6 +392,6 @@ internal static class PixelConverter /// The destination span of bytes. [MethodImpl(InliningOptions.ShortMethod)] public static void ToRgb24(ReadOnlySpan source, Span dest) - => SimdUtils.Shuffle3(source, dest, new DefaultShuffle3(SimdUtils.Shuffle.MMShuffle3012)); + => SimdUtils.Shuffle3(source, dest); } } diff --git a/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs b/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs new file mode 100644 index 000000000..4bf8c5e26 --- /dev/null +++ b/src/ImageSharp/PixelFormats/Utils/SignedShort4PixelOperations.cs @@ -0,0 +1,415 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.PixelFormats.Utils; + +/// +/// Provides shared SIMD conversion for pixel layouts containing four signed 16-bit components. +/// +internal static class SignedShort4PixelOperations +{ + private const byte RestorePackedPixelOrder = 0b_11_01_10_00; + private const float ShortMaximum = short.MaxValue; + private const float SignedNormalizedMagnitude = ShortMaximum * 2F; + private const float SignedIntegerMinimum = short.MinValue; + private const float SignedIntegerRange = ushort.MaxValue; + + /// + /// Expands signed 16-bit components to native or scaled vectors. + /// + /// The source components. Each consecutive group of four components represents one pixel. + /// The destination vectors. + /// Whether native components use signed-normalized values rather than integer values. + /// Whether to map native components to the scaled range. + internal static void ToVector4(ReadOnlySpan source, Span destination, bool normalized, bool scaled) + { + ref short sourceBase = ref MemoryMarshal.GetReference(source); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + int index = 0; + + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + + for (; index <= destination.Length - pixelsPerVector; index += pixelsPerVector) + { + // Packed shorts occupy half the expanded width, so one 256-bit load feeds a complete 512-bit conversion. + Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)(index * Vector128.Count)); + Vector512 integers = Vector512.Create(Vector256.WidenLower(packed), Vector256.WidenUpper(packed)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)index)) = ConvertToVector4(Vector512.ConvertToSingle(integers), normalized, scaled); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + + for (; index <= destination.Length - pixelsPerVector; index += pixelsPerVector) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(index * Vector128.Count)); + Vector256 integers = Vector256.Create(Vector128.WidenLower(packed), Vector128.WidenUpper(packed)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)index)) = ConvertToVector4(Vector256.ConvertToSingle(integers), normalized, scaled); + } + } + + if (Vector128.IsHardwareAccelerated) + { + ref byte sourceBytes = ref Unsafe.As(ref sourceBase); + + for (; index < destination.Length; index++) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBytes, (uint)(index * sizeof(ulong)))); + Vector128 integers = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsInt16()); + Unsafe.Add(ref destinationBase, (uint)index) = ConvertToVector4(Vector128.ConvertToSingle(integers), normalized, scaled).AsVector4(); + } + + return; + } + + // The fallback preserves the format implementations' operation order on platforms without vector acceleration. + for (; index < destination.Length; index++) + { + int componentIndex = index * Vector128.Count; + Vector4 vector = new(source[componentIndex], source[componentIndex + 1], source[componentIndex + 2], source[componentIndex + 3]); + + if (normalized) + { + // Both minimum two's-complement SNORM encodings represent -1. + vector = Vector4.Max(vector, new Vector4(-ShortMaximum)); + + if (scaled) + { + // Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound. + vector += new Vector4(ShortMaximum); + vector /= SignedNormalizedMagnitude; + } + else + { + vector /= ShortMaximum; + } + } + else if (scaled) + { + // SINT uses every two's-complement code, unlike SNORM where both minimum encodings represent -1. + vector -= new Vector4(SignedIntegerMinimum); + vector /= SignedIntegerRange; + } + + Unsafe.Add(ref destinationBase, (uint)index) = vector; + } + } + + /// + /// Packs native or scaled vectors into signed 16-bit components. + /// + /// The source vectors. + /// The destination components. Each consecutive group of four components represents one pixel. + /// Whether native components use signed-normalized values rather than integer values. + /// Whether the source vectors use the scaled range. + internal static void FromVector4(Span source, Span destination, bool normalized, bool scaled) + { + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source); + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + int index = 0; + + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + + for (; index <= source.Length - pixelsPerVector; index += pixelsPerVector) + { + Vector512 vectors = Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)index)); + Vector512 integers = ConvertToInt32(vectors, normalized, scaled); + + // AVX2 packing operates independently in each 128-bit lane, producing pixels 0, 2, 1, 3. Restore the + // source order with a native lane permutation; the portable PackSignedSaturate fallback is already ordered. + Vector256 packed = Vector256_.PackSignedSaturate(integers.GetLower(), integers.GetUpper()); + + if (Avx2.IsSupported) + { + packed = Avx2.Permute4x64(packed.AsInt64(), RestorePackedPixelOrder).AsInt16(); + } + + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)(index * Vector128.Count))) = packed; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + + for (; index <= source.Length - pixelsPerVector; index += pixelsPerVector) + { + Vector256 vectors = Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)index)); + Vector256 integers = ConvertToInt32(vectors, normalized, scaled); + Vector128 packed = Vector128_.PackSignedSaturate(integers.GetLower(), integers.GetUpper()); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)(index * Vector128.Count))) = packed; + } + } + + if (Vector128.IsHardwareAccelerated) + { + ref byte destinationBytes = ref Unsafe.As(ref destinationBase); + + for (; index < source.Length; index++) + { + Vector128 vector = Unsafe.As>(ref Unsafe.Add(ref sourceBase, (uint)index)); + Vector128 integers = ConvertToInt32(vector, normalized, scaled); + Vector128 packed = Vector128_.PackSignedSaturate(integers, integers); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationBytes, (uint)(index * sizeof(ulong))), packed.AsUInt64().GetElement(0)); + } + + return; + } + + for (; index < source.Length; index++) + { + Vector4 vector = Unsafe.Add(ref sourceBase, (uint)index); + + if (normalized) + { + if (scaled) + { + vector *= 2F; + vector -= Vector4.One; + } + + vector *= ShortMaximum; + vector = Numerics.Clamp(vector, new Vector4(-ShortMaximum), new Vector4(ShortMaximum)); + } + else + { + if (scaled) + { + vector *= SignedIntegerRange; + vector += new Vector4(SignedIntegerMinimum); + } + + vector = Numerics.Clamp(vector, new Vector4(short.MinValue), new Vector4(short.MaxValue)); + } + + int componentIndex = index * Vector128.Count; + destination[componentIndex] = (short)MathF.Round(vector.X); + destination[componentIndex + 1] = (short)MathF.Round(vector.Y); + destination[componentIndex + 2] = (short)MathF.Round(vector.Z); + destination[componentIndex + 3] = (short)MathF.Round(vector.W); + } + } + + /// + /// Converts signed integer components to native or scaled vectors while preserving the scalar conversion order. + /// + /// The signed integer components. + /// Whether native components use signed-normalized values. + /// Whether to map native components to the scaled range. + /// The converted vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ConvertToVector4(Vector512 source, bool normalized, bool scaled) + { + if (normalized) + { + // Both minimum two's-complement SNORM encodings represent -1. + source = Vector512.Max(source, Vector512.Create(-ShortMaximum)); + + if (scaled) + { + // Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound. + source += Vector512.Create(ShortMaximum); + source /= Vector512.Create(SignedNormalizedMagnitude); + } + else + { + source /= Vector512.Create(ShortMaximum); + } + } + else if (scaled) + { + source -= Vector512.Create(SignedIntegerMinimum); + source /= Vector512.Create(SignedIntegerRange); + } + + return source; + } + + /// + /// Converts signed integer components to native or scaled vectors while preserving the scalar conversion order. + /// + /// The signed integer components. + /// Whether native components use signed-normalized values. + /// Whether to map native components to the scaled range. + /// The converted vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConvertToVector4(Vector256 source, bool normalized, bool scaled) + { + if (normalized) + { + // Both minimum two's-complement SNORM encodings represent -1. + source = Vector256.Max(source, Vector256.Create(-ShortMaximum)); + + if (scaled) + { + // Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound. + source += Vector256.Create(ShortMaximum); + source /= Vector256.Create(SignedNormalizedMagnitude); + } + else + { + source /= Vector256.Create(ShortMaximum); + } + } + else if (scaled) + { + source -= Vector256.Create(SignedIntegerMinimum); + source /= Vector256.Create(SignedIntegerRange); + } + + return source; + } + + /// + /// Converts signed integer components to native or scaled vectors while preserving the scalar conversion order. + /// + /// The signed integer components. + /// Whether native components use signed-normalized values. + /// Whether to map native components to the scaled range. + /// The converted vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ConvertToVector4(Vector128 source, bool normalized, bool scaled) + { + if (normalized) + { + // Both minimum two's-complement SNORM encodings represent -1. + source = Vector128.Max(source, Vector128.Create(-ShortMaximum)); + + if (scaled) + { + // Offset exact integer components before division to avoid cancellation near the signed-normalized lower bound. + source += Vector128.Create(ShortMaximum); + source /= Vector128.Create(SignedNormalizedMagnitude); + } + else + { + source /= Vector128.Create(ShortMaximum); + } + } + else if (scaled) + { + source -= Vector128.Create(SignedIntegerMinimum); + source /= Vector128.Create(SignedIntegerRange); + } + + return source; + } + + /// + /// Converts vectors to signed integer components using the format's packing contract. + /// + /// The source vectors. + /// Whether native components use signed-normalized values. + /// Whether the source vectors use the scaled range. + /// The converted integer components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ConvertToInt32(Vector512 source, bool normalized, bool scaled) + { + if (normalized) + { + if (scaled) + { + source *= Vector512.Create(2F); + source -= Vector512.One; + } + + source *= Vector512.Create(ShortMaximum); + source = Vector512.Min(Vector512.Max(source, Vector512.Create(-ShortMaximum)), Vector512.Create(ShortMaximum)); + } + else + { + if (scaled) + { + source *= Vector512.Create(SignedIntegerRange); + source += Vector512.Create(SignedIntegerMinimum); + } + + source = Vector512.Min(Vector512.Max(source, Vector512.Create((float)short.MinValue)), Vector512.Create((float)short.MaxValue)); + } + + return Vector512_.ConvertToInt32RoundToEven(source); + } + + /// + /// Converts vectors to signed integer components using the format's packing contract. + /// + /// The source vectors. + /// Whether native components use signed-normalized values. + /// Whether the source vectors use the scaled range. + /// The converted integer components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConvertToInt32(Vector256 source, bool normalized, bool scaled) + { + if (normalized) + { + if (scaled) + { + source *= Vector256.Create(2F); + source -= Vector256.One; + } + + source *= Vector256.Create(ShortMaximum); + source = Vector256.Min(Vector256.Max(source, Vector256.Create(-ShortMaximum)), Vector256.Create(ShortMaximum)); + } + else + { + if (scaled) + { + source *= Vector256.Create(SignedIntegerRange); + source += Vector256.Create(SignedIntegerMinimum); + } + + source = Vector256.Min(Vector256.Max(source, Vector256.Create((float)short.MinValue)), Vector256.Create((float)short.MaxValue)); + } + + return Vector256_.ConvertToInt32RoundToEven(source); + } + + /// + /// Converts vectors to signed integer components using the format's packing contract. + /// + /// The source vectors. + /// Whether native components use signed-normalized values. + /// Whether the source vectors use the scaled range. + /// The converted integer components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ConvertToInt32(Vector128 source, bool normalized, bool scaled) + { + if (normalized) + { + if (scaled) + { + source *= Vector128.Create(2F); + source -= Vector128.One; + } + + source *= Vector128.Create(ShortMaximum); + source = Vector128.Min(Vector128.Max(source, Vector128.Create(-ShortMaximum)), Vector128.Create(ShortMaximum)); + } + else + { + if (scaled) + { + source *= Vector128.Create(SignedIntegerRange); + source += Vector128.Create(SignedIntegerMinimum); + } + + source = Vector128.Min(Vector128.Max(source, Vector128.Create((float)short.MinValue)), Vector128.Create((float)short.MaxValue)); + } + + return Vector128_.ConvertToInt32RoundToEven(source); + } +} diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Affine.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Affine.cs new file mode 100644 index 000000000..637071421 --- /dev/null +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Affine.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.Utils; + +internal static partial class Vector4Converters +{ + /// + /// Multiplies each vector component and then adds the corresponding offset component. + /// + /// The vectors to transform in place. + /// The component-wise multiplier. + /// The component-wise offset applied after multiplication. + public static void MultiplyThenAdd(Span vectors, Vector4 multiplier, Vector4 offset) + => Apply(vectors, new MultiplyThenAddOperator(multiplier, offset)); + + /// + /// Adds the corresponding offset component and then divides each vector component by its divisor. + /// + /// The vectors to transform in place. + /// The component-wise offset applied before division. + /// The component-wise divisor. + public static void AddThenDivide(Span vectors, Vector4 offset, Vector4 divisor) + => Apply(vectors, new AddThenDivideOperator(offset, divisor)); + + /// + /// Applies a stateful component transform to a vector buffer in place. + /// + /// The transform selected for this closed traversal. + /// The vectors to transform. + /// The transform and its component-wise state. + // Closing and inlining the traversal lets the JIT devirtualize every Invoke call, + // specialize the active hardware-width branches, and discard unused operator state. + [MethodImpl(InliningOptions.AlwaysInline)] + private static void Apply(Span vectors, TOperator transform) + where TOperator : struct, IStatefulVector4Operator + { + ref Vector4 vectorBase = ref MemoryMarshal.GetReference(vectors); + int index = 0; + + // A Vector4 is one complete pixel. Descending register widths therefore consume groups + // of four, two, and one pixels without splitting a pixel across traversal boundaries. + if (Vector512.IsHardwareAccelerated) + { + int vectorsPerRegister = Vector512.Count / Vector128.Count; + int oneRegisterFromEnd = vectors.Length - vectorsPerRegister; + + for (; index <= oneRegisterFromEnd; index += vectorsPerRegister) + { + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = transform.Invoke(vector); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorsPerRegister = Vector256.Count / Vector128.Count; + int oneRegisterFromEnd = vectors.Length - vectorsPerRegister; + + for (; index <= oneRegisterFromEnd; index += vectorsPerRegister) + { + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = transform.Invoke(vector); + } + } + + if (Vector128.IsHardwareAccelerated) + { + // Vector128 and Vector4 have the same four-lane layout, so this stage + // consumes every remaining complete pixel and leaves no scalar remainder. + for (; index < vectors.Length; index++) + { + ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = transform.Invoke(vector); + } + + return; + } + + // Vector4 retains the same component order and expression ordering when hardware + // intrinsics are unavailable, preserving the format-specific conversion contract. + for (; index < vectors.Length; index++) + { + ref Vector4 vector = ref Unsafe.Add(ref vectorBase, (uint)index); + vector = transform.Invoke(vector); + } + } +} diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs new file mode 100644 index 000000000..96d81617b --- /dev/null +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.Utils; + +internal static partial class Vector4Converters +{ + /// + /// Defines a stateful component transform for each register width used by the shared traversal. + /// + private interface IStatefulVector4Operator + { + /// + /// Transforms one pixel represented by four components. + /// + /// The source components. + /// The transformed components. + public Vector4 Invoke(Vector4 source); + + /// + /// Transforms one pixel represented by the four single-precision lanes in a 128-bit register. + /// + /// The source components. + /// The transformed components. + public Vector128 Invoke(Vector128 source); + + /// + /// Transforms two pixels represented by the eight single-precision lanes in a 256-bit register. + /// + /// The source components. + /// The transformed components. + public Vector256 Invoke(Vector256 source); + + /// + /// Transforms four pixels represented by the sixteen single-precision lanes in a 512-bit register. + /// + /// The source components. + /// The transformed components. + public Vector512 Invoke(Vector512 source); + } + + /// + /// Carries the component state for a multiply-then-add transform. + /// + private readonly struct MultiplyThenAddOperator : IStatefulVector4Operator + { + private readonly Vector512 multiplier; + private readonly Vector512 offset; + + /// + /// Initializes a new instance of the struct. + /// + /// The component-wise multiplier. + /// The component-wise offset applied after multiplication. + public MultiplyThenAddOperator(Vector4 multiplier, Vector4 offset) + { + Vector128 multiplier128 = multiplier.AsVector128(); + Vector128 offset128 = offset.AsVector128(); + Vector256 multiplier256 = Vector256.Create(multiplier128, multiplier128); + Vector256 offset256 = Vector256.Create(offset128, offset128); + + // Expanding the invariant state once prevents the width-specific Invoke methods + // from rebuilding identical lane groups for every vector processed by the loop. + this.multiplier = Vector512.Create(multiplier256, multiplier256); + this.offset = Vector512.Create(offset256, offset256); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Invoke(Vector4 source) + { + Vector128 result = (source.AsVector128() * this.multiplier.GetLower().GetLower()) + this.offset.GetLower().GetLower(); + + return result.AsVector4(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 source) + => (source * this.multiplier.GetLower().GetLower()) + this.offset.GetLower().GetLower(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 source) + => (source * this.multiplier.GetLower()) + this.offset.GetLower(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 source) + => (source * this.multiplier) + this.offset; + } + + /// + /// Carries the component state for an add-then-divide transform. + /// + private readonly struct AddThenDivideOperator : IStatefulVector4Operator + { + private readonly Vector512 offset; + private readonly Vector512 divisor; + + /// + /// Initializes a new instance of the struct. + /// + /// The component-wise offset applied before division. + /// The component-wise divisor. + public AddThenDivideOperator(Vector4 offset, Vector4 divisor) + { + Vector128 offset128 = offset.AsVector128(); + Vector128 divisor128 = divisor.AsVector128(); + Vector256 offset256 = Vector256.Create(offset128, offset128); + Vector256 divisor256 = Vector256.Create(divisor128, divisor128); + + // All register widths consume prefixes of this repeated four-pixel state, + // so one construction serves the wide loop and every narrower remainder. + this.offset = Vector512.Create(offset256, offset256); + this.divisor = Vector512.Create(divisor256, divisor256); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Invoke(Vector4 source) + { + Vector128 result = (source.AsVector128() + this.offset.GetLower().GetLower()) / this.divisor.GetLower().GetLower(); + + return result.AsVector4(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 source) + => (source + this.offset.GetLower().GetLower()) / this.divisor.GetLower().GetLower(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 source) + => (source + this.offset.GetLower()) / this.divisor.GetLower(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 source) + => (source + this.offset) / this.divisor; + } +} diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs new file mode 100644 index 000000000..b47779df8 --- /dev/null +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AssociatedRgbaCompatible.cs @@ -0,0 +1,892 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.PixelFormats.Utils; + +/// +/// Contains . +/// +internal static partial class Vector4Converters +{ + /// + /// Provides efficient batched conversion for four-byte pixel types that store premultiplied alpha. + /// + public static class AssociatedRgbaCompatible + { + /// + /// Converts associated RGBA byte components to an unassociated scaled vector. + /// + /// The associated red component. + /// The associated green component. + /// The associated blue component. + /// The alpha component. + /// The unassociated scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Vector4 ToUnassociatedVector4(byte red, byte green, byte blue, byte alpha) + { + // Divide the original byte magnitudes before normalization so a floating-point intermediate cannot move an exact byte conversion across its rounding midpoint. + Vector4 vector = new(red, green, blue, alpha); + float colorDivisor = alpha == 0 ? byte.MaxValue : alpha; + return vector / new Vector4(colorDivisor, colorDivisor, colorDivisor, byte.MaxValue); + } + + /// + /// Converts packed RGBA byte magnitudes to unassociated scaled vectors. + /// + /// The packed RGBA bytes. + /// The destination vectors. + private static void ToUnassociatedVector4(ReadOnlySpan source, Span destination) + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + + // Each packed component occupies one byte, so the destination Vector4 lane count also defines the source byte stride. + int componentsPerPixel = Vector128.Count; + int i = 0; + + // Portable widening advances one integer width at a time, so assemble the wider vectors from ordered 128-bit halves. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / componentsPerPixel; + + for (; i <= destination.Length - pixelsPerVector; i += pixelsPerVector) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)); + Vector128 lowerShorts = Vector128.WidenLower(packed); + Vector128 upperShorts = Vector128.WidenUpper(packed); + Vector256 lowerIntegers = Vector256.Create(Vector128.WidenLower(lowerShorts), Vector128.WidenUpper(lowerShorts)); + Vector256 upperIntegers = Vector256.Create(Vector128.WidenLower(upperShorts), Vector128.WidenUpper(upperShorts)); + Vector512 integers = Vector512.Create(lowerIntegers, upperIntegers).AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector512.ConvertToSingle(integers)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / componentsPerPixel; + + for (; i <= destination.Length - pixelsPerVector; i += pixelsPerVector) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref sourceBase, (uint)(i * componentsPerPixel))); + Vector128 shorts = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + Vector256 integers = Vector256.Create(Vector128.WidenLower(shorts), Vector128.WidenUpper(shorts)).AsInt32(); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, (uint)i)) = ToUnassociatedVector4(Vector256.ConvertToSingle(integers)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int pixelsPerVector = Vector128.Count / componentsPerPixel; + + for (; i <= destination.Length - pixelsPerVector; i += pixelsPerVector) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i * componentsPerPixel)); + (Vector128 shorts0, Vector128 shorts1) = Vector128.Widen(packed); + (Vector128 integers0, Vector128 integers1) = Vector128.Widen(shorts0); + (Vector128 integers2, Vector128 integers3) = Vector128.Widen(shorts1); + + Unsafe.Add(ref destinationBase, (uint)i) = ToUnassociatedVector4(Vector128.ConvertToSingle(integers0.AsInt32())).AsVector4(); + Unsafe.Add(ref destinationBase, (uint)i + 1u) = ToUnassociatedVector4(Vector128.ConvertToSingle(integers1.AsInt32())).AsVector4(); + Unsafe.Add(ref destinationBase, (uint)i + 2u) = ToUnassociatedVector4(Vector128.ConvertToSingle(integers2.AsInt32())).AsVector4(); + Unsafe.Add(ref destinationBase, (uint)i + 3u) = ToUnassociatedVector4(Vector128.ConvertToSingle(integers3.AsInt32())).AsVector4(); + } + } + + for (; i < destination.Length; i++) + { + int offset = i * componentsPerPixel; + + Unsafe.Add(ref destinationBase, (uint)i) = ToUnassociatedVector4( + Unsafe.Add(ref sourceBase, (uint)offset), + Unsafe.Add(ref sourceBase, (uint)offset + 1u), + Unsafe.Add(ref sourceBase, (uint)offset + 2u), + Unsafe.Add(ref sourceBase, (uint)offset + 3u)); + } + } + + /// + /// Converts four associated RGBA byte-magnitude vectors to unassociated scaled vectors. + /// + /// The associated byte-magnitude vectors. + /// The unassociated scaled vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ToUnassociatedVector4(Vector512 source) + { + Vector512 zero = Vector512.Zero; + Vector512 byteMax = Vector512.Create((float)byte.MaxValue); + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + + // Zero-alpha storage has no alpha divisor, while W always uses the byte normalization divisor. + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + Vector512 normalizeMask = Vector512.Equals(alpha, zero) | alphaMask; + Vector512 divisor = Vector512.ConditionalSelect(normalizeMask, byteMax, alpha); + return source / divisor; + } + + /// + /// Converts two associated RGBA byte-magnitude vectors to unassociated scaled vectors. + /// + /// The associated byte-magnitude vectors. + /// The unassociated scaled vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ToUnassociatedVector4(Vector256 source) + { + Vector256 zero = Vector256.Zero; + Vector256 byteMax = Vector256.Create((float)byte.MaxValue); + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + + // Zero-alpha storage has no alpha divisor, while W always uses the byte normalization divisor. + Vector256 alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + Vector256 normalizeMask = Vector256.Equals(alpha, zero) | alphaMask; + Vector256 divisor = Vector256.ConditionalSelect(normalizeMask, byteMax, alpha); + return source / divisor; + } + + /// + /// Converts an associated RGBA byte-magnitude vector to an unassociated scaled vector. + /// + /// The associated byte-magnitude vector. + /// The unassociated scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ToUnassociatedVector4(Vector128 source) + { + Vector128 zero = Vector128.Zero; + Vector128 byteMax = Vector128.Create((float)byte.MaxValue); + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + + // Zero-alpha storage has no alpha divisor, while W always uses the byte normalization divisor. + Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); + Vector128 normalizeMask = Vector128.Equals(alpha, zero) | alphaMask; + Vector128 divisor = Vector128.ConditionalSelect(normalizeMask, byteMax, alpha); + return source / divisor; + } + + /// + /// Converts unassociated scaled vectors to associated byte magnitudes. + /// + /// The vectors to convert in place. + private static void AssociateToByte(Span source) + { + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + ref Vector512 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector512 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length / (uint)pixelsPerVector); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = AssociateToByte(sourceBase); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + // SIMD widths are powers of two, so masking finds the start of the unprocessed remainder without division. + source = source[(source.Length & ~(pixelsPerVector - 1))..]; + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length / (uint)pixelsPerVector); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = AssociateToByte(sourceBase); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + // SIMD widths are powers of two, so masking finds the start of the unprocessed remainder without division. + source = source[(source.Length & ~(pixelsPerVector - 1))..]; + } + + if (Vector128.IsHardwareAccelerated) + { + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector128 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)source.Length); + + while (Unsafe.IsAddressLessThan(ref sourceBase, ref sourceEnd)) + { + sourceBase = AssociateToByte(sourceBase); + sourceBase = ref Unsafe.Add(ref sourceBase, 1); + } + + return; + } + + ref Vector4 tailBase = ref MemoryMarshal.GetReference(source); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref tailBase, i) = AssociateToByte(Unsafe.Add(ref tailBase, i)); + } + } + + /// + /// Converts four unassociated scaled vectors to associated byte magnitudes. + /// + /// The unassociated scaled vectors. + /// The associated byte magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 AssociateToByte(Vector512 source) + { + Vector512 zero = Vector512.Zero; + Vector512 one = Vector512.One; + Vector512 byteMax = Vector512.Create((float)byte.MaxValue); + source = Vector512.Min(Vector512.Max(source, zero), one); + Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); + Vector512 storedAlpha = Vector512.Floor((alpha * byteMax) + Vector512.Create(.5F)); + Vector512 result = source * storedAlpha; + + // RGB is associated using the quantized alpha byte, while W stores that byte directly. + 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, storedAlpha, result); + } + + /// + /// Converts two unassociated scaled vectors to associated byte magnitudes. + /// + /// The unassociated scaled vectors. + /// The associated byte magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 AssociateToByte(Vector256 source) + { + Vector256 zero = Vector256.Zero; + Vector256 one = Vector256.One; + Vector256 byteMax = Vector256.Create((float)byte.MaxValue); + source = Vector256.Min(Vector256.Max(source, zero), one); + Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); + Vector256 storedAlpha = Vector256.Floor((alpha * byteMax) + Vector256.Create(.5F)); + Vector256 result = source * storedAlpha; + + // RGB is associated using the quantized alpha byte, while W stores that byte directly. + Vector256 alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + return Vector256.ConditionalSelect(alphaMask, storedAlpha, result); + } + + /// + /// Converts an unassociated scaled vector to associated byte magnitudes. + /// + /// The unassociated scaled vector. + /// The associated byte magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AssociateToByte(Vector128 source) + { + Vector128 zero = Vector128.Zero; + Vector128 one = Vector128.One; + Vector128 byteMax = Vector128.Create((float)byte.MaxValue); + source = Vector128.Min(Vector128.Max(source, zero), one); + Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); + Vector128 storedAlpha = Vector128.Floor((alpha * byteMax) + Vector128.Create(.5F)); + Vector128 result = source * storedAlpha; + + // RGB is associated using the quantized alpha byte, while W stores that byte directly. + Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); + return Vector128.ConditionalSelect(alphaMask, storedAlpha, result); + } + + /// + /// Converts an unassociated scaled vector to associated byte magnitudes. + /// + /// The unassociated scaled vector. + /// The associated byte magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 AssociateToByte(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + float storedAlpha = (byte)Numerics.Clamp((source.W * byte.MaxValue) + 0.5F, 0, byte.MaxValue); + + source *= storedAlpha; + source.W = storedAlpha; + return source; + } + + /// + /// Converts an unassociated scaled vector to an pixel. + /// + /// The unassociated scaled vector. + /// The associated pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Rgba32P FromUnassociatedVector4ToRgba32P(Vector4 source) + { + source = AssociateToByte(source); + return new Rgba32P(ConvertToByte(source.X), ConvertToByte(source.Y), ConvertToByte(source.Z), ConvertToByte(source.W)); + } + + /// + /// Converts an unassociated scaled vector to a pixel. + /// + /// The unassociated scaled vector. + /// The associated pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Bgra32P FromUnassociatedVector4ToBgra32P(Vector4 source) + { + source = AssociateToByte(source); + return new Bgra32P(ConvertToByte(source.X), ConvertToByte(source.Y), ConvertToByte(source.Z), ConvertToByte(source.W)); + } + + /// + /// Converts an unassociated scaled vector to an pixel. + /// + /// The unassociated scaled vector. + /// The associated pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Argb32P FromUnassociatedVector4ToArgb32P(Vector4 source) + { + source = AssociateToByte(source); + return new Argb32P(ConvertToByte(source.X), ConvertToByte(source.Y), ConvertToByte(source.Z), ConvertToByte(source.W)); + } + + /// + /// Converts an unassociated scaled vector to an pixel. + /// + /// The unassociated scaled vector. + /// The associated pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Abgr32P FromUnassociatedVector4ToAbgr32P(Vector4 source) + { + source = AssociateToByte(source); + return new Abgr32P(ConvertToByte(source.X), ConvertToByte(source.Y), ConvertToByte(source.Z), ConvertToByte(source.W)); + } + + /// + /// Converts an associated scaled vector to associated byte magnitudes using the alpha byte the destination stores. + /// + /// The associated scaled vector. + /// The associated byte magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 ReassociateToByte(Vector4 source) + { + float alpha = source.W; + + if (alpha <= 0) + { + return Vector4.Zero; + } + + float byteAlpha = alpha * byte.MaxValue; + float storedAlpha = (byte)Numerics.Clamp(byteAlpha + 0.5F, 0, byte.MaxValue); + + if (byteAlpha == storedAlpha) + { + // Quantization leaves alpha unchanged, so RGB is already associated correctly. Avoiding division preserves exact byte midpoints produced by scaling stored components. + source *= byte.MaxValue; + } + else + { + // Recover straight RGB before multiplying by the stored alpha byte. Keeping the association in byte magnitude preserves exact half-byte rounding boundaries. + Numerics.UnPremultiply(ref source); + source *= storedAlpha; + } + + source.W = storedAlpha; + Numerics.ClampRgbToAlpha(ref source); + return source; + } + + /// + /// Converts associated scaled vectors to associated byte magnitudes using the alpha bytes the destination stores. + /// + /// The vectors to convert in place. + private static void ReassociateToByte(Span source) + { + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerVector = Vector512.Count / Vector128.Count; + ref Vector512 vectorSourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector512 sourceEnd = ref Unsafe.Add(ref vectorSourceBase, (uint)source.Length / (uint)pixelsPerVector); + + while (Unsafe.IsAddressLessThan(ref vectorSourceBase, ref sourceEnd)) + { + vectorSourceBase = ReassociateToByte(vectorSourceBase); + vectorSourceBase = ref Unsafe.Add(ref vectorSourceBase, 1); + } + + // SIMD widths are powers of two, so masking finds the start of the unprocessed remainder without division. + source = source[(source.Length & ~(pixelsPerVector - 1))..]; + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerVector = Vector256.Count / Vector128.Count; + ref Vector256 vectorSourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 sourceEnd = ref Unsafe.Add(ref vectorSourceBase, (uint)source.Length / (uint)pixelsPerVector); + + while (Unsafe.IsAddressLessThan(ref vectorSourceBase, ref sourceEnd)) + { + vectorSourceBase = ReassociateToByte(vectorSourceBase); + vectorSourceBase = ref Unsafe.Add(ref vectorSourceBase, 1); + } + + // SIMD widths are powers of two, so masking finds the start of the unprocessed remainder without division. + source = source[(source.Length & ~(pixelsPerVector - 1))..]; + } + + if (Vector128.IsHardwareAccelerated) + { + ref Vector128 vectorSourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector128 sourceEnd = ref Unsafe.Add(ref vectorSourceBase, (uint)source.Length); + + while (Unsafe.IsAddressLessThan(ref vectorSourceBase, ref sourceEnd)) + { + vectorSourceBase = ReassociateToByte(vectorSourceBase); + vectorSourceBase = ref Unsafe.Add(ref vectorSourceBase, 1); + } + + return; + } + + ref Vector4 sourceBase = ref MemoryMarshal.GetReference(source); + + for (nuint i = 0; i < (uint)source.Length; i++) + { + Unsafe.Add(ref sourceBase, i) = ReassociateToByte(Unsafe.Add(ref sourceBase, i)); + } + } + + /// + /// Converts four associated scaled vectors to associated byte magnitudes using the alpha bytes the destination stores. + /// + /// The associated scaled vectors. + /// The associated byte magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 ReassociateToByte(Vector512 source) + { + Vector512 zero = Vector512.Zero; + Vector512 byteMax = Vector512.Create((float)byte.MaxValue); + Vector512 alpha = Vector512.Max(Vector512_.ShuffleNative(source, 0b_11_11_11_11), zero); + Vector512 byteAlpha = alpha * byteMax; + Vector512 storedAlpha = Vector512.Floor(Vector512.Min(Vector512.Max(byteAlpha + Vector512.Create(.5F), zero), byteMax)); + Vector512 result = (source / alpha) * storedAlpha; + + // Exact byte alpha values need no reassociation. Multiplying by 255 directly preserves RGB values that already lie on byte midpoints. + result = Vector512.ConditionalSelect(Vector512.Equals(byteAlpha, storedAlpha), source * byteMax, result); + Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + return Vector512.ConditionalSelect(Vector512.Equals(alpha, zero), zero, result); + } + + /// + /// Converts two associated scaled vectors to associated byte magnitudes using the alpha bytes the destination stores. + /// + /// The associated scaled vectors. + /// The associated byte magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ReassociateToByte(Vector256 source) + { + Vector256 zero = Vector256.Zero; + Vector256 byteMax = Vector256.Create((float)byte.MaxValue); + Vector256 alpha = Vector256.Max(Vector256_.ShuffleNative(source, 0b_11_11_11_11), zero); + Vector256 byteAlpha = alpha * byteMax; + Vector256 storedAlpha = Vector256.Floor(Vector256.Min(Vector256.Max(byteAlpha + Vector256.Create(.5F), zero), byteMax)); + Vector256 result = (source / alpha) * storedAlpha; + + // Exact byte alpha values need no reassociation. Multiplying by 255 directly preserves RGB values that already lie on byte midpoints. + result = Vector256.ConditionalSelect(Vector256.Equals(byteAlpha, storedAlpha), source * byteMax, result); + Vector256 alphaMask = Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); + result = Vector256.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + return Vector256.ConditionalSelect(Vector256.Equals(alpha, zero), zero, result); + } + + /// + /// Converts an associated scaled vector to associated byte magnitudes using the alpha byte the destination stores. + /// + /// The associated scaled vector. + /// The associated byte magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ReassociateToByte(Vector128 source) + { + Vector128 zero = Vector128.Zero; + Vector128 byteMax = Vector128.Create((float)byte.MaxValue); + Vector128 alpha = Vector128.Max(Vector128_.ShuffleNative(source, 0b_11_11_11_11), zero); + Vector128 byteAlpha = alpha * byteMax; + Vector128 storedAlpha = Vector128.Floor(Vector128.Min(Vector128.Max(byteAlpha + Vector128.Create(.5F), zero), byteMax)); + Vector128 result = (source / alpha) * storedAlpha; + + // Exact byte alpha values need no reassociation. Multiplying by 255 directly preserves RGB values that already lie on byte midpoints. + result = Vector128.ConditionalSelect(Vector128.Equals(byteAlpha, storedAlpha), source * byteMax, result); + Vector128 alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle(); + result = Vector128.ConditionalSelect(alphaMask, storedAlpha, result); + result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + return Vector128.ConditionalSelect(Vector128.Equals(alpha, zero), zero, result); + } + + /// + /// Converts an associated scaled vector to an pixel. + /// + /// The associated scaled vector. + /// The associated pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Rgba32P FromAssociatedVector4ToRgba32P(Vector4 source) + { + source = ReassociateToByte(source); + return new Rgba32P(ConvertToByte(source.X), ConvertToByte(source.Y), ConvertToByte(source.Z), ConvertToByte(source.W)); + } + + /// + /// Converts an associated scaled vector to a pixel. + /// + /// The associated scaled vector. + /// The associated pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Bgra32P FromAssociatedVector4ToBgra32P(Vector4 source) + { + source = ReassociateToByte(source); + return new Bgra32P(ConvertToByte(source.X), ConvertToByte(source.Y), ConvertToByte(source.Z), ConvertToByte(source.W)); + } + + /// + /// Converts an associated scaled vector to an pixel. + /// + /// The associated scaled vector. + /// The associated pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Argb32P FromAssociatedVector4ToArgb32P(Vector4 source) + { + source = ReassociateToByte(source); + return new Argb32P(ConvertToByte(source.X), ConvertToByte(source.Y), ConvertToByte(source.Z), ConvertToByte(source.W)); + } + + /// + /// Converts an associated scaled vector to an pixel. + /// + /// The associated scaled vector. + /// The associated pixel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Abgr32P FromAssociatedVector4ToAbgr32P(Vector4 source) + { + source = ReassociateToByte(source); + return new Abgr32P(ConvertToByte(source.X), ConvertToByte(source.Y), ConvertToByte(source.Z), ConvertToByte(source.W)); + } + + /// + /// Converts associated RGBA pixels to associated scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ToAssociatedVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + SimdUtils.ByteToNormalizedFloat(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts premultiplied RGBA pixels to unassociated scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + internal static void ToUnassociatedVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + ToUnassociatedVector4(MemoryMarshal.Cast(source), destination[..source.Length]); + } + + /// + /// Converts unassociated scaled vectors to premultiplied RGBA pixels. + /// + /// The source vectors, modified in place during conversion. + /// The destination pixels. + internal static void FromUnassociatedVector4(Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + AssociateToByte(source); + SimdUtils.FloatToByteSaturate(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts associated scaled vectors to premultiplied RGBA pixels. + /// + /// The source vectors, modified in place during conversion. + /// The destination pixels. + internal static void FromAssociatedVector4(Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + ReassociateToByte(source); + SimdUtils.FloatToByteSaturate(MemoryMarshal.Cast(source), MemoryMarshal.Cast(destination)); + } + + /// + /// Converts associated BGRA pixels to associated scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ToAssociatedVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + if (source.IsEmpty) + { + return; + } + + // ByteToNormalizedFloat preserves component order, so the packed BGRA bytes must first be shuffled to RGBA. + // Reuse the unwritten end of the larger Vector4 destination as staging to avoid a temporary allocation. + // The final vector overlaps that staging, so its source pixel is converted after the staged bytes are consumed. + int lastIndex = source.Length - 1; + Span temporary = MemoryMarshal.Cast(destination).Slice((3 * source.Length) + 1, lastIndex); + PixelConverter.FromBgra32.ToRgba32(MemoryMarshal.Cast(source[..lastIndex]), MemoryMarshal.Cast(temporary)); + SimdUtils.ByteToNormalizedFloat(MemoryMarshal.Cast(temporary), MemoryMarshal.Cast(destination[..lastIndex])); + destination[lastIndex] = source[lastIndex].ToVector4(); + } + + /// + /// Converts premultiplied BGRA pixels to unassociated scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + internal static void ToUnassociatedVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + if (source.IsEmpty) + { + return; + } + + // Stage all but the final pixel as RGBA bytes in the unwritten end of the expanding Vector4 destination. + // The final expanded vector overlaps that staging region, so convert it directly after the staged bytes are consumed. + int lastIndex = source.Length - 1; + Span temporary = MemoryMarshal.Cast(destination).Slice((3 * source.Length) + 1, lastIndex); + PixelConverter.FromBgra32.ToRgba32(MemoryMarshal.Cast(source[..lastIndex]), MemoryMarshal.Cast(temporary)); + ToUnassociatedVector4(MemoryMarshal.Cast(temporary), destination[..lastIndex]); + destination[lastIndex] = ToUnassociatedVector4(source[lastIndex].R, source[lastIndex].G, source[lastIndex].B, source[lastIndex].A); + } + + /// + /// Converts unassociated scaled vectors to premultiplied BGRA pixels. + /// + /// The source vectors, modified in place during conversion. + /// The destination pixels. + internal static void FromUnassociatedVector4(Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + AssociateToByte(source); + + // FloatToByteSaturate emits RGBA bytes, which can be shuffled in place to avoid a temporary pixel buffer. + Span destinationBytes = MemoryMarshal.Cast(destination); + SimdUtils.FloatToByteSaturate(MemoryMarshal.Cast(source), destinationBytes); + PixelConverter.FromRgba32.ToBgra32(destinationBytes, destinationBytes); + } + + /// + /// Converts associated scaled vectors to premultiplied BGRA pixels. + /// + /// The source vectors, modified in place during conversion. + /// The destination pixels. + internal static void FromAssociatedVector4(Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + ReassociateToByte(source); + + Span destinationBytes = MemoryMarshal.Cast(destination); + SimdUtils.FloatToByteSaturate(MemoryMarshal.Cast(source), destinationBytes); + PixelConverter.FromRgba32.ToBgra32(destinationBytes, destinationBytes); + } + + /// + /// Converts associated ARGB pixels to associated scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ToAssociatedVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + if (source.IsEmpty) + { + return; + } + + // ByteToNormalizedFloat preserves component order, so the packed ARGB bytes must first be shuffled to RGBA. + // Reuse the unwritten end of the larger Vector4 destination as staging to avoid a temporary allocation. + // The final vector overlaps that staging, so its source pixel is converted after the staged bytes are consumed. + int lastIndex = source.Length - 1; + Span temporary = MemoryMarshal.Cast(destination).Slice((3 * source.Length) + 1, lastIndex); + PixelConverter.FromArgb32.ToRgba32(MemoryMarshal.Cast(source[..lastIndex]), MemoryMarshal.Cast(temporary)); + SimdUtils.ByteToNormalizedFloat(MemoryMarshal.Cast(temporary), MemoryMarshal.Cast(destination[..lastIndex])); + destination[lastIndex] = source[lastIndex].ToVector4(); + } + + /// + /// Converts premultiplied ARGB pixels to unassociated scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + internal static void ToUnassociatedVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + if (source.IsEmpty) + { + return; + } + + // Stage all but the final pixel as RGBA bytes in the unwritten end of the expanding Vector4 destination. + // The final expanded vector overlaps that staging region, so convert it directly after the staged bytes are consumed. + int lastIndex = source.Length - 1; + Span temporary = MemoryMarshal.Cast(destination).Slice((3 * source.Length) + 1, lastIndex); + PixelConverter.FromArgb32.ToRgba32(MemoryMarshal.Cast(source[..lastIndex]), MemoryMarshal.Cast(temporary)); + ToUnassociatedVector4(MemoryMarshal.Cast(temporary), destination[..lastIndex]); + destination[lastIndex] = ToUnassociatedVector4(source[lastIndex].R, source[lastIndex].G, source[lastIndex].B, source[lastIndex].A); + } + + /// + /// Converts unassociated scaled vectors to premultiplied ARGB pixels. + /// + /// The source vectors, modified in place during conversion. + /// The destination pixels. + internal static void FromUnassociatedVector4(Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + AssociateToByte(source); + + // FloatToByteSaturate emits RGBA bytes, which can be shuffled in place to avoid a temporary pixel buffer. + Span destinationBytes = MemoryMarshal.Cast(destination); + SimdUtils.FloatToByteSaturate(MemoryMarshal.Cast(source), destinationBytes); + PixelConverter.FromRgba32.ToArgb32(destinationBytes, destinationBytes); + } + + /// + /// Converts associated scaled vectors to premultiplied ARGB pixels. + /// + /// The source vectors, modified in place during conversion. + /// The destination pixels. + internal static void FromAssociatedVector4(Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + ReassociateToByte(source); + + Span destinationBytes = MemoryMarshal.Cast(destination); + SimdUtils.FloatToByteSaturate(MemoryMarshal.Cast(source), destinationBytes); + PixelConverter.FromRgba32.ToArgb32(destinationBytes, destinationBytes); + } + + /// + /// Converts associated ABGR pixels to associated scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ToAssociatedVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + if (source.IsEmpty) + { + return; + } + + // ByteToNormalizedFloat preserves component order, so the packed ABGR bytes must first be shuffled to RGBA. + // Reuse the unwritten end of the larger Vector4 destination as staging to avoid a temporary allocation. + // The final vector overlaps that staging, so its source pixel is converted after the staged bytes are consumed. + int lastIndex = source.Length - 1; + Span temporary = MemoryMarshal.Cast(destination).Slice((3 * source.Length) + 1, lastIndex); + PixelConverter.FromAbgr32.ToRgba32(MemoryMarshal.Cast(source[..lastIndex]), MemoryMarshal.Cast(temporary)); + SimdUtils.ByteToNormalizedFloat(MemoryMarshal.Cast(temporary), MemoryMarshal.Cast(destination[..lastIndex])); + destination[lastIndex] = source[lastIndex].ToVector4(); + } + + /// + /// Converts premultiplied ABGR pixels to unassociated scaled vectors. + /// + /// The source pixels. + /// The destination vectors. + internal static void ToUnassociatedVector4(ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + if (source.IsEmpty) + { + return; + } + + // Stage all but the final pixel as RGBA bytes in the unwritten end of the expanding Vector4 destination. + // The final expanded vector overlaps that staging region, so convert it directly after the staged bytes are consumed. + int lastIndex = source.Length - 1; + Span temporary = MemoryMarshal.Cast(destination).Slice((3 * source.Length) + 1, lastIndex); + PixelConverter.FromAbgr32.ToRgba32(MemoryMarshal.Cast(source[..lastIndex]), MemoryMarshal.Cast(temporary)); + ToUnassociatedVector4(MemoryMarshal.Cast(temporary), destination[..lastIndex]); + destination[lastIndex] = ToUnassociatedVector4(source[lastIndex].R, source[lastIndex].G, source[lastIndex].B, source[lastIndex].A); + } + + /// + /// Converts unassociated scaled vectors to premultiplied ABGR pixels. + /// + /// The source vectors, modified in place during conversion. + /// The destination pixels. + internal static void FromUnassociatedVector4(Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + AssociateToByte(source); + + // FloatToByteSaturate emits RGBA bytes, which can be shuffled in place to avoid a temporary pixel buffer. + Span destinationBytes = MemoryMarshal.Cast(destination); + SimdUtils.FloatToByteSaturate(MemoryMarshal.Cast(source), destinationBytes); + PixelConverter.FromRgba32.ToAbgr32(destinationBytes, destinationBytes); + } + + /// + /// Converts associated scaled vectors to premultiplied ABGR pixels. + /// + /// The source vectors, modified in place during conversion. + /// The destination pixels. + internal static void FromAssociatedVector4(Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + ReassociateToByte(source); + + Span destinationBytes = MemoryMarshal.Cast(destination); + SimdUtils.FloatToByteSaturate(MemoryMarshal.Cast(source), destinationBytes); + PixelConverter.FromRgba32.ToAbgr32(destinationBytes, destinationBytes); + } + + /// + /// Converts a floating-point byte magnitude to a byte. + /// + /// The byte magnitude. + /// The rounded and clamped byte. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ConvertToByte(float value) => (byte)Numerics.Clamp(value + .5F, 0, byte.MaxValue); + } +} diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Default.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Default.cs index d7cac1530..05799961d 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Default.cs +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Default.cs @@ -50,15 +50,43 @@ internal static partial class Vector4Converters PixelConversionModifiers modifiers) where TPixel : unmanaged, IPixel { - ApplyBackwardConversionModifiers(source, modifiers); + bool scaled = modifiers.IsDefined(PixelConversionModifiers.Scale); - if (modifiers.IsDefined(PixelConversionModifiers.Scale)) + if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand)) { - UnsafeFromScaledVector4Core(source, destination); + // Transfer functions consume straight color, so preserve the established modifier order before selecting a scalar storage conversion. + ApplyBackwardConversionModifiers(source, modifiers); + + if (scaled) + { + UnsafeFromUnassociatedScaledVector4Core(source, destination); + } + else + { + UnsafeFromUnassociatedVector4Core(source, destination); + } + + return; + } + + if (scaled) + { + if (modifiers.IsDefined(PixelConversionModifiers.Premultiply)) + { + UnsafeFromAssociatedScaledVector4Core(source, destination); + } + else + { + UnsafeFromUnassociatedScaledVector4Core(source, destination); + } + } + else if (modifiers.IsDefined(PixelConversionModifiers.Premultiply)) + { + UnsafeFromAssociatedVector4Core(source, destination); } else { - UnsafeFromVector4Core(source, destination); + UnsafeFromUnassociatedVector4Core(source, destination); } } @@ -69,20 +97,82 @@ internal static partial class Vector4Converters PixelConversionModifiers modifiers) where TPixel : unmanaged, IPixel { - if (modifiers.IsDefined(PixelConversionModifiers.Scale)) + // Conversion length is defined by the source. Limiting the destination here also keeps modifiers from + // changing spare capacity supplied by callers of either the checked or unchecked conversion path. + destination = destination[..source.Length]; + + bool scaled = modifiers.IsDefined(PixelConversionModifiers.Scale); + + if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand)) { - UnsafeToScaledVector4Core(source, destination); + if (scaled) + { + UnsafeToUnassociatedScaledVector4Core(source, destination); + } + else + { + UnsafeToUnassociatedVector4Core(source, destination); + } + + // Transfer functions consume straight color; association is applied only after expansion. + ApplyForwardConversionModifiers(destination, modifiers); + return; + } + + if (scaled) + { + if (modifiers.IsDefined(PixelConversionModifiers.Premultiply)) + { + UnsafeToAssociatedScaledVector4Core(source, destination); + } + else + { + UnsafeToUnassociatedScaledVector4Core(source, destination); + } + } + else if (modifiers.IsDefined(PixelConversionModifiers.Premultiply)) + { + UnsafeToAssociatedVector4Core(source, destination); } else { - UnsafeToVector4Core(source, destination); + UnsafeToUnassociatedVector4Core(source, destination); } + } - ApplyForwardConversionModifiers(destination, modifiers); + /// + /// Converts unassociated native-range vectors to pixels without checking span lengths. + /// + /// The destination pixel format. + /// The source vectors. + /// The destination pixels. Its length must be at least length. + [MethodImpl(InliningOptions.ShortMethod)] + private static void UnsafeFromUnassociatedVector4Core( + ReadOnlySpan source, + Span destination) + where TPixel : unmanaged, IPixel + { + ref Vector4 sourceStart = ref MemoryMarshal.GetReference(source); + ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceStart, (uint)source.Length); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationBase = TPixel.FromUnassociatedVector4(sourceStart); + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + } } + /// + /// Converts associated native-range vectors to pixels without checking span lengths. + /// + /// The destination pixel format. + /// The source vectors. + /// The destination pixels. Its length must be at least length. [MethodImpl(InliningOptions.ShortMethod)] - private static void UnsafeFromVector4Core( + private static void UnsafeFromAssociatedVector4Core( ReadOnlySpan source, Span destination) where TPixel : unmanaged, IPixel @@ -93,15 +183,46 @@ internal static partial class Vector4Converters while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) { - destinationBase = TPixel.FromVector4(sourceStart); + destinationBase = TPixel.FromAssociatedVector4(sourceStart); + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + } + } + + /// + /// Converts pixels to unassociated native-range vectors without checking span lengths. + /// + /// The source pixel format. + /// The source pixels. + /// The destination vectors. Its length must be at least length. + [MethodImpl(InliningOptions.ShortMethod)] + private static void UnsafeToUnassociatedVector4Core( + ReadOnlySpan source, + Span destination) + where TPixel : unmanaged, IPixel + { + ref TPixel sourceStart = ref MemoryMarshal.GetReference(source); + ref TPixel sourceEnd = ref Unsafe.Add(ref sourceStart, (uint)source.Length); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationBase = sourceStart.ToUnassociatedVector4(); sourceStart = ref Unsafe.Add(ref sourceStart, 1); destinationBase = ref Unsafe.Add(ref destinationBase, 1); } } + /// + /// Converts pixels to associated native-range vectors without checking span lengths. + /// + /// The source pixel format. + /// The source pixels. + /// The destination vectors. Its length must be at least length. [MethodImpl(InliningOptions.ShortMethod)] - private static void UnsafeToVector4Core( + private static void UnsafeToAssociatedVector4Core( ReadOnlySpan source, Span destination) where TPixel : unmanaged, IPixel @@ -112,15 +233,21 @@ internal static partial class Vector4Converters while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) { - destinationBase = sourceStart.ToVector4(); + destinationBase = sourceStart.ToAssociatedVector4(); sourceStart = ref Unsafe.Add(ref sourceStart, 1); destinationBase = ref Unsafe.Add(ref destinationBase, 1); } } + /// + /// Converts unassociated scaled vectors to pixels without checking span lengths. + /// + /// The destination pixel format. + /// The source vectors. + /// The destination pixels. Its length must be at least length. [MethodImpl(InliningOptions.ShortMethod)] - private static void UnsafeFromScaledVector4Core( + private static void UnsafeFromUnassociatedScaledVector4Core( ReadOnlySpan source, Span destination) where TPixel : unmanaged, IPixel @@ -131,15 +258,71 @@ internal static partial class Vector4Converters while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) { - destinationBase = TPixel.FromScaledVector4(sourceStart); + destinationBase = TPixel.FromUnassociatedScaledVector4(sourceStart); + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + } + } + + /// + /// Converts associated scaled vectors to pixels without checking span lengths. + /// + /// The destination pixel format. + /// The source vectors. + /// The destination pixels. Its length must be at least length. + [MethodImpl(InliningOptions.ShortMethod)] + private static void UnsafeFromAssociatedScaledVector4Core( + ReadOnlySpan source, + Span destination) + where TPixel : unmanaged, IPixel + { + ref Vector4 sourceStart = ref MemoryMarshal.GetReference(source); + ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceStart, (uint)source.Length); + ref TPixel destinationBase = ref MemoryMarshal.GetReference(destination); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationBase = TPixel.FromAssociatedScaledVector4(sourceStart); + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationBase = ref Unsafe.Add(ref destinationBase, 1); + } + } + + /// + /// Converts pixels to unassociated scaled vectors without checking span lengths. + /// + /// The source pixel format. + /// The source pixels. + /// The destination vectors. Its length must be at least length. + [MethodImpl(InliningOptions.ShortMethod)] + private static void UnsafeToUnassociatedScaledVector4Core( + ReadOnlySpan source, + Span destination) + where TPixel : unmanaged, IPixel + { + ref TPixel sourceStart = ref MemoryMarshal.GetReference(source); + ref TPixel sourceEnd = ref Unsafe.Add(ref sourceStart, (uint)source.Length); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationBase = sourceStart.ToUnassociatedScaledVector4(); sourceStart = ref Unsafe.Add(ref sourceStart, 1); destinationBase = ref Unsafe.Add(ref destinationBase, 1); } } + /// + /// Converts pixels to associated scaled vectors without checking span lengths. + /// + /// The source pixel format. + /// The source pixels. + /// The destination vectors. Its length must be at least length. [MethodImpl(InliningOptions.ShortMethod)] - private static void UnsafeToScaledVector4Core( + private static void UnsafeToAssociatedScaledVector4Core( ReadOnlySpan source, Span destination) where TPixel : unmanaged, IPixel @@ -150,7 +333,7 @@ internal static partial class Vector4Converters while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) { - destinationBase = sourceStart.ToScaledVector4(); + destinationBase = sourceStart.ToAssociatedScaledVector4(); sourceStart = ref Unsafe.Add(ref sourceStart, 1); destinationBase = ref Unsafe.Add(ref destinationBase, 1); diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs index 9e649f3c0..886732eb9 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs @@ -15,10 +15,7 @@ namespace SixLabors.ImageSharp.PixelFormats.Utils; internal static partial class Vector4Converters { /// - /// Provides efficient implementations for batched to/from conversion. - /// which is applicable for -compatible pixel types where - /// returns the same scaled result as . - /// The method is works by internally converting to a therefore it's not applicable for that type! + /// Provides efficient implementations for batched conversion between RGBA-compatible pixel types and values. /// public static class RgbaCompatible { @@ -51,6 +48,7 @@ internal static partial class Vector4Converters Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); int count = source.Length; + destination = destination[..count]; // Not worth for small buffers: if (count < Vector4ConversionThreshold) @@ -60,14 +58,14 @@ internal static partial class Vector4Converters return; } - // Using the last quarter of 'destination' as a temporary buffer to avoid allocation: + // ToVector4 expands each pixel into a 16-byte vector. Reuse the unwritten destination tail as RGBA staging + // so pixelOperations can reorder the source without allocating a temporary row. int countWithoutLastItem = count - 1; ReadOnlySpan reducedSource = source[..countWithoutLastItem]; Span lastQuarterOfDestination = MemoryMarshal.Cast(destination).Slice((3 * count) + 1, countWithoutLastItem); pixelOperations.ToRgba32(configuration, reducedSource, lastQuarterOfDestination); - // 'destination' and 'lastQuarterOfDestination' are overlapping buffers, - // but we are always reading/writing at different positions: + // Staging overlaps the final output vector, which remains unwritten until the staged bytes are consumed. SimdUtils.ByteToNormalizedFloat( MemoryMarshal.Cast(lastQuarterOfDestination), MemoryMarshal.Cast(destination[..countWithoutLastItem])); @@ -102,6 +100,10 @@ internal static partial class Vector4Converters int count = source.Length; + // Source defines the conversion length. Do not expose spare destination capacity + // to downstream shuffles whose scalar tail is destination-length driven. + destination = destination[..count]; + // Not worth for small buffers: if (count < Vector4ConversionThreshold) { diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.cs index 0a0b5660d..d701b0ab5 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.cs +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.PixelFormats.Utils; internal static partial class Vector4Converters { /// - /// Apply modifiers used requested by ToVector4() conversion. + /// Applies the modifiers requested by ToVector4() conversion. /// /// The span of vectors. /// The modifier rule. @@ -29,7 +29,7 @@ internal static partial class Vector4Converters } /// - /// Apply modifiers used requested by FromVector4() conversion. + /// Applies the modifiers requested by FromVector4() conversion. /// /// The span of vectors. /// The modifier rule. diff --git a/src/ImageSharp/Primitives/Point.cs b/src/ImageSharp/Primitives/Point.cs index 8627fe980..99193e3bb 100644 --- a/src/ImageSharp/Primitives/Point.cs +++ b/src/ImageSharp/Primitives/Point.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp; @@ -232,7 +233,19 @@ public struct Point : IEquatable /// The transformation matrix used. /// The transformed . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Point Transform(Point point, Matrix3x2 matrix) => Round(Vector2.Transform(new Vector2(point.X, point.Y), matrix)); + public static PointF Transform(Point point, Matrix3x2 matrix) + => Vector2.Transform(new Vector2(point.X, point.Y), matrix); + + /// + /// Transforms a point by a specified 4x4 matrix, applying a projective transform + /// flattened into 2D space. + /// + /// The point to transform. + /// The transformation matrix used. + /// The transformed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Transform(Point point, Matrix4x4 matrix) + => TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix); /// /// Deconstructs this point into two integers. diff --git a/src/ImageSharp/Primitives/PointF.cs b/src/ImageSharp/Primitives/PointF.cs index 35a506bb4..7979c0af0 100644 --- a/src/ImageSharp/Primitives/PointF.cs +++ b/src/ImageSharp/Primitives/PointF.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp; @@ -246,6 +247,17 @@ public struct PointF : IEquatable [MethodImpl(MethodImplOptions.AggressiveInlining)] public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix); + /// + /// Transforms a point by a specified 4x4 matrix, applying a projective transform + /// flattened into 2D space. + /// + /// The point to transform. + /// The transformation matrix used. + /// The transformed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Transform(PointF point, Matrix4x4 matrix) + => TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix); + /// /// Deconstructs this point into two floats. /// diff --git a/src/ImageSharp/Primitives/Rectangle.cs b/src/ImageSharp/Primitives/Rectangle.cs index e2ae5071e..26a94837f 100644 --- a/src/ImageSharp/Primitives/Rectangle.cs +++ b/src/ImageSharp/Primitives/Rectangle.cs @@ -260,11 +260,17 @@ public struct Rectangle : IEquatable /// The transformation matrix. /// A transformed rectangle. public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix) - { - PointF bottomRight = Point.Transform(new Point(rectangle.Right, rectangle.Bottom), matrix); - PointF topLeft = Point.Transform(rectangle.Location, matrix); - return new RectangleF(topLeft, new SizeF(bottomRight - topLeft)); - } + => RectangleF.Transform(rectangle, matrix); + + /// + /// Transforms a rectangle by the given 4x4 matrix, applying a projective transform + /// flattened into 2D space. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed rectangle. + public static RectangleF Transform(Rectangle rectangle, Matrix4x4 matrix) + => RectangleF.Transform(rectangle, matrix); /// /// Converts a to a by performing a truncate operation on all the coordinates. diff --git a/src/ImageSharp/Primitives/RectangleF.cs b/src/ImageSharp/Primitives/RectangleF.cs index 68add77d0..3f41c2d07 100644 --- a/src/ImageSharp/Primitives/RectangleF.cs +++ b/src/ImageSharp/Primitives/RectangleF.cs @@ -236,9 +236,39 @@ public struct RectangleF : IEquatable /// A transformed . public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix) { + PointF topLeft = PointF.Transform(rectangle.Location, matrix); + PointF topRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Top), matrix); + PointF bottomLeft = PointF.Transform(new PointF(rectangle.Left, rectangle.Bottom), matrix); PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix); + + float left = MathF.Min(MathF.Min(topLeft.X, topRight.X), MathF.Min(bottomLeft.X, bottomRight.X)); + float top = MathF.Min(MathF.Min(topLeft.Y, topRight.Y), MathF.Min(bottomLeft.Y, bottomRight.Y)); + float right = MathF.Max(MathF.Max(topLeft.X, topRight.X), MathF.Max(bottomLeft.X, bottomRight.X)); + float bottom = MathF.Max(MathF.Max(topLeft.Y, topRight.Y), MathF.Max(bottomLeft.Y, bottomRight.Y)); + + return FromLTRB(left, top, right, bottom); + } + + /// + /// Transforms a rectangle by the given 4x4 matrix, applying a projective transform + /// flattened into 2D space. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed . + public static RectangleF Transform(RectangleF rectangle, Matrix4x4 matrix) + { PointF topLeft = PointF.Transform(rectangle.Location, matrix); - return new RectangleF(topLeft, new SizeF(bottomRight - topLeft)); + PointF topRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Top), matrix); + PointF bottomLeft = PointF.Transform(new PointF(rectangle.Left, rectangle.Bottom), matrix); + PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix); + + float left = MathF.Min(MathF.Min(topLeft.X, topRight.X), MathF.Min(bottomLeft.X, bottomRight.X)); + float top = MathF.Min(MathF.Min(topLeft.Y, topRight.Y), MathF.Min(bottomLeft.Y, bottomRight.Y)); + float right = MathF.Max(MathF.Max(topLeft.X, topRight.X), MathF.Max(bottomLeft.X, bottomRight.X)); + float bottom = MathF.Max(MathF.Max(topLeft.Y, topRight.Y), MathF.Max(bottomLeft.Y, bottomRight.Y)); + + return FromLTRB(left, top, right, bottom); } /// diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index c1e11a1d4..84d0d2ea1 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -349,6 +349,16 @@ public class AffineTransformBuilder internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix) => TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size); + /// + /// Clears all accumulated transform matrices, resetting the builder to its initial state. + /// + /// The . + public AffineTransformBuilder Clear() + { + this.transformMatrixFactories.Clear(); + return this; + } + private static void CheckDegenerate(Matrix3x2 matrix) { if (TransformUtilities.IsDegenerate(matrix)) diff --git a/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs b/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs index bf6d2221f..b0f5cb7d6 100644 --- a/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs @@ -12,12 +12,12 @@ namespace SixLabors.ImageSharp.Processing; public static class QuantizeExtensions { /// - /// Applies quantization to the image using the . + /// Applies quantization to the image using the . /// /// The current image processing context. /// The . public static IImageProcessingContext Quantize(this IImageProcessingContext source) => - Quantize(source, KnownQuantizers.Octree); + Quantize(source, KnownQuantizers.Hexadecatree); /// /// Applies quantization to the image. @@ -29,7 +29,7 @@ public static class QuantizeExtensions source.ApplyProcessor(new QuantizeProcessor(quantizer)); /// - /// Applies quantization to the image using the . + /// Applies quantization to the image using the . /// /// The current image processing context. /// @@ -37,7 +37,7 @@ public static class QuantizeExtensions /// /// The . public static IImageProcessingContext Quantize(this IImageProcessingContext source, Rectangle rectangle) => - Quantize(source, KnownQuantizers.Octree, rectangle); + Quantize(source, KnownQuantizers.Hexadecatree, rectangle); /// /// Applies quantization to the image. diff --git a/src/ImageSharp/Processing/KnownQuantizers.cs b/src/ImageSharp/Processing/KnownQuantizers.cs index 6fb3c72e8..b63ba597d 100644 --- a/src/ImageSharp/Processing/KnownQuantizers.cs +++ b/src/ImageSharp/Processing/KnownQuantizers.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -6,14 +6,14 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Processing; /// -/// Contains reusable static instances of known quantizing algorithms +/// Contains reusable static instances of known quantizing algorithms. /// public static class KnownQuantizers { /// - /// Gets the adaptive Octree quantizer. Fast with good quality. + /// Gets the adaptive hexadecatree quantizer. Fast with good quality. /// - public static IQuantizer Octree { get; } = new OctreeQuantizer(); + public static IQuantizer Hexadecatree { get; } = new HexadecatreeQuantizer(); /// /// Gets the Xiaolin Wu's Color Quantizer which generates high quality output. diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs index a96fa1993..7023a3cd9 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.ColorProfiles.Companding; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters; @@ -112,7 +113,7 @@ internal class BokehBlurProcessor : ImageProcessor } else { - ApplyInverseGammaExposureRowOperation operation = new(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, 1 / this.gamma); + ApplyInverseGammaExposureRowOperation operation = new(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, this.gamma); ParallelRowIterator.IterateRows( this.Configuration, sourceRectangle, @@ -237,9 +238,10 @@ internal class BokehBlurProcessor : ImageProcessor Span targetBuffer = this.targetValues.DangerousGetRowSpan(y); targetBuffer.Clear(); - // Execute the bulk pixel format conversion for the current row + // Gamma exposure temporarily stores premultiplied numeric intermediates in TPixel without changing their + // representation. Read the native scaled values back directly to avoid an associate/unassociate round trip. Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, span); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, span, PixelConversionModifiers.Scale); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(span); ref ComplexVector4 targetStart = ref MemoryMarshal.GetReference(targetBuffer); @@ -304,18 +306,14 @@ internal class BokehBlurProcessor : ImageProcessor public void Invoke(int y, Span span) { Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Premultiply); - ref Vector4 baseRef = ref MemoryMarshal.GetReference(span); + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); - for (int x = 0; x < this.bounds.Width; x++) - { - ref Vector4 v = ref Unsafe.Add(ref baseRef, (uint)x); - v.X = MathF.Pow(v.X, this.gamma); - v.Y = MathF.Pow(v.Y, this.gamma); - v.Z = MathF.Pow(v.Z, this.gamma); - } + // Input is premultiplied [0,1] so the LUT is safe here. + GammaCompanding.Expand(span[..this.bounds.Width], this.gamma); - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); + // The first convolution pass consumes these values as raw premultiplied numerics. Preserve them in the + // native TPixel representation instead of quantizing through an alpha-association conversion. + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan, PixelConversionModifiers.Scale); } } @@ -350,11 +348,13 @@ internal class BokehBlurProcessor : ImageProcessor { Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Premultiply); + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); Numerics.CubePowOnXYZ(span); - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); + // The first convolution pass consumes these values as raw premultiplied numerics. Preserve them in the + // native TPixel representation instead of quantizing through an alpha-association conversion. + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan, PixelConversionModifiers.Scale); } } @@ -367,7 +367,7 @@ internal class BokehBlurProcessor : ImageProcessor private readonly Buffer2D targetPixels; private readonly Buffer2D sourceValues; private readonly Configuration configuration; - private readonly float inverseGamma; + private readonly float gamma; [MethodImpl(InliningOptions.ShortMethod)] public ApplyInverseGammaExposureRowOperation( @@ -375,36 +375,26 @@ internal class BokehBlurProcessor : ImageProcessor Buffer2D targetPixels, Buffer2D sourceValues, Configuration configuration, - float inverseGamma) + float gamma) { this.bounds = bounds; this.targetPixels = targetPixels; this.sourceValues = sourceValues; this.configuration = configuration; - this.inverseGamma = inverseGamma; + this.gamma = gamma; } /// [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Vector4 low = Vector4.Zero; - Vector4 high = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); - Span targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; - Span sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y)[this.bounds.X..]; - ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); + Span sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); - for (int x = 0; x < this.bounds.Width; x++) - { - ref Vector4 v = ref Unsafe.Add(ref sourceRef, (uint)x); - Vector4 clamp = Numerics.Clamp(v, low, high); - v.X = MathF.Pow(clamp.X, this.inverseGamma); - v.Y = MathF.Pow(clamp.Y, this.inverseGamma); - v.Z = MathF.Pow(clamp.Z, this.inverseGamma); - } + Numerics.Clamp(MemoryMarshal.Cast(sourceRowSpan), 0, 1F); + GammaCompanding.Compress(sourceRowSpan, this.gamma); - PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan[..this.bounds.Width], targetPixelSpan, PixelConversionModifiers.Premultiply); + PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan, targetPixelSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); } } @@ -433,17 +423,16 @@ internal class BokehBlurProcessor : ImageProcessor /// [MethodImpl(InliningOptions.ShortMethod)] - public unsafe void Invoke(int y) + public void Invoke(int y) { Span sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); - ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); - Numerics.Clamp(MemoryMarshal.Cast(sourceRowSpan), 0, float.PositiveInfinity); + Numerics.Clamp(MemoryMarshal.Cast(sourceRowSpan), 0, 1F); Numerics.CubeRootOnXYZ(sourceRowSpan); Span targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..]; - PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan[..this.bounds.Width], targetPixelSpan, PixelConversionModifiers.Premultiply); + PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan, targetPixelSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs index 02e06db49..4ed6934a2 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Numerics; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -79,10 +79,16 @@ internal class Convolution2DProcessor : ImageProcessor this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows, Vector4>( - this.Configuration, - interest, - in operation); + // Convolution is memory-bandwidth-bound with low arithmetic intensity. + // Parallelization degrades performance due to cache line contention from + // overlapping source row reads. See #3111. + using IMemoryOwner buffer = allocator.Allocate(operation.GetRequiredBufferLength(interest)); + Span span = buffer.Memory.Span; + + for (int y = interest.Top; y < interest.Bottom; y++) + { + operation.Invoke(y, span); + } } Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs index bccf19174..409480871 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs @@ -78,6 +78,8 @@ internal readonly struct Convolution2DRowOperation : IRowOperation : IRowOperation.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); @@ -115,7 +117,7 @@ internal readonly struct Convolution2DRowOperation : IRowOperation.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); for (nuint x = 0; x < (uint)sourceRow.Length; x++) { @@ -128,7 +130,7 @@ internal readonly struct Convolution2DRowOperation : IRowOperation targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRowSpan); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRowSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -144,6 +146,8 @@ internal readonly struct Convolution2DRowOperation : IRowOperation : IRowOperation sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleY).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); - Numerics.Premultiply(sourceBuffer); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); for (uint x = 0; x < (uint)sourceBuffer.Length; x++) @@ -186,11 +189,12 @@ internal readonly struct Convolution2DRowOperation : IRowOperation targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRow, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index 1bbbdb350..1e94812fd 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -199,12 +199,14 @@ internal class Convolution2PassProcessor : ImageProcessor Span sourceBuffer = span[..this.bounds.Width]; Span targetBuffer = span[this.bounds.Width..]; + // Alpha is preserved separately, so filter straight color and restore the source alpha after applying the kernel. + // Clear the target buffer for each row run. targetBuffer.Clear(); // Get the precalculated source sample row for this kernel row and copy to our buffer. Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); ref Vector4 targetStart = ref MemoryMarshal.GetReference(targetBuffer); @@ -233,9 +235,6 @@ internal class Convolution2PassProcessor : ImageProcessor } // Now we need to copy the original alpha values from the source row. - sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); - targetStart = ref MemoryMarshal.GetReference(targetBuffer); while (Unsafe.IsAddressLessThan(ref targetStart, ref targetEnd)) @@ -247,7 +246,7 @@ internal class Convolution2PassProcessor : ImageProcessor } Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -261,14 +260,14 @@ internal class Convolution2PassProcessor : ImageProcessor Span sourceBuffer = span[..this.bounds.Width]; Span targetBuffer = span[this.bounds.Width..]; + // Alpha participates in this convolution, so filter associated color to keep RGB weighted by coverage throughout the kernel. + // Clear the target buffer for each row run. targetBuffer.Clear(); // Get the precalculated source sample row for this kernel row and copy to our buffer. Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); - - Numerics.Premultiply(sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); ref Vector4 targetStart = ref MemoryMarshal.GetReference(targetBuffer); @@ -296,10 +295,8 @@ internal class Convolution2PassProcessor : ImageProcessor sampleColumnBase = ref Unsafe.Add(ref sampleColumnBase, (uint)kernelSize); } - Numerics.UnPremultiply(targetBuffer); - Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); } } @@ -367,6 +364,8 @@ internal class Convolution2PassProcessor : ImageProcessor ref int sampleRowBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(this.map.GetRowOffsetSpan()), (uint)((y - this.bounds.Y) * kernelSize)); + // Alpha is preserved separately, so filter straight color and restore the source alpha after applying the kernel. + // Clear the target buffer for each row run. targetBuffer.Clear(); @@ -380,7 +379,7 @@ internal class Convolution2PassProcessor : ImageProcessor // Get the precalculated source sample row for this kernel row and copy to our buffer. sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleRowBase).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)sourceBuffer.Length); @@ -401,7 +400,7 @@ internal class Convolution2PassProcessor : ImageProcessor // Now we need to copy the original alpha values from the source row. sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); { ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)sourceBuffer.Length); @@ -416,7 +415,7 @@ internal class Convolution2PassProcessor : ImageProcessor } Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -432,6 +431,8 @@ internal class Convolution2PassProcessor : ImageProcessor ref int sampleRowBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(this.map.GetRowOffsetSpan()), (uint)((y - this.bounds.Y) * kernelSize)); + // Alpha participates in this convolution, so filter associated color to keep RGB weighted by coverage throughout the kernel. + // Clear the target buffer for each row run. targetBuffer.Clear(); @@ -445,9 +446,7 @@ internal class Convolution2PassProcessor : ImageProcessor // Get the precalculated source sample row for this kernel row and copy to our buffer. sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleRowBase).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); - - Numerics.Premultiply(sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); ref Vector4 sourceEnd = ref Unsafe.Add(ref sourceBase, (uint)sourceBuffer.Length); @@ -466,10 +465,8 @@ internal class Convolution2PassProcessor : ImageProcessor sampleRowBase = ref Unsafe.Add(ref sampleRowBase, 1); } - Numerics.UnPremultiply(targetBuffer); - Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs index 4aefa0dae..db9eff55c 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Processing.Processors.Convolution; @@ -36,11 +37,8 @@ internal static class ConvolutionProcessorHelpers kernel[i] = gx; } - // Normalize kernel so that the sum of all weights equals 1 - for (int i = 0; i < size; i++) - { - kernel[i] /= sum; - } + // Divide every weight by the accumulated Gaussian sum so the kernel has unit response. + TensorPrimitives_.Divide(kernel, sum, kernel); return kernel; } @@ -68,25 +66,15 @@ internal static class ConvolutionProcessorHelpers // Invert the kernel for sharpening. int midpointRounded = (int)midpoint; - for (int i = 0; i < size; i++) - { - if (i == midpointRounded) - { - // Calculate central value - kernel[i] = (2F * sum) - kernel[i]; - } - else - { - // invert value - kernel[i] = -kernel[i]; - } - } + float midpointValue = kernel[midpointRounded]; + TensorPrimitives_.Negate(kernel, kernel); - // Normalize kernel so that the sum of all weights equals 1 - for (int i = 0; i < size; i++) - { - kernel[i] /= sum; - } + // The sharpening kernel negates every Gaussian weight except its center. Restore that original + // center while adding twice the Gaussian sum so the complete kernel retains unit response. + kernel[midpointRounded] = (2F * sum) - midpointValue; + + // Sharpening changes signs but preserves the Gaussian sum, so the same divisor produces unit response. + TensorPrimitives_.Divide(kernel, sum, kernel); return kernel; } diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs index feaaf30ce..e599324f9 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -161,6 +161,8 @@ internal class ConvolutionProcessor : ImageProcessor if (this.preserveAlpha) { + // Alpha is preserved separately, so filter straight color and restore the source alpha after applying the kernel. + // Clear the target buffer for each row run. targetBuffer.Clear(); ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); @@ -171,7 +173,7 @@ internal class ConvolutionProcessor : ImageProcessor // Get the precalculated source sample row for this kernel row and copy to our buffer. int offsetY = Unsafe.Add(ref sampleRowBase, kY); sourceRow = this.sourcePixels.DangerousGetRowSpan(offsetY).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); @@ -191,16 +193,20 @@ internal class ConvolutionProcessor : ImageProcessor // Now we need to copy the original alpha values from the source row. sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); for (nuint x = 0; x < (uint)sourceRow.Length; x++) { ref Vector4 target = ref Unsafe.Add(ref targetBase, x); target.W = Unsafe.Add(ref MemoryMarshal.GetReference(sourceBuffer), x).W; } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } else { + // Alpha participates in this convolution, so filter associated color to keep RGB weighted by coverage throughout the kernel. + // Clear the target buffer for each row run. targetBuffer.Clear(); ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); @@ -210,9 +216,8 @@ internal class ConvolutionProcessor : ImageProcessor // Get the precalculated source sample row for this kernel row and copy to our buffer. int offsetY = Unsafe.Add(ref sampleRowBase, kY); Span sourceRow = this.sourcePixels.DangerousGetRowSpan(offsetY).Slice(boundsX, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); - Numerics.Premultiply(sourceBuffer); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); for (uint x = 0; x < (uint)sourceBuffer.Length; x++) @@ -229,10 +234,8 @@ internal class ConvolutionProcessor : ImageProcessor } } - Numerics.UnPremultiply(targetBuffer); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); } - - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs index eae748166..5b62b9b8a 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs @@ -122,10 +122,12 @@ internal class EdgeDetectorCompassProcessor : ImageProcessor for (nuint x = this.minX; x < this.maxX; x++) { - // Grab the max components of the two pixels + // Compass responses are logical color magnitudes. Comparing associated components would make the edge strength + // depend on the storage alpha representation instead of the filtered colors. ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x); ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, x); - currentTargetPixel = TPixel.FromVector4(Vector4.Max(currentPassPixel.ToVector4(), currentTargetPixel.ToVector4())); + currentTargetPixel = TPixel.FromUnassociatedScaledVector4( + Vector4.Max(currentPassPixel.ToUnassociatedScaledVector4(), currentTargetPixel.ToUnassociatedScaledVector4())); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs index 7653aeaa9..2e4292890 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs @@ -104,7 +104,15 @@ internal sealed class KernelSamplingMap : IDisposable { int affectedSize = (kernelSize >> 1) * kernelSize; ref int spanBase = ref MemoryMarshal.GetReference(span); - if (affectedSize > 0) + if (affectedSize >= span.Length && span.Length > 0) + { + // The bounds are no larger than the kernel radius: every offset can overshoot the + // borders by a full sampling extent or more, which the single-pass head and tail + // corrections below cannot fold back into range (and for bounds strictly smaller + // than the radius they cannot even slice the span). Fold each offset exactly. + CorrectBorderExact(span, min, max, borderMode); + } + else if (affectedSize > 0) { switch (borderMode) { @@ -180,4 +188,58 @@ internal sealed class KernelSamplingMap : IDisposable } } } + + private static void CorrectBorderExact(Span span, int min, int max, BorderWrappingMode borderMode) + { + int extent = max - min + 1; + switch (borderMode) + { + case BorderWrappingMode.Repeat: + Numerics.Clamp(span, min, max); + break; + + case BorderWrappingMode.Mirror: + // Reflection about the half-sample border: ..., min+1, min | min, min+1, ... + int mirrorPeriod = 2 * extent; + for (int i = 0; i < span.Length; i++) + { + int offset = Modulo(span[i] - min, mirrorPeriod); + span[i] = min + (offset < extent ? offset : mirrorPeriod - 1 - offset); + } + + break; + + case BorderWrappingMode.Bounce: + // Reflection about the border sample itself: ..., min+2, min+1 | min, min+1, ... + if (extent == 1) + { + span.Fill(min); + break; + } + + int bouncePeriod = (2 * extent) - 2; + for (int i = 0; i < span.Length; i++) + { + int offset = Modulo(span[i] - min, bouncePeriod); + span[i] = min + (offset < extent ? offset : bouncePeriod - offset); + } + + break; + + case BorderWrappingMode.Wrap: + for (int i = 0; i < span.Length; i++) + { + span[i] = min + Modulo(span[i] - min, extent); + } + + break; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Modulo(int value, int period) + { + int remainder = value % period; + return remainder < 0 ? remainder + period : remainder; + } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs index ca2cabab5..97ddd3be3 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -78,7 +78,7 @@ internal readonly struct MedianRowOperation : IRowOperation int currentYIndex = Unsafe.Add(ref sampleRowBase, (uint)i); Span sourceRow = this.sourcePixels.DangerousGetRowSpan(currentYIndex).Slice(boundsX, boundsWidth); Span sourceVectorRow = sourceVectorBuffer.Slice(i * boundsWidth, boundsWidth); - PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceVectorRow); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceVectorRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } if (this.preserveAlpha) @@ -130,7 +130,7 @@ internal readonly struct MedianRowOperation : IRowOperation } Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs b/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs index 4b3dd7fe7..12f6e55d5 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters; @@ -209,13 +210,11 @@ internal static class BokehBlurKernelDataProvider for (int i = 0; i < kernelsSpan.Length; i++) { ref Complex64[] kernelsRef = ref Unsafe.Add(ref baseKernelsRef, (uint)i); - int length = kernelsRef.Length; - ref Complex64 valueRef = ref MemoryMarshal.GetArrayDataReference(kernelsRef); - for (int j = 0; j < length; j++) - { - Unsafe.Add(ref valueRef, (uint)j) *= scalar; - } + // Complex64 stores each value as adjacent real and imaginary floats. Multiplying that flattened + // float span scales both parts independently, which is exactly complex multiplication by a real scalar. + Span values = MemoryMarshal.Cast(kernelsRef.AsSpan()); + TensorPrimitives_.Multiply(values, scalar, values); } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 3c0ebb707..36cc5564a 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -168,8 +168,8 @@ public readonly partial struct ErrorDither : IDither, IEquatable, I return transformed; } - // Calculate the error - Vector4 error = (source.ToVector4() - transformed.ToVector4()) * scale; + // Palette error is a logical color difference, so it must be diffused independently of the pixel storage representation. + Vector4 error = (source.ToUnassociatedScaledVector4() - transformed.ToUnassociatedScaledVector4()) * scale; int offset = this.offset; DenseMatrix matrix = this.matrix; @@ -200,10 +200,19 @@ public readonly partial struct ErrorDither : IDither, IEquatable, I } ref TPixel pixel = ref rowSpan[targetX]; - Vector4 result = pixel.ToVector4(); + Vector4 result = pixel.ToUnassociatedScaledVector4(); + + // Do not diffuse error into fully transparent pixels. They carry no visible color + // (a decoder shows whatever is behind them), so perturbing them is meaningless and, + // for indexed transparency, nudges them off the exact transparent color so they are + // matched to the nearest opaque palette entry instead of being kept transparent. + if (result.W <= 0) + { + continue; + } result += error * coefficient; - pixel = TPixel.FromVector4(result); + pixel = TPixel.FromUnassociatedScaledVector4(result); } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index b0622cac5..cec4874f6 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -182,6 +182,16 @@ public readonly partial struct OrderedDither : IDither, IEquatable { Rgba32 rgba = source.ToRgba32(); + + // Leave fully transparent pixels untouched. They carry no visible color (a decoder shows + // whatever is behind them), so perturbing them is meaningless and, for indexed transparency, + // nudges them off the exact transparent color so they are matched to the nearest opaque + // palette entry instead of being kept transparent. + if (rgba.A == 0) + { + return source; + } + Unsafe.SkipInit(out Rgba32 attempt); float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale; diff --git a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs index 7a1ffb85f..f2faee4f9 100644 --- a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs +++ b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -145,7 +146,7 @@ internal class DrawImageProcessor : ImageProcessor this.Blender, this.Opacity); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRows( configuration, new Rectangle(0, 0, foregroundRectangle.Width, foregroundRectangle.Height), in operation); @@ -161,7 +162,7 @@ internal class DrawImageProcessor : ImageProcessor /// /// A implementing the draw logic for . /// - private readonly struct RowOperation : IRowOperation + private readonly struct RowOperation : IRowOperation { private readonly Buffer2D background; private readonly Buffer2D foreground; @@ -190,13 +191,20 @@ internal class DrawImageProcessor : ImageProcessor this.opacity = opacity; } + /// + public int GetRequiredBufferLength(Rectangle bounds) + + // By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend + // We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending. + => 3 * bounds.Width; + /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(int y) + public void Invoke(int y, Span span) { Span background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width); Span foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width); - this.blender.Blend(this.configuration, background, background, foreground, this.opacity); + this.blender.Blend(this.configuration, background, background, foreground, this.opacity, span); } } } diff --git a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs index f811bae0f..ddb6d1545 100644 --- a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs @@ -119,13 +119,15 @@ internal class OilPaintingProcessor : ImageProcessor Span redBinSpan = binsSpan[this.levels..]; Span blueBinSpan = redBinSpan[this.levels..]; Span greenBinSpan = blueBinSpan[this.levels..]; + PixelOperations pixelOperations = PixelOperations.Instance; for (int y = rows.Min; y < rows.Max; y++) { Span sourceRowPixelSpan = this.source.DangerousGetRowSpan(y); Span sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width); - PixelOperations.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span, PixelConversionModifiers.Scale); + // Intensity bins and channel averages describe logical colors, so they must not depend on the pixel storage association. + pixelOperations.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); for (int x = this.bounds.X; x < this.bounds.Right; x++) { @@ -149,7 +151,7 @@ internal class OilPaintingProcessor : ImageProcessor int offsetX = x + fxr; offsetX = Numerics.Clamp(offsetX, 0, maxX); - Vector4 vector = sourceOffsetRow[offsetX].ToScaledVector4(); + Vector4 vector = sourceOffsetRow[offsetX].ToUnassociatedScaledVector4(); float sourceRed = vector.X; float sourceBlue = vector.Z; @@ -180,7 +182,7 @@ internal class OilPaintingProcessor : ImageProcessor Span targetRowAreaPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); - PixelOperations.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan, PixelConversionModifiers.Scale); + pixelOperations.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs index 37286086c..8b287b727 100644 --- a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs @@ -74,11 +74,14 @@ internal class FilterProcessor : ImageProcessor public void Invoke(int y, Span span) { Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); - PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale); + PixelOperations pixelOperations = PixelOperations.Instance; + + // Color matrices transform logical color components, so associated storage must be unassociated before applying the matrix. + pixelOperations.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ColorNumerics.Transform(span, ref Unsafe.AsRef(in this.matrix)); - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan, PixelConversionModifiers.Scale); + pixelOperations.FromVector4Destructive(this.configuration, span, rowSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs index 41560d120..bffe6cf63 100644 --- a/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs @@ -56,7 +56,10 @@ internal sealed class OpaqueProcessor : ImageProcessor public void Invoke(int y, Span span) { Span targetRowSpan = this.target.DangerousGetRowSpan(y)[this.bounds.X..]; - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Scale); + PixelOperations pixelOperations = PixelOperations.Instance; + + // Opacity changes alpha without changing logical color, so associated storage must be unassociated before replacing alpha. + pixelOperations.ToVector4(this.configuration, targetRowSpan[..span.Length], span, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ref Vector4 baseRef = ref MemoryMarshal.GetReference(span); for (int x = 0; x < this.bounds.Width; x++) @@ -65,7 +68,7 @@ internal sealed class OpaqueProcessor : ImageProcessor v.W = 1F; } - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan, PixelConversionModifiers.Scale); + pixelOperations.FromVector4Destructive(this.configuration, span, targetRowSpan[..span.Length], PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs index 3d54836c5..b18a52bec 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs @@ -144,8 +144,11 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz for (int dx = xStart; dx < xEnd; dx++) { ref TPixel pixel = ref rowSpan[dx]; - float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, GetLuminance(pixel, luminanceLevels)); - pixel = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + Vector4 vector = pixel.ToUnassociatedScaledVector4(); + int luminance = ColorNumerics.GetBT709Luminance(vector, luminanceLevels); + float luminanceEqualized = cdfData.RemapGreyValue(cdfX, cdfY, luminance); + vector.X = vector.Y = vector.Z = luminanceEqualized; + pixel = TPixel.FromUnassociatedScaledVector4(vector); } } } @@ -190,8 +193,10 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz for (int dx = xStart; dx < xEnd; dx++) { ref TPixel pixel = ref rowSpan[dx]; - float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX, cdfY + 1, tileY, tileHeight, luminanceLevels); - pixel = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + Vector4 vector = pixel.ToUnassociatedScaledVector4(); + float luminanceEqualized = InterpolateBetweenTwoTiles(vector, cdfData, cdfX, cdfY, cdfX, cdfY + 1, tileY, tileHeight, luminanceLevels); + vector.X = vector.Y = vector.Z = luminanceEqualized; + pixel = TPixel.FromUnassociatedScaledVector4(vector); } tileY++; @@ -242,8 +247,10 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz for (int dx = x; dx < xLimit; dx++) { ref TPixel pixel = ref rowSpan[dx]; - float luminanceEqualized = InterpolateBetweenTwoTiles(pixel, cdfData, cdfX, cdfY, cdfX + 1, cdfY, tileX, tileWidth, luminanceLevels); - pixel = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + Vector4 vector = pixel.ToUnassociatedScaledVector4(); + float luminanceEqualized = InterpolateBetweenTwoTiles(vector, cdfData, cdfX, cdfY, cdfX + 1, cdfY, tileX, tileWidth, luminanceLevels); + vector.X = vector.Y = vector.Z = luminanceEqualized; + pixel = TPixel.FromUnassociatedScaledVector4(vector); tileX++; } } @@ -256,7 +263,7 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz /// /// Bilinear interpolation between four adjacent tiles. /// - /// The pixel to remap the grey value from. + /// The unassociated scaled vector to remap the grey value from. /// The pre-computed lookup tables to remap the grey values for each tiles. /// The number of tiles in the x-direction. /// The number of tiles in the y-direction. @@ -273,7 +280,7 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz /// A re-mapped grey value. [MethodImpl(InliningOptions.ShortMethod)] private static float InterpolateBetweenFourTiles( - TPixel sourcePixel, + Vector4 sourceVector, CdfTileData cdfData, int tileCountX, int tileCountY, @@ -285,7 +292,7 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz int tileHeight, int luminanceLevels) { - int luminance = GetLuminance(sourcePixel, luminanceLevels); + int luminance = ColorNumerics.GetBT709Luminance(sourceVector, luminanceLevels); float tx = tileX / (float)(tileWidth - 1); float ty = tileY / (float)(tileHeight - 1); @@ -304,7 +311,7 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz /// /// Linear interpolation between two tiles. /// - /// The pixel to remap the grey value from. + /// The unassociated scaled vector to remap the grey value from. /// The CDF lookup map. /// X position inside the first tile. /// Y position inside the first tile. @@ -319,7 +326,7 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz /// A re-mapped grey value. [MethodImpl(InliningOptions.ShortMethod)] private static float InterpolateBetweenTwoTiles( - TPixel sourcePixel, + Vector4 sourceVector, CdfTileData cdfData, int tileX1, int tileY1, @@ -329,7 +336,7 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz int tileWidth, int luminanceLevels) { - int luminance = GetLuminance(sourcePixel, luminanceLevels); + int luminance = ColorNumerics.GetBT709Luminance(sourceVector, luminanceLevels); float tx = tilePos / (float)(tileWidth - 1); float cdfLuminance1 = cdfData.RemapGreyValue(tileX1, tileY1, luminance); @@ -419,8 +426,9 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz for (int dx = x; dx < xEnd; dx++) { ref TPixel pixel = ref rowSpan[dx]; + Vector4 vector = pixel.ToUnassociatedScaledVector4(); float luminanceEqualized = InterpolateBetweenFourTiles( - pixel, + vector, this.cdfData, this.tileCount, this.tileCount, @@ -432,7 +440,8 @@ internal class AdaptiveHistogramEqualizationProcessor : HistogramEqualiz this.tileHeight, this.luminanceLevels); - pixel = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); + vector.X = vector.Y = vector.Z = luminanceEqualized; + pixel = TPixel.FromUnassociatedScaledVector4(vector); tileX++; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs index 8ca4a86a2..624820d2f 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs @@ -194,7 +194,7 @@ internal class AdaptiveHistogramEqualizationSlidingWindowProcessor : His int idx = 0; for (int dx = x; dx < x + tileWidth; dx++) { - rowPixels[idx] = source[Numerics.Abs(dx), y].ToVector4(); + rowPixels[idx] = source[Numerics.Abs(dx), y].ToUnassociatedScaledVector4(); idx++; } @@ -209,11 +209,11 @@ internal class AdaptiveHistogramEqualizationSlidingWindowProcessor : His if (dx >= source.Width) { int diff = dx - source.Width; - rowPixels[idx] = source[dx - diff - 1, y].ToVector4(); + rowPixels[idx] = source[dx - diff - 1, y].ToUnassociatedScaledVector4(); } else { - rowPixels[idx] = source[dx, y].ToVector4(); + rowPixels[idx] = source[dx, y].ToUnassociatedScaledVector4(); } idx++; @@ -242,7 +242,7 @@ internal class AdaptiveHistogramEqualizationSlidingWindowProcessor : His int y, int tileWidth, Configuration configuration) - => PixelOperations.Instance.ToVector4(configuration, source.DangerousGetRowSpan(y).Slice(start: x, length: tileWidth), rowPixels); + => PixelOperations.Instance.ToVector4(configuration, source.DangerousGetRowSpan(y).Slice(start: x, length: tileWidth), rowPixels, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); /// /// Adds a column of grey values to the histogram. @@ -256,7 +256,7 @@ internal class AdaptiveHistogramEqualizationSlidingWindowProcessor : His { for (nuint idx = 0; idx < (uint)length; idx++) { - int luminance = ColorNumerics.GetBT709Luminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + int luminance = ColorNumerics.GetBT709Luminance(Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); Unsafe.Add(ref histogramBase, (uint)luminance)++; } } @@ -273,7 +273,7 @@ internal class AdaptiveHistogramEqualizationSlidingWindowProcessor : His { for (nuint idx = 0; idx < (uint)length; idx++) { - int luminance = ColorNumerics.GetBT709Luminance(ref Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); + int luminance = ColorNumerics.GetBT709Luminance(Unsafe.Add(ref greyValuesBase, idx), luminanceLevels); Unsafe.Add(ref histogramBase, (uint)luminance)--; } } @@ -381,9 +381,11 @@ internal class AdaptiveHistogramEqualizationSlidingWindowProcessor : His float numberOfPixelsMinusCdfMin = this.swInfos.PixelInTile - cdfMin; // Map the current pixel to the new equalized value. - int luminance = GetLuminance(this.source[x, y], this.processor.LuminanceLevels); + Vector4 vector = this.source[x, y].ToUnassociatedScaledVector4(); + int luminance = ColorNumerics.GetBT709Luminance(vector, this.processor.LuminanceLevels); float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / numberOfPixelsMinusCdfMin; - this.targetPixels[x, y] = TPixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, this.source[x, y].ToVector4().W)); + vector.X = vector.Y = vector.Z = luminanceEqualized; + this.targetPixels[x, y] = TPixel.FromUnassociatedScaledVector4(vector); // Remove top most row from the histogram, mirroring rows which exceeds the borders. if (this.useFastPath) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs index e830cc55f..9acca6d40 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs @@ -142,18 +142,19 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor pixelRow = sourceAccess.GetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); - PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer); + PixelOperations operations = PixelOperations.Instance; + operations.ToVector4(this.configuration, pixelRow, vectorBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); for (int x = 0; x < this.bounds.Width; x++) { Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); - int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); + int luminance = ColorNumerics.GetBT709Luminance(vector, levels); float scaledLuminance = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; float scalingFactor = scaledLuminance * levels / luminance; Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W); } - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); + operations.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } @@ -201,12 +202,17 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor pixelRow = sourceAccess.GetRowSpan(y); - PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer); + Span pixelRow = sourceAccess.GetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + PixelOperations operations = PixelOperations.Instance; + operations.ToVector4(this.configuration, pixelRow, vectorBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); for (int x = 0; x < this.bounds.Width; x++) { - Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x) * levelsMinusOne; + Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); + float alpha = vector.W; + + // Alpha is not a histogram component and must not be scaled into the CDF index range. + vector *= levelsMinusOne; uint originalX = (uint)MathF.Round(vector.X); float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin; @@ -214,10 +220,10 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); + operations.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs index 3ab8f7431..3c07d05c6 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -124,18 +124,19 @@ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizat int levels = this.luminanceLevels; float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; - Span pixelRow = this.source.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer); + Span pixelRow = this.source.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + PixelOperations operations = PixelOperations.Instance; + operations.ToVector4(this.configuration, pixelRow, vectorBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); for (int x = 0; x < this.bounds.Width; x++) { Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); - int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); + int luminance = ColorNumerics.GetBT709Luminance(vector, levels); float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W); } - PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); + operations.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs index 3584f2954..c7e40b420 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs @@ -51,13 +51,13 @@ internal readonly struct GrayscaleLevelsRowOperation : IRowOperation pixelRow = this.source.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer); + Span pixelRow = this.source.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorBuffer, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); for (int x = 0; x < this.bounds.Width; x++) { Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x); - int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); + int luminance = ColorNumerics.GetBT709Luminance(vector, levels); Interlocked.Increment(ref Unsafe.Add(ref histogramBase, (uint)luminance)); } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs index 6ac36f3ce..b624d5b0e 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Normalization; @@ -115,10 +116,7 @@ internal abstract class HistogramEqualizationProcessor : ImageProcessor< int addToEachBin = sumOverClip > 0 ? (int)MathF.Floor(sumOverClip / this.luminanceLevelsFloat) : 0; if (addToEachBin > 0) { - for (nuint i = 0; i < (uint)histogram.Length; i++) - { - Unsafe.Add(ref histogramBase, i) += addToEachBin; - } + TensorPrimitives_.Add(histogram, addToEachBin, histogram); } int residual = sumOverClip - (addToEachBin * this.LuminanceLevels); @@ -142,7 +140,8 @@ internal abstract class HistogramEqualizationProcessor : ImageProcessor< public static int GetLuminance(TPixel sourcePixel, int luminanceLevels) { // TODO: We need a bulk per span equivalent. - Vector4 vector = sourcePixel.ToVector4(); - return ColorNumerics.GetBT709Luminance(ref vector, luminanceLevels); + // Histogram bins describe logical color, so storage alpha association must not scale luminance. + Vector4 vector = sourcePixel.ToUnassociatedScaledVector4(); + return ColorNumerics.GetBT709Luminance(vector, luminanceLevels); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs b/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs index 26fd7d5d7..c520d7c54 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs @@ -15,14 +15,8 @@ public enum ColorMatchingMode Coarse, /// - /// Enables an exact color match cache for the first 512 unique colors encountered, - /// falling back to coarse matching thereafter. - /// - Hybrid, - - /// - /// Performs exact color matching without any caching optimizations. - /// This is the slowest but most accurate matching strategy. + /// Performs exact color matching using a bounded exact-match cache with eviction. + /// This preserves exact color matching while accelerating repeated colors. /// Exact } diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs index 5b0c7252c..e2e7206e0 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs @@ -3,6 +3,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -71,32 +73,107 @@ internal sealed class EuclideanPixelMap : PixelMap [MethodImpl(InliningOptions.ColdPath)] private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) { - // Loop through the palette and find the nearest match. + ReadOnlySpan rgbaPalette = this.rgbaPalette; + ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPalette); int index = 0; - float leastDistance = float.MaxValue; - for (int i = 0; i < this.rgbaPalette.Length; i++) + int leastDistance = int.MaxValue; + int i = 0; + + if (Vector128.IsHardwareAccelerated && rgbaPalette.Length >= 4) { - Rgba32 candidate = this.rgbaPalette[i]; - if (candidate.PackedValue == rgba.PackedValue) - { - index = i; - break; - } + // Duplicate the query color so one 128-bit register can be subtracted from + // two packed RGBA candidates at a time after widening. + Vector128 pixel = Vector128.Create( + rgba.R, + rgba.G, + rgba.B, + rgba.A, + rgba.R, + rgba.G, + rgba.B, + rgba.A); - float distance = DistanceSquared(rgba, candidate); - if (distance == 0) + int vectorizedLength = rgbaPalette.Length & ~0x03; + + for (; i < vectorizedLength; i += 4) { - index = i; - break; + // Load four packed Rgba32 values (16 bytes) and widen them into two vectors: + // [c0.r, c0.g, c0.b, c0.a, c1.r, ...] and [c2.r, c2.g, c2.b, c2.a, c3.r, ...]. + Vector128 packed = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.Add(ref rgbaPaletteRef, i))); + Vector128 lowerDiff = Vector128.WidenLower(packed).AsInt16() - pixel; + Vector128 upperDiff = Vector128.WidenUpper(packed).AsInt16() - pixel; + + // MultiplyAddAdjacent collapses channel squares into RG + BA partial sums, + // so each pair of int lanes still corresponds to one candidate color. + Vector128 lowerPairs = Vector128_.MultiplyAddAdjacent(lowerDiff, lowerDiff); + Vector128 upperPairs = Vector128_.MultiplyAddAdjacent(upperDiff, upperDiff); + + // Sum the two partials for candidates i and i + 1. + ref int lowerRef = ref Unsafe.As, int>(ref lowerPairs); + int distance = lowerRef + Unsafe.Add(ref lowerRef, 1); + if (distance < leastDistance) + { + index = i; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + distance = Unsafe.Add(ref lowerRef, 2) + Unsafe.Add(ref lowerRef, 3); + if (distance < leastDistance) + { + index = i + 1; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + // Sum the two partials for candidates i + 2 and i + 3. + ref int upperRef = ref Unsafe.As, int>(ref upperPairs); + distance = upperRef + Unsafe.Add(ref upperRef, 1); + if (distance < leastDistance) + { + index = i + 2; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + distance = Unsafe.Add(ref upperRef, 2) + Unsafe.Add(ref upperRef, 3); + if (distance < leastDistance) + { + index = i + 3; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } } + } + for (; i < rgbaPalette.Length; i++) + { + int distance = DistanceSquared(rgba, Unsafe.Add(ref rgbaPaletteRef, i)); if (distance < leastDistance) { index = i; leastDistance = distance; + if (distance == 0) + { + goto Found; + } } } + Found: + // Now I have the index, pop it into the cache for next time _ = this.cache.TryAdd(rgba, (short)index); match = Unsafe.Add(ref paletteRef, (uint)index); @@ -111,12 +188,12 @@ internal sealed class EuclideanPixelMap : PixelMap /// The second point. /// The distance squared. [MethodImpl(InliningOptions.ShortMethod)] - private static float DistanceSquared(Rgba32 a, Rgba32 b) + private static int DistanceSquared(Rgba32 a, Rgba32 b) { - float deltaR = a.R - b.R; - float deltaG = a.G - b.G; - float deltaB = a.B - b.B; - float deltaA = a.A - b.A; + int deltaR = a.R - b.R; + int deltaG = a.G - b.G; + int deltaB = a.B - b.B; + int deltaA = a.A - b.A; return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); } @@ -177,8 +254,7 @@ internal static class PixelMapFactory ColorMatchingMode colorMatchingMode) where TPixel : unmanaged, IPixel => colorMatchingMode switch { - ColorMatchingMode.Hybrid => new EuclideanPixelMap(configuration, palette), - ColorMatchingMode.Exact => new EuclideanPixelMap(configuration, palette), + ColorMatchingMode.Exact => new EuclideanPixelMap(configuration, palette), _ => new EuclideanPixelMap(configuration, palette), }; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs similarity index 51% rename from src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs rename to src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs index 0a1032bf0..6b2f5a013 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs @@ -6,25 +6,29 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization; /// -/// Allows the quantization of images pixels using Octrees. -/// +/// Quantizes images by grouping colors in an adaptive 16-way tree and reducing those groups into a palette. /// -public class OctreeQuantizer : IQuantizer +/// +/// Each level routes colors using one bit of RGB and, when useful, one bit of alpha. Fully opaque mid-tone colors +/// use RGB-only routing so more branch resolution is spent on visible color detail, while transparent, dark, and +/// light colors use alpha-aware routing so opacity changes can form their own palette buckets. +/// +public class HexadecatreeQuantizer : IQuantizer { /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// using the default . /// - public OctreeQuantizer() + public HexadecatreeQuantizer() : this(new QuantizerOptions()) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The quantizer options defining quantization rules. - public OctreeQuantizer(QuantizerOptions options) + /// The quantizer options that control palette size, dithering, and transparency behavior. + public HexadecatreeQuantizer(QuantizerOptions options) { Guard.NotNull(options, nameof(options)); this.Options = options; @@ -41,5 +45,5 @@ public class OctreeQuantizer : IQuantizer /// public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : unmanaged, IPixel - => new OctreeQuantizer(configuration, options); + => new HexadecatreeQuantizer(configuration, options); } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs similarity index 52% rename from src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs rename to src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs index 07596b68a..d669f885a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs @@ -12,19 +12,28 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization; /// -/// Encapsulates methods to calculate the color palette if an image using an Octree pattern. -/// +/// Quantizes an image by building an adaptive 16-way color tree and reducing it to the requested palette size. /// +/// +/// +/// Each level routes colors using one bit of RGB and, when useful, one bit of alpha, giving the tree up to 16 children +/// per node and letting transparency participate directly in palette construction. +/// +/// +/// Fully opaque mid-tone colors use RGB-only routing so more branch resolution is spent on visible color detail. +/// Transparent, dark, and light colors use alpha-aware routing so opacity changes can form distinct palette buckets. +/// +/// /// The pixel format. #pragma warning disable CA1001 // Types that own disposable fields should be disposable // See https://github.com/dotnet/roslyn-analyzers/issues/6151 -public struct OctreeQuantizer : IQuantizer +public struct HexadecatreeQuantizer : IQuantizer #pragma warning restore CA1001 // Types that own disposable fields should be disposable where TPixel : unmanaged, IPixel { private readonly int maxColors; private readonly int bitDepth; - private readonly Octree octree; + private readonly Hexadecatree tree; private readonly IMemoryOwner paletteOwner; private ReadOnlyMemory palette; private PixelMap? pixelMap; @@ -32,19 +41,19 @@ public struct OctreeQuantizer : IQuantizer private bool isDisposed; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// - /// The configuration which allows altering default behavior or extending the library. - /// The quantizer options defining quantization rules. + /// The configuration that provides memory allocation and pixel conversion services. + /// The quantizer options that control palette size, dithering, and transparency behavior. [MethodImpl(InliningOptions.ShortMethod)] - public OctreeQuantizer(Configuration configuration, QuantizerOptions options) + public HexadecatreeQuantizer(Configuration configuration, QuantizerOptions options) { this.Configuration = configuration; this.Options = options; this.maxColors = this.Options.MaxColors; this.bitDepth = Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.maxColors), 1, 8); - this.octree = new Octree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold); + this.tree = new Hexadecatree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold); this.paletteOwner = configuration.MemoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); this.pixelMap = default; this.palette = default; @@ -76,23 +85,28 @@ public struct OctreeQuantizer : IQuantizer /// public readonly void AddPaletteColors(in Buffer2DRegion pixelRegion) { - PixelRowDelegate pixelRowDelegate = new(this.octree); - QuantizerUtilities.AddPaletteColors, TPixel, Rgba32, PixelRowDelegate>( + PixelRowDelegate pixelRowDelegate = new(this.tree); + QuantizerUtilities.AddPaletteColors, TPixel, Rgba32, PixelRowDelegate>( ref Unsafe.AsRef(in this), in pixelRegion, in pixelRowDelegate); } + /// + /// Materializes the final palette from the accumulated tree and prepares the dither lookup map when needed. + /// private void ResolvePalette() { short paletteIndex = 0; Span paletteSpan = this.paletteOwner.GetSpan(); - this.octree.Palettize(paletteSpan, ref paletteIndex); + this.tree.Palettize(paletteSpan, ref paletteIndex); ReadOnlyMemory result = this.paletteOwner.Memory[..paletteSpan.Length]; if (this.isDithering) { + // Dithered colors often no longer land on a color that was seen during palette construction, + // so the quantization pass switches to nearest-palette matching once the palette is finalized. this.pixelMap = PixelMapFactory.Create(this.Configuration, result, this.Options.ColorMatchingMode); } @@ -108,17 +122,15 @@ public struct OctreeQuantizer : IQuantizer [MethodImpl(InliningOptions.ShortMethod)] public readonly byte GetQuantizedColor(TPixel color, out TPixel match) { - // Due to the addition of new colors by dithering that are not part of the original histogram, - // the octree nodes might not match the correct color. - // In this case, we must use the pixel map to get the closest color. if (this.isDithering) { + // Dithering introduces adjusted colors that were never inserted into the tree, so tree lookup + // is only reliable for the non-dithered path. return (byte)this.pixelMap!.GetClosestColor(color, out match); } ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span); - - int index = this.octree.GetPaletteIndex(color); + int index = this.tree.GetPaletteIndex(color); match = Unsafe.Add(ref paletteRef, (nuint)index); return (byte)index; } @@ -132,34 +144,43 @@ public struct OctreeQuantizer : IQuantizer this.paletteOwner.Dispose(); this.pixelMap?.Dispose(); this.pixelMap = null; - this.octree.Dispose(); + this.tree.Dispose(); } } + /// + /// Forwards source rows into the tree without creating an intermediate buffer. + /// private readonly struct PixelRowDelegate : IQuantizingPixelRowDelegate { - private readonly Octree octree; + private readonly Hexadecatree tree; - public PixelRowDelegate(Octree octree) => this.octree = octree; + /// + /// Initializes a new instance of the struct. + /// + /// The destination tree that should accumulate each visited row. + public PixelRowDelegate(Hexadecatree tree) => this.tree = tree; - public void Invoke(ReadOnlySpan row, int rowIndex) => this.octree.AddColors(row); + /// + public void Invoke(ReadOnlySpan row, int rowIndex) => this.tree.AddColors(row); } /// - /// A hexadecatree-based color quantization structure used for fast color distance lookups and palette generation. - /// This tree maintains a fixed pool of nodes (capacity 4096) where each node can have up to 16 children, stores - /// color accumulation data, and supports dynamic node allocation and reduction. It offers near-constant-time insertions - /// and lookups while consuming roughly 240 KB for the node pool. + /// Stores the adaptive 16-way partition tree used to accumulate colors and emit palette entries. /// - internal sealed class Octree : IDisposable + /// + /// The tree uses a fixed node arena for predictable allocation behavior, keeps per-level reducible node lists so + /// deeper buckets can be merged until the palette fits, and caches the previously inserted leaf so repeated colors + /// can be accumulated cheaply. + /// + internal sealed class Hexadecatree : IDisposable { - // The memory allocator. - private readonly MemoryAllocator allocator; - // Pooled buffer for OctreeNodes. - private readonly IMemoryOwner nodesOwner; + private readonly IMemoryOwner nodesOwner; - // Reducible nodes: one per level; we use an integer index; -1 means “no node.” + // One reducible-node head per level. + // Each entry stores a node index, or -1 when that level currently + // has no reducible nodes. private readonly short[] reducibleNodes; // Maximum number of allowable colors. @@ -168,8 +189,8 @@ public struct OctreeQuantizer : IQuantizer // Maximum significant bits. private readonly int maxColorBits; - // The threshold for transparent colors. - private readonly int transparencyThreshold255; + // Keep the fractional byte-domain threshold so values such as .5 remain distinct from alpha 127. + private readonly float transparencyThreshold255; // Instead of a reference to the root, we store the index of the root node. // Index 0 is reserved for the root. @@ -186,13 +207,13 @@ public struct OctreeQuantizer : IQuantizer private readonly Stack freeIndices = new(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The configuration which allows altering default behavior or extending the library. - /// The maximum number of significant bits in the image. - /// The maximum number of colors to allow in the palette. - /// The threshold for transparent colors. - public Octree( + /// The configuration that provides the backing memory allocator. + /// The number of levels to descend before forcing leaves. + /// The maximum number of palette entries the reduced tree may retain. + /// The alpha threshold below which generated palette entries become fully transparent. + public Hexadecatree( Configuration configuration, int maxColorBits, int maxColors, @@ -200,15 +221,14 @@ public struct OctreeQuantizer : IQuantizer { this.maxColorBits = maxColorBits; this.maxColors = maxColors; - this.transparencyThreshold255 = (int)(transparencyThreshold * 255F); + this.transparencyThreshold255 = transparencyThreshold * 255F; this.Leaves = 0; this.previousNode = -1; this.previousColor = default; // Allocate a conservative buffer for nodes. const int capacity = 4096; - this.allocator = configuration.MemoryAllocator; - this.nodesOwner = this.allocator.Allocate(capacity, AllocationOptions.Clean); + this.nodesOwner = configuration.MemoryAllocator.Allocate(capacity, AllocationOptions.Clean); // Create the reducible nodes array (one per level 0 .. maxColorBits-1). this.reducibleNodes = new short[this.maxColorBits]; @@ -216,24 +236,24 @@ public struct OctreeQuantizer : IQuantizer // Reserve index 0 for the root. this.rootIndex = 0; - ref OctreeNode root = ref this.Nodes[this.rootIndex]; + ref Node root = ref this.Nodes[this.rootIndex]; root.Initialize(0, this.maxColorBits, this, this.rootIndex); } /// - /// Gets or sets the number of leaves in the tree. + /// Gets or sets the number of leaf nodes currently representing palette buckets. /// public int Leaves { get; set; } /// - /// Gets the full collection of nodes as a span. + /// Gets the underlying node arena. /// - internal Span Nodes => this.nodesOwner.Memory.Span; + internal Span Nodes => this.nodesOwner.Memory.Span; /// - /// Adds a span of colors to the octree. + /// Adds a row of colors to the tree. /// - /// A span of color values to be added. + /// The colors to accumulate. public void AddColors(ReadOnlySpan row) { for (int x = 0; x < row.Length; x++) @@ -243,12 +263,13 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Add a color to the Octree. + /// Adds a single color sample to the tree. /// - /// The color to add. + /// The color to accumulate. private void AddColor(Rgba32 color) { - // Ensure that the tree is not already full. + // Once the node arena is full and there are no recycled slots available, keep collapsing + // reducible leaves until the tree is small enough to make forward progress again. if (this.nextNode >= this.Nodes.Length && this.freeIndices.Count == 0) { while (this.Leaves > this.maxColors) @@ -257,32 +278,32 @@ public struct OctreeQuantizer : IQuantizer } } - // If the color is the same as the previous color, increment the node. - // Otherwise, add a new node. + // Scanlines often contain long runs of the same color. Caching the previous leaf lets those + // repeats skip the tree walk and just bump the accumulated sums in place. if (this.previousColor.Equals(color)) { if (this.previousNode == -1) { this.previousColor = color; - OctreeNode.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); + Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); } else { - OctreeNode.Increment(this.previousNode, color, this); + Node.Increment(this.previousNode, color, this); } } else { this.previousColor = color; - OctreeNode.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); + Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); } } /// - /// Construct the palette from the octree. + /// Reduces the tree to the requested palette size and emits the final palette entries. /// - /// The palette to construct. - /// The current palette index. + /// The destination palette span. + /// The running palette index. public void Palettize(Span palette, ref short paletteIndex) { while (this.Leaves > this.maxColors) @@ -294,48 +315,45 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Get the palette index for the passed color. + /// Gets the palette index selected by the tree for the supplied color. /// - /// The color to get the palette index for. - /// The . + /// The color to resolve. + /// The palette index represented by the best matching leaf in the reduced tree. [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetPaletteIndex(TPixel color) => this.Nodes[this.rootIndex].GetPaletteIndex(color.ToRgba32(), 0, this); /// - /// Track the previous node and color. + /// Records the most recently touched leaf so repeated colors can bypass another descent. /// - /// The node index. + /// The leaf node index. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void TrackPrevious(int nodeIndex) => this.previousNode = nodeIndex; /// - /// Reduce the depth of the tree. + /// Collapses the deepest currently reducible node into a single leaf. /// private void Reduce() { - // Find the deepest level containing at least one reducible node int index = this.maxColorBits - 1; while ((index > 0) && (this.reducibleNodes[index] == -1)) { index--; } - // Reduce the node most recently added to the list at level 'index' - ref OctreeNode node = ref this.Nodes[this.reducibleNodes[index]]; + ref Node node = ref this.Nodes[this.reducibleNodes[index]]; this.reducibleNodes[index] = node.NextReducibleIndex; - - // Decrement the leaf count after reducing the node node.Reduce(this); - // And just in case I've reduced the last color to be added, and the next color to - // be added is the same, invalidate the previousNode... + // If the last inserted leaf was merged away, the next repeated color must walk the tree again. this.previousNode = -1; } - // Allocate a new OctreeNode from the pooled buffer. - // First check the freeIndices stack. + /// + /// Allocates a node index from the free list or from the unused tail of the arena. + /// + /// The allocated node index, or -1 if no node can be allocated. internal short AllocateNode() { if (this.freeIndices.Count > 0) @@ -354,9 +372,9 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Free a node index, making it available for re-allocation. + /// Returns a node index to the free list. /// - /// The index to free. + /// The node index to recycle. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void FreeNode(short index) { @@ -367,32 +385,37 @@ public struct OctreeQuantizer : IQuantizer /// public void Dispose() => this.nodesOwner.Dispose(); + /// + /// Represents one node in the hexadecatree node arena. + /// [StructLayout(LayoutKind.Sequential)] - internal unsafe struct OctreeNode + internal struct Node { public bool Leaf; - public int PixelCount; - public int Red; - public int Green; - public int Blue; - public int Alpha; + public long PixelCount; + public long Red; + public long Green; + public long Blue; + public long Alpha; public short PaletteIndex; public short NextReducibleIndex; private InlineArray16 children; + /// + /// Gets the 16 child slots for this node. + /// [UnscopedRef] public Span Children => this.children; /// - /// Initialize the . + /// Initializes a node either as a leaf or as a reducible interior node. /// - /// The level of the node. - /// The number of significant color bits in the image. - /// The parent octree. - /// The index of the node. - public void Initialize(int level, int colorBits, Octree octree, short index) + /// The depth of the node being initialized. + /// The maximum tree depth. + /// The owning tree. + /// The node index in the arena. + public void Initialize(int level, int colorBits, Hexadecatree tree, short index) { - // Construct the new node. this.Leaf = level == colorBits; this.Red = 0; this.Green = 0; @@ -401,76 +424,73 @@ public struct OctreeQuantizer : IQuantizer this.PixelCount = 0; this.PaletteIndex = 0; this.NextReducibleIndex = -1; - - // Always clear the Children array. this.Children.Fill(-1); if (this.Leaf) { - octree.Leaves++; + tree.Leaves++; } else { - // Add this node to the reducible nodes list for its level. - this.NextReducibleIndex = octree.reducibleNodes[level]; - octree.reducibleNodes[level] = index; + // Track reducible nodes per level so palette reduction can always collapse the deepest + // buckets first without scanning the entire arena. + this.NextReducibleIndex = tree.reducibleNodes[level]; + tree.reducibleNodes[level] = index; } } /// - /// Add a color to the Octree. + /// Descends the tree for the supplied color, allocating nodes as needed until a leaf is reached. /// - /// The node index. - /// The color to add. - /// The number of significant color bits in the image. - /// The level of the node. - /// The parent octree. - public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Octree octree) + /// The current node index. + /// The color being accumulated. + /// The maximum tree depth. + /// The current depth. + /// The owning tree. + public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Hexadecatree tree) { - ref OctreeNode node = ref octree.Nodes[nodeIndex]; + ref Node node = ref tree.Nodes[nodeIndex]; if (node.Leaf) { - Increment(nodeIndex, color, octree); - octree.TrackPrevious(nodeIndex); + Increment(nodeIndex, color, tree); + tree.TrackPrevious(nodeIndex); + return; } - else - { - int index = GetColorIndex(color, level); - short childIndex; - Span children = node.Children; - childIndex = children[index]; + int index = GetColorIndex(color, level); + Span children = node.Children; + short childIndex = children[index]; + if (childIndex == -1) + { + childIndex = tree.AllocateNode(); if (childIndex == -1) { - childIndex = octree.AllocateNode(); - - if (childIndex == -1) - { - // No room in the tree, so increment the count and return. - Increment(nodeIndex, color, octree); - octree.TrackPrevious(nodeIndex); - return; - } - - ref OctreeNode child = ref octree.Nodes[childIndex]; - child.Initialize(level + 1, colorBits, octree, childIndex); - children[index] = childIndex; + // If the arena is exhausted and no node can be reclaimed yet, fall back to + // accumulating into the current node instead of failing the insert outright. + Increment(nodeIndex, color, tree); + tree.TrackPrevious(nodeIndex); + return; } - AddColor(childIndex, color, colorBits, level + 1, octree); + ref Node child = ref tree.Nodes[childIndex]; + child.Initialize(level + 1, colorBits, tree, childIndex); + children[index] = childIndex; } + + // Keep descending until we reach the leaf bucket that should accumulate this sample. + AddColor(childIndex, color, colorBits, level + 1, tree); } /// - /// Increment the color components of this node. + /// Adds the supplied color sample to an existing node's running sums. /// - /// The node index. - /// The color to increment by. - /// The parent octree. - public static void Increment(int nodeIndex, Rgba32 color, Octree octree) + /// The node index to update. + /// The color sample being accumulated. + /// The owning tree. + public static void Increment(int nodeIndex, Rgba32 color, Hexadecatree tree) { - ref OctreeNode node = ref octree.Nodes[nodeIndex]; + ref Node node = ref tree.Nodes[nodeIndex]; node.PixelCount++; node.Red += color.R; node.Green += color.G; @@ -479,10 +499,10 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Reduce this node by ensuring its children are all reduced (i.e. leaves) and then merging their data. + /// Merges all child nodes into this node and turns it into a leaf. /// - /// The parent octree. - public void Reduce(Octree octree) + /// The owning tree. + public void Reduce(Hexadecatree tree) { // If already a leaf, do nothing. if (this.Leaf) @@ -490,27 +510,30 @@ public struct OctreeQuantizer : IQuantizer return; } - // Now merge the (presumably reduced) children. - int pixelCount = 0; - int sumRed = 0, sumGreen = 0, sumBlue = 0, sumAlpha = 0; + // Allocation fallback can accumulate samples on an interior node. Seed the merge + // with this node's own sums so reduction preserves those samples with its children. + long pixelCount = this.PixelCount; + long sumRed = this.Red; + long sumGreen = this.Green; + long sumBlue = this.Blue; + long sumAlpha = this.Alpha; Span children = this.Children; + for (int i = 0; i < children.Length; i++) { short childIndex = children[i]; if (childIndex != -1) { - ref OctreeNode child = ref octree.Nodes[childIndex]; - int pixels = child.PixelCount; - + ref Node child = ref tree.Nodes[childIndex]; + long pixels = child.PixelCount; sumRed += child.Red; sumGreen += child.Green; sumBlue += child.Blue; sumAlpha += child.Alpha; pixelCount += pixels; - // Free the child immediately. children[i] = -1; - octree.FreeNode(childIndex); + tree.FreeNode(childIndex); } } @@ -529,16 +552,16 @@ public struct OctreeQuantizer : IQuantizer } this.Leaf = true; - octree.Leaves++; + tree.Leaves++; } /// - /// Traverse the tree to construct the palette. + /// Traverses the reduced tree and emits one palette color per leaf. /// - /// The parent octree. - /// The palette to construct. - /// The current palette index. - public void ConstructPalette(Octree octree, Span palette, ref short paletteIndex) + /// The owning tree. + /// The destination palette span. + /// The running palette index. + public void ConstructPalette(Hexadecatree tree, Span palette, ref short paletteIndex) { if (this.Leaf) { @@ -549,13 +572,12 @@ public struct OctreeQuantizer : IQuantizer Vector4.Zero, new Vector4(255)); - if (vector.W < octree.transparencyThreshold255) + if (vector.W < tree.transparencyThreshold255) { vector = Vector4.Zero; } palette[paletteIndex] = TPixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, (byte)vector.W)); - this.PaletteIndex = paletteIndex++; } else @@ -566,19 +588,20 @@ public struct OctreeQuantizer : IQuantizer int childIndex = children[i]; if (childIndex != -1) { - octree.Nodes[childIndex].ConstructPalette(octree, palette, ref paletteIndex); + tree.Nodes[childIndex].ConstructPalette(tree, palette, ref paletteIndex); } } } } /// - /// Get the palette index for the passed color. + /// Resolves the palette index represented by this node for the supplied color. /// - /// The color to get the palette index for. - /// The level of the node. - /// The parent octree. - public int GetPaletteIndex(Rgba32 color, int level, Octree octree) + /// The color to resolve. + /// The current tree depth. + /// The owning tree. + /// The palette index for the best reachable leaf, or -1 if no leaf can be reached. + public int GetPaletteIndex(Rgba32 color, int level, Hexadecatree tree) { if (this.Leaf) { @@ -590,15 +613,16 @@ public struct OctreeQuantizer : IQuantizer int childIndex = children[colorIndex]; if (childIndex != -1) { - return octree.Nodes[childIndex].GetPaletteIndex(color, level + 1, octree); + return tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree); } + // After reductions the exact branch can disappear, so fall back to the first reachable descendant leaf. for (int i = 0; i < children.Length; i++) { childIndex = children[i]; if (childIndex != -1) { - int childPaletteIndex = octree.Nodes[childIndex].GetPaletteIndex(color, level + 1, octree); + int childPaletteIndex = tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree); if (childPaletteIndex != -1) { return childPaletteIndex; @@ -610,37 +634,35 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Gets the color index at the given level. + /// Computes the child slot for a color at the supplied tree level. /// - /// The color to get the index for. - /// The level to get the index at. + /// The color being routed. + /// The tree depth whose bit plane should be sampled. + /// The child slot index for the color at the supplied level. + /// + /// For fully opaque mid-tone colors the tree ignores alpha and routes on RGB only, preserving more branch + /// resolution for visible color detail. For transparent, dark, and light colors it includes alpha as the + /// most significant routing bit so opacity changes can form their own branches. + /// public static int GetColorIndex(Rgba32 color, int level) { - // Determine how many bits to shift based on the current tree level. - // At level 0, shift = 7; as level increases, the shift decreases. + // Sample one bit plane per level, starting at the most significant bit and moving downward. int shift = 7 - level; byte mask = (byte)(1 << shift); - // Compute the luminance of the RGB components using the BT.709 standard. - // This gives a measure of brightness for the color. + // Use BT.709 luminance as a cheap brightness estimate for deciding whether alpha carries + // useful information at this level for fully opaque colors. int luminance = ColorNumerics.Get8BitBT709Luminance(color.R, color.G, color.B); - // Define thresholds for determining when to include the alpha bit in the index. - // The thresholds are scaled according to the current level. - // 128 is the midpoint of the 8-bit range (0–255), so shifting it right by 'level' - // produces a threshold that scales with the color cube subdivision. + // Scale the brightness thresholds with depth so deeper levels become stricter about when + // to spend a branch bit on alpha instead of RGB detail. int darkThreshold = 128 >> level; - - // The light threshold is set symmetrically: 255 minus the scaled midpoint. int lightThreshold = 255 - (128 >> level); - // If the pixel is fully opaque and its brightness falls between the dark and light thresholds, - // ignore the alpha channel to maximize RGB resolution. - // Otherwise (if the pixel is dark, light, or semi-transparent), include the alpha bit - // to preserve any gradient that may be present. if (color.A == 255 && luminance > darkThreshold && luminance < lightThreshold) { - // Extract one bit each from R, G, and B channels and combine them into a 3-bit index. + // Fully opaque mid-tone colors route on RGB only, which preserves more visible color + // resolution because alpha would contribute no extra separation here. int rBits = ((color.R & mask) >> shift) << 2; int gBits = ((color.G & mask) >> shift) << 1; int bBits = (color.B & mask) >> shift; @@ -648,7 +670,8 @@ public struct OctreeQuantizer : IQuantizer } else { - // Extract one bit from each channel including alpha (alpha becomes the most significant bit). + // Transparent, dark, and light colors include alpha as the high routing bit so opacity + // changes can form distinct buckets alongside RGB differences. int aBits = ((color.A & mask) >> shift) << 3; int rBits = ((color.R & mask) >> shift) << 2; int gBits = ((color.G & mask) >> shift) << 1; diff --git a/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs b/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs index 32d95137b..76598e004 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs @@ -56,147 +56,6 @@ internal interface IColorIndexCache : IColorIndexCache public static abstract T Create(MemoryAllocator allocator); } -/// -/// A hybrid color distance cache that combines a small, fixed-capacity exact-match dictionary -/// (ExactCache, ~4–5 KB for up to 512 entries) with a coarse lookup table (CoarseCache) for 5,5,5,6 precision. -/// -/// -/// ExactCache provides O(1) lookup for common cases using a simple 256-entry hash-based dictionary, while CoarseCache -/// quantizes RGB channels to 5 bits (yielding 32^3 buckets) and alpha to 6 bits, storing up to 4 alpha entries per bucket -/// (a design chosen based on probability theory to capture most real-world variations) for a total memory footprint of -/// roughly 576 KB. Lookups and insertions are performed in constant time, making the overall design both fast and memory-predictable. -/// -internal unsafe struct HybridCache : IColorIndexCache -{ - private CoarseCache coarseCache; - private AccurateCache accurateCache; - - public HybridCache(MemoryAllocator allocator) - { - this.accurateCache = AccurateCache.Create(allocator); - this.coarseCache = CoarseCache.Create(allocator); - } - - /// - public static HybridCache Create(MemoryAllocator allocator) => new(allocator); - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public bool TryAdd(Rgba32 color, short index) - { - if (this.accurateCache.TryAdd(color, index)) - { - return true; - } - - return this.coarseCache.TryAdd(color, index); - } - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly bool TryGetValue(Rgba32 color, out short value) - { - if (this.accurateCache.TryGetValue(color, out value)) - { - return true; - } - - return this.coarseCache.TryGetValue(color, out value); - } - - /// - public readonly void Clear() - { - this.accurateCache.Clear(); - this.coarseCache.Clear(); - } - - /// - public void Dispose() - { - this.accurateCache.Dispose(); - this.coarseCache.Dispose(); - } -} - -/// -/// A coarse cache for color distance lookups that uses a fixed-size lookup table. -/// -/// -/// This cache uses a fixed lookup table with 2,097,152 bins, each storing a 2-byte value, -/// resulting in a memory usage of approximately 4 MB. Lookups and insertions are -/// performed in constant time (O(1)) via direct table indexing. This design is optimized for -/// speed while maintaining a predictable, fixed memory footprint. -/// -internal unsafe struct CoarseCache : IColorIndexCache -{ - private const int IndexRBits = 5; - private const int IndexGBits = 5; - private const int IndexBBits = 5; - private const int IndexABits = 6; - private const int IndexRCount = 1 << IndexRBits; // 32 bins for red - private const int IndexGCount = 1 << IndexGBits; // 32 bins for green - private const int IndexBCount = 1 << IndexBBits; // 32 bins for blue - private const int IndexACount = 1 << IndexABits; // 64 bins for alpha - private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount; // 2,097,152 bins - - private readonly IMemoryOwner binsOwner; - private readonly short* binsPointer; - private MemoryHandle binsHandle; - - private CoarseCache(MemoryAllocator allocator) - { - this.binsOwner = allocator.Allocate(TotalBins); - this.binsOwner.GetSpan().Fill(-1); - this.binsHandle = this.binsOwner.Memory.Pin(); - this.binsPointer = (short*)this.binsHandle.Pointer; - } - - /// - public static CoarseCache Create(MemoryAllocator allocator) => new(allocator); - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly bool TryAdd(Rgba32 color, short value) - { - this.binsPointer[GetCoarseIndex(color)] = value; - return true; - } - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly bool TryGetValue(Rgba32 color, out short value) - { - value = this.binsPointer[GetCoarseIndex(color)]; - return value > -1; // Coarse match found - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static int GetCoarseIndex(Rgba32 color) - { - int rIndex = color.R >> (8 - IndexRBits); - int gIndex = color.G >> (8 - IndexGBits); - int bIndex = color.B >> (8 - IndexBBits); - int aIndex = color.A >> (8 - IndexABits); - - return (aIndex * IndexRCount * IndexGCount * IndexBCount) + - (rIndex * IndexGCount * IndexBCount) + - (gIndex * IndexBCount) + - bIndex; - } - - /// - public readonly void Clear() - => this.binsOwner.GetSpan().Fill(-1); - - /// - public void Dispose() - { - this.binsHandle.Dispose(); - this.binsOwner.Dispose(); - } -} - /// /// /// CoarseCache is a fast, low-memory lookup structure for caching palette indices associated with RGBA values, @@ -225,7 +84,7 @@ internal unsafe struct CoarseCache : IColorIndexCache /// making it ideal for applications such as color distance caching in images with a limited palette (up to 256 entries). /// /// -internal unsafe struct CoarseCacheLite : IColorIndexCache +internal unsafe struct CoarseCache : IColorIndexCache { // Use 5 bits per channel for R, G, and B: 32 levels each. // Total buckets = 32^3 = 32768. @@ -236,7 +95,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache private readonly AlphaBucket* buckets; private MemoryHandle bucketHandle; - private CoarseCacheLite(MemoryAllocator allocator) + private CoarseCache(MemoryAllocator allocator) { this.bucketsOwner = allocator.Allocate(BucketCount, AllocationOptions.Clean); this.bucketHandle = this.bucketsOwner.Memory.Pin(); @@ -244,7 +103,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache } /// - public static CoarseCacheLite Create(MemoryAllocator allocator) => new(allocator); + public static CoarseCache Create(MemoryAllocator allocator) => new(allocator); /// public readonly bool TryAdd(Rgba32 color, short paletteIndex) @@ -289,14 +148,11 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache } [MethodImpl(InliningOptions.ShortMethod)] - private static byte QuantizeAlpha(byte a) - - // Quantize to 6 bits: shift right by (8 - 6) = 2 bits. - => (byte)(a >> 2); + private static byte QuantizeAlpha(byte a) => (byte)(a >> 2); public struct AlphaEntry { - // Store the alpha value quantized to 6 bits (0..63) + // Store the alpha value quantized to 6 bits (0..63). public byte QuantizedAlpha; public short PaletteIndex; } @@ -312,7 +168,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache // 2. However, in practice (based on probability theory and typical image data), // the number of unique alpha values that actually occur for a given quantized RGB // bucket is usually very small. If you randomly sample 8 values out of 64, - // the probability that these 4 samples are all unique is high if the distribution + // the probability that these samples are all unique is high if the distribution // of alpha values is skewed or if only a few alpha values are used. // // 3. Statistically, for many real-world images, most RGB buckets will have only a couple @@ -377,51 +233,49 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache } /// -/// A fixed-capacity dictionary with exactly 512 entries mapping a key -/// to a value. +/// A fixed-size exact-match cache that stores packed RGBA keys with 4-way set associativity. /// /// -/// The dictionary is implemented using a fixed array of 512 buckets and an entries array -/// of the same size. The bucket for a key is computed as (key & 0x1FF), and collisions are -/// resolved through a linked chain stored in the field. +/// The cache holds 512 total entries split across 128 sets. Entries are evicted within a set +/// using round-robin replacement, but cached values are returned only when the full packed RGBA +/// key matches, preserving exact quantization results with predictable memory usage. /// The overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are, -/// on average, O(1) since the bucket is determined via a simple bitmask and collision chains are -/// typically very short; in the worst-case, the number of iterations is bounded by 256. +/// on average, O(1) since each lookup probes at most four candidate entries within the selected set. /// This guarantees highly efficient and predictable performance for small, fixed-size color palettes. /// internal unsafe struct AccurateCache : IColorIndexCache { - // Buckets array: each bucket holds the index (0-based) into the entries array - // of the first entry in the chain, or -1 if empty. - private readonly IMemoryOwner bucketsOwner; - private MemoryHandle bucketsHandle; - private short* buckets; + public const int Capacity = 512; + private const int Ways = 4; + private const int SetCount = Capacity / Ways; + private const int SetMask = SetCount - 1; - // Entries array: stores up to 256 entries. - private readonly IMemoryOwner entriesOwner; - private MemoryHandle entriesHandle; - private Entry* entries; + private readonly IMemoryOwner keysOwner; + private MemoryHandle keysHandle; + private uint* keys; - public const int Capacity = 512; + private readonly IMemoryOwner valuesOwner; + private MemoryHandle valuesHandle; + private ushort* values; + + private readonly IMemoryOwner nextVictimOwner; + private MemoryHandle nextVictimHandle; + private byte* nextVictim; private AccurateCache(MemoryAllocator allocator) { - this.Count = 0; - - // Allocate exactly 512 indexes for buckets. - this.bucketsOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); - Span bucketSpan = this.bucketsOwner.GetSpan(); - bucketSpan.Fill(-1); - this.bucketsHandle = this.bucketsOwner.Memory.Pin(); - this.buckets = (short*)this.bucketsHandle.Pointer; - - // Allocate exactly 512 entries. - this.entriesOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); - this.entriesHandle = this.entriesOwner.Memory.Pin(); - this.entries = (Entry*)this.entriesHandle.Pointer; - } + this.keysOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); + this.keysHandle = this.keysOwner.Memory.Pin(); + this.keys = (uint*)this.keysHandle.Pointer; - public int Count { get; private set; } + this.valuesOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); + this.valuesHandle = this.valuesOwner.Memory.Pin(); + this.values = (ushort*)this.valuesHandle.Pointer; + + this.nextVictimOwner = allocator.Allocate(SetCount, AllocationOptions.Clean); + this.nextVictimHandle = this.nextVictimOwner.Memory.Pin(); + this.nextVictim = (byte*)this.nextVictimHandle.Pointer; + } /// public static AccurateCache Create(MemoryAllocator allocator) => new(allocator); @@ -430,140 +284,113 @@ internal unsafe struct AccurateCache : IColorIndexCache [MethodImpl(InliningOptions.ShortMethod)] public bool TryAdd(Rgba32 color, short value) { - if (this.Count == Capacity) - { - return false; // Dictionary is full. - } - uint key = color.PackedValue; + int set = GetSetIndex(key); + int start = set * Ways; + int empty = -1; + + uint* keys = this.keys; + ushort* values = this.values; + ushort storedValue = (ushort)(value + 1); - // The key is a 32-bit unsigned integer representing an RGBA color, where the bytes are laid out as R|G|B|A - // (with R in the most significant byte and A in the least significant). - // To compute the bucket index: - // 1. (key >> 16) extracts the top 16 bits, effectively giving us the R and G channels. - // 2. (key >> 8) shifts the key right by 8 bits, bringing R, G, and B into the lower 24 bits (dropping A). - // 3. XORing these two values with the original key mixes bits from all four channels (R, G, B, and A), - // which helps to counteract situations where one or more channels have a limited range. - // 4. Finally, we apply a bitmask of 0x1FF to keep only the lowest 9 bits, ensuring the result is between 0 and 511, - // which corresponds to our fixed bucket count of 512. - int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); - int i = this.buckets[bucket]; - - // Traverse the collision chain. - Entry* entries = this.entries; - while (i != -1) + for (int i = start; i < start + Ways; i++) { - Entry e = entries[i]; - if (e.Key == key) + ushort candidate = values[i]; + if (candidate == 0) { - // Key already exists; do not overwrite. - return false; + empty = i; + continue; } - i = e.Next; + if (keys[i] == key) + { + values[i] = storedValue; + return true; + } } - short index = (short)this.Count; - this.Count++; + int slot = empty >= 0 ? empty : start + this.nextVictim[set]; + keys[slot] = key; + values[slot] = storedValue; - // Insert the new entry: - entries[index].Key = key; - entries[index].Value = value; + if (empty < 0) + { + this.nextVictim[set] = (byte)((this.nextVictim[set] + 1) & (Ways - 1)); + } - // Link this new entry into the bucket chain. - entries[index].Next = this.buckets[bucket]; - this.buckets[bucket] = index; return true; } /// [MethodImpl(InliningOptions.ShortMethod)] - public bool TryGetValue(Rgba32 color, out short value) + public readonly bool TryGetValue(Rgba32 color, out short value) { uint key = color.PackedValue; - int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); - int i = this.buckets[bucket]; + int start = GetSetIndex(key) * Ways; - // If the bucket is empty, return immediately. - if (i == -1) - { - value = -1; - return false; - } + uint* keys = this.keys; + ushort* values = this.values; - // Traverse the chain. - Entry* entries = this.entries; - do + for (int i = start; i < start + Ways; i++) { - Entry e = entries[i]; - if (e.Key == key) + ushort candidate = values[i]; + if (candidate != 0 && keys[i] == key) { - value = e.Value; + value = (short)(candidate - 1); return true; } - - i = e.Next; } - while (i != -1); value = -1; return false; } /// - /// Clears the dictionary. + /// Clears the cache. /// - public void Clear() + public readonly void Clear() { - Span bucketSpan = this.bucketsOwner.GetSpan(); - bucketSpan.Fill(-1); - this.Count = 0; + this.valuesOwner.GetSpan().Clear(); + this.nextVictimOwner.GetSpan().Clear(); } public void Dispose() { - this.bucketsHandle.Dispose(); - this.bucketsOwner.Dispose(); - this.entriesHandle.Dispose(); - this.entriesOwner.Dispose(); - this.buckets = null; - this.entries = null; + this.keysHandle.Dispose(); + this.keysOwner.Dispose(); + this.valuesHandle.Dispose(); + this.valuesOwner.Dispose(); + this.nextVictimHandle.Dispose(); + this.nextVictimOwner.Dispose(); + this.keys = null; + this.values = null; + this.nextVictim = null; } - private struct Entry - { - public uint Key; // The key (packed RGBA) - public short Value; // The value; -1 means unused. - public short Next; // Index of the next entry in the chain, or -1 if none. - } -} - -/// -/// Represents a cache that does not store any values. -/// It allows adding colors, but always returns false when trying to retrieve them. -/// -internal readonly struct NullCache : IColorIndexCache -{ - /// - public static NullCache Create(MemoryAllocator allocator) => default; - - /// - public bool TryAdd(Rgba32 color, short value) => true; - - /// - public bool TryGetValue(Rgba32 color, out short value) - { - value = -1; - return false; - } - - /// - public void Clear() - { - } - - /// - public void Dispose() - { - } + /// + /// Maps a packed RGBA key to one of the cache sets used by . + /// + /// The packed key. + /// The zero-based set index for the key. + /// + /// + /// The cache is 4-way set-associative, so this hash only needs to choose one of + /// sets before probing up to four candidate entries. + /// + /// + /// is laid out as R | (G << 8) | (B << 16) | (A << 24). + /// The XOR-fold mixes neighboring bytes into the low bits, and the final mask selects the + /// set. With the current 128-set layout that makes the selected set effectively depend on + /// the low 7 bits of R ^ G ^ B. Alpha still participates in the later exact key + /// comparison, but not in set selection. + /// + /// + /// Collisions are expected and acceptable here. Correctness comes from the full packed-key + /// comparison during probing; this hash only aims to spread keys cheaply enough that each + /// access touches at most one 4-entry set. + /// + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetSetIndex(uint key) + => (int)(((key >> 16) ^ (key >> 8) ^ key) & SetMask); } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs index e121aff90..1e3453294 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs @@ -39,7 +39,7 @@ public static class QuantizerUtilities /// Returns true if transparent pixels can be replaced; otherwise, false. public static bool ShouldReplacePixelsByAlphaThreshold(float threshold) where TPixel : unmanaged, IPixel - => threshold > 0 && TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated; + => threshold > 0 && TPixel.GetPixelTypeInfo().AlphaRepresentation != PixelAlphaRepresentation.None; /// /// Replaces pixels in a span with fully transparent pixels based on an alpha threshold. @@ -295,11 +295,11 @@ public static class QuantizerUtilities for (int y = 0; y < source.Height; y++) { Span sourceRow = source.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ReplacePixelsByAlphaThreshold(vectorRow, threshold); - PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, delegateRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, delegateRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); rowDelegate.Invoke(delegateRow, y); } } @@ -308,11 +308,11 @@ public static class QuantizerUtilities for (int y = 0; y < source.Height; y++) { Span sourceRow = source.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); EncodingUtilities.ReplaceTransparentPixels(vectorRow); - PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, delegateRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, delegateRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); rowDelegate.Invoke(delegateRow, y); } } @@ -363,11 +363,11 @@ public static class QuantizerUtilities for (int y = 0; y < region.Height; y++) { Span sourceRow = region.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ReplacePixelsByAlphaThreshold(vectorRow, threshold); - PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, quantizingRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, quantizingRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y); for (int x = 0; x < destinationRow.Length; x++) @@ -381,11 +381,11 @@ public static class QuantizerUtilities for (int y = 0; y < region.Height; y++) { Span sourceRow = region.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); EncodingUtilities.ReplaceTransparentPixels(vectorRow); - PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, quantizingRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, quantizingRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y); for (int x = 0; x < destinationRow.Length; x++) @@ -420,11 +420,11 @@ public static class QuantizerUtilities for (int y = 0; y < region.Height; y++) { Span sourceRow = region.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); ReplacePixelsByAlphaThreshold(vectorRow, threshold); - PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, sourceRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, sourceRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } else @@ -432,11 +432,11 @@ public static class QuantizerUtilities for (int y = 0; y < region.Height; y++) { Span sourceRow = region.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(configuration, sourceRow, vectorRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); EncodingUtilities.ReplaceTransparentPixels(vectorRow); - PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, sourceRow, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, vectorRow, sourceRow, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index 03d6ac0da..b4b95adc1 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -144,7 +144,8 @@ internal struct WuQuantizer : IQuantizer normalized = Vector4.Zero; } - paletteSpan[k] = TPixel.FromScaledVector4(normalized); + // Histogram moments are accumulated from straight-alpha Rgba32 values, so the normalized palette color is unassociated. + paletteSpan[k] = TPixel.FromUnassociatedScaledVector4(normalized); } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs index 96c350c6c..dd992df8c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs @@ -53,22 +53,20 @@ internal class CropProcessor : TransformProcessor && this.SourceRectangle == this.cropRectangle) { // the cloned will be blank here copy all the pixel data over - source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); + source.PixelBuffer.CopyTo(destination.PixelBuffer); return; } Rectangle bounds = this.cropRectangle; - // Copying is cheap, we should process more pixels per task: - ParallelExecutionSettings parallelSettings = - ParallelExecutionSettings.FromConfiguration(this.Configuration).MultiplyMinimumPixelsPerTask(4); - + // Copying is too cheap to benefit from parallelization; + // the overhead exceeds the work per task. See #3111. RowOperation operation = new(bounds, source.PixelBuffer, destination.PixelBuffer); - ParallelRowIterator.IterateRows( - bounds, - in parallelSettings, - in operation); + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + operation.Invoke(y); + } } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs index 8a76ca42f..b9973eea3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs @@ -96,13 +96,16 @@ internal class EntropyCropProcessor : ImageProcessor int height = bitmap.Height; Point topLeft = default; Point bottomRight = default; + + // BinaryThresholdProcessor produces opaque black and white, so association no longer affects the components. Scaled vectors still normalize pixel formats whose native vectors use another range. Func, int, int, float, bool> delegateFunc = channel switch { - RgbaComponent.R => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToVector4().X - b) > Constants.Epsilon, - RgbaComponent.G => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToVector4().Y - b) > Constants.Epsilon, - RgbaComponent.B => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToVector4().Z - b) > Constants.Epsilon, - _ => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToVector4().W - b) > Constants.Epsilon, + RgbaComponent.R => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToUnassociatedScaledVector4().X - b) > Constants.Epsilon, + RgbaComponent.G => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToUnassociatedScaledVector4().Y - b) > Constants.Epsilon, + RgbaComponent.B => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToUnassociatedScaledVector4().Z - b) > Constants.Epsilon, + _ => (pixels, x, y, b) => MathF.Abs(pixels[x, y].ToUnassociatedScaledVector4().W - b) > Constants.Epsilon, }; + int GetMinY(ImageFrame pixels) { for (int y = 0; y < height; y++) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs index 260a366b9..61cde6e2a 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -199,15 +199,14 @@ internal class AffineTransformProcessor : TransformProcessor, IR int maxY = this.bounds.Bottom - 1; int minX = this.bounds.X; int maxX = this.bounds.Right - 1; + PixelOperations pixelOperations = PixelOperations.Instance; for (int y = rows.Min; y < rows.Max; y++) { Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4( - this.configuration, - destinationRowSpan, - span, - PixelConversionModifiers.Scale); + + // The temporary row is reused, so clear values for out-of-bounds samples that skip the resampling loop below. + span.Clear(); for (int x = 0; x < span.Length; x++) { @@ -225,6 +224,7 @@ internal class AffineTransformProcessor : TransformProcessor, IR continue; } + // Resampling interpolates color with coverage, so use associated vectors to prevent transparent RGB from bleeding into visible edges. Vector4 sum = Vector4.Zero; for (int yK = top; yK <= bottom; yK++) { @@ -235,8 +235,7 @@ internal class AffineTransformProcessor : TransformProcessor, IR { float xWeight = sampler.GetValue(xK - pX); - Vector4 current = sourceRowSpan[xK].ToScaledVector4(); - Numerics.Premultiply(ref current); + Vector4 current = sourceRowSpan[xK].ToAssociatedScaledVector4(); sum += current * xWeight * yWeight; } } @@ -244,12 +243,7 @@ internal class AffineTransformProcessor : TransformProcessor, IR span[x] = sum; } - Numerics.UnPremultiply(span); - PixelOperations.Instance.FromVector4Destructive( - this.configuration, - span, - destinationRowSpan, - PixelConversionModifiers.Scale); + pixelOperations.FromVector4Destructive(this.configuration, span, destinationRowSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs index ecf587684..90ca8743c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -197,15 +197,14 @@ internal class ProjectiveTransformProcessor : TransformProcessor int maxY = this.bounds.Bottom - 1; int minX = this.bounds.X; int maxX = this.bounds.Right - 1; + PixelOperations pixelOperations = PixelOperations.Instance; for (int y = rows.Min; y < rows.Max; y++) { Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); - PixelOperations.Instance.ToVector4( - this.configuration, - destinationRowSpan, - span, - PixelConversionModifiers.Scale); + + // The temporary row is reused, so clear values for out-of-bounds samples that skip the resampling loop below. + span.Clear(); for (int x = 0; x < span.Length; x++) { @@ -223,6 +222,7 @@ internal class ProjectiveTransformProcessor : TransformProcessor continue; } + // Resampling interpolates color with coverage, so use associated vectors to prevent transparent RGB from bleeding into visible edges. Vector4 sum = Vector4.Zero; for (int yK = top; yK <= bottom; yK++) { @@ -233,8 +233,7 @@ internal class ProjectiveTransformProcessor : TransformProcessor { float xWeight = sampler.GetValue(xK - pX); - Vector4 current = sourceRowSpan[xK].ToScaledVector4(); - Numerics.Premultiply(ref current); + Vector4 current = sourceRowSpan[xK].ToAssociatedScaledVector4(); sum += current * xWeight * yWeight; } } @@ -242,12 +241,7 @@ internal class ProjectiveTransformProcessor : TransformProcessor span[x] = sum; } - Numerics.UnPremultiply(span); - PixelOperations.Instance.FromVector4Destructive( - this.configuration, - span, - destinationRowSpan, - PixelConversionModifiers.Scale); + pixelOperations.FromVector4Destructive(this.configuration, span, destinationRowSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs index 8d3ded09f..e9d0ecf57 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs @@ -97,7 +97,7 @@ internal class RotateProcessor : AffineTransformProcessor if (MathF.Abs(degrees) < Constants.Epsilon) { // The destination will be blank here so copy all the pixel data over - source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); + source.PixelBuffer.CopyTo(destination.PixelBuffer); return true; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index a85487d1c..c0380224c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -104,8 +104,8 @@ internal readonly unsafe struct ResizeKernel Vector256 pixels256_0 = Unsafe.As>(ref rowStartRef); Vector256 pixels256_1 = Unsafe.As>(ref Unsafe.Add(ref rowStartRef, (nuint)2)); - result256_0 = Vector256_.MultiplyAdd(result256_0, Vector256.Load(bufferStart), pixels256_0); - result256_1 = Vector256_.MultiplyAdd(result256_1, Vector256.Load(bufferStart + 8), pixels256_1); + result256_0 = Vector256_.MultiplyAddEstimate(Vector256.Load(bufferStart), pixels256_0, result256_0); + result256_1 = Vector256_.MultiplyAddEstimate(Vector256.Load(bufferStart + 8), pixels256_1, result256_1); bufferStart += 16; rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)4); @@ -116,7 +116,7 @@ internal readonly unsafe struct ResizeKernel if ((this.Length & 3) >= 2) { Vector256 pixels256_0 = Unsafe.As>(ref rowStartRef); - result256_0 = Vector256_.MultiplyAdd(result256_0, Vector256.Load(bufferStart), pixels256_0); + result256_0 = Vector256_.MultiplyAddEstimate(Vector256.Load(bufferStart), pixels256_0, result256_0); bufferStart += 8; rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)2); @@ -127,7 +127,7 @@ internal readonly unsafe struct ResizeKernel if ((this.Length & 1) != 0) { Vector128 pixels128 = Unsafe.As>(ref rowStartRef); - result128 = Vector128_.MultiplyAdd(result128, Vector128.Load(bufferStart), pixels128); + result128 = Vector128_.MultiplyAddEstimate(Vector128.Load(bufferStart), pixels128, result128); } return result128.AsVector4(); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs index 49439545b..870c38241 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs @@ -91,7 +91,7 @@ internal class ResizeProcessor : TransformProcessor, IResampling ImageFrame destinationFrame = destination.Frames[i]; // The cloned will be blank here copy all the pixel data over - sourceFrame.GetPixelMemoryGroup().CopyTo(destinationFrame.GetPixelMemoryGroup()); + sourceFrame.PixelBuffer.CopyTo(destinationFrame.PixelBuffer); } return; @@ -189,16 +189,6 @@ internal class ResizeProcessor : TransformProcessor, IResampling in operation); } - private static PixelConversionModifiers GetModifiers(bool compand, bool premultiplyAlpha) - { - if (premultiplyAlpha) - { - return PixelConversionModifiers.Premultiply.ApplyCompanding(compand); - } - - return PixelConversionModifiers.None.ApplyCompanding(compand); - } - private static void ApplyResizeFrameTransform( Configuration configuration, ImageFrame source, @@ -211,12 +201,9 @@ internal class ResizeProcessor : TransformProcessor, IResampling bool compand, bool premultiplyAlpha) { - PixelAlphaRepresentation? alphaRepresentation = PixelOperations.Instance.GetPixelTypeInfo().AlphaRepresentation; - - // Premultiply only if alpha representation is unknown or Unassociated: - bool needsPremultiplication = alphaRepresentation == null || alphaRepresentation.Value == PixelAlphaRepresentation.Unassociated; - premultiplyAlpha &= needsPremultiplication; - PixelConversionModifiers conversionModifiers = GetModifiers(compand, premultiplyAlpha); + // Resampling uses normalized vectors and interpolates them in the alpha representation selected by the processor options. + PixelConversionModifiers alphaModifier = premultiplyAlpha ? PixelConversionModifiers.Premultiply : PixelConversionModifiers.UnPremultiply; + PixelConversionModifiers conversionModifiers = (PixelConversionModifiers.Scale | alphaModifier).ApplyCompanding(compand); Buffer2DRegion sourceRegion = source.PixelBuffer.GetRegion(sourceRectangle); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 0a4d38655..599882fdb 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -59,7 +59,7 @@ internal sealed class ResizeWorker : IDisposable { this.configuration = configuration; this.source = source; - this.sourceRectangle = source.Rectangle; + this.sourceRectangle = source.Bounds; this.conversionModifiers = conversionModifiers; this.horizontalKernelMap = horizontalKernelMap; this.verticalKernelMap = verticalKernelMap; @@ -169,7 +169,6 @@ internal sealed class ResizeWorker : IDisposable } Span targetRowSpan = destination.DangerousGetRowSpan(y).Slice(left, width); - PixelOperations.Instance.FromVector4Destructive(this.configuration, tempColSpan, targetRowSpan, this.conversionModifiers); } } @@ -215,12 +214,7 @@ internal sealed class ResizeWorker : IDisposable for (int y = calculationInterval.Min; y < calculationInterval.Max; y++) { Span sourceRow = this.source.DangerousGetRowSpan(y); - - PixelOperations.Instance.ToVector4( - this.configuration, - sourceRow, - tempRowSpan, - this.conversionModifiers); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, tempRowSpan, this.conversionModifiers); ref Vector4 firstPassBaseRef = ref transposedFirstPassBufferSpan[y - currentWindowMin]; diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs index 17bdeadde..b1d7b61ee 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs @@ -19,7 +19,7 @@ internal static class TransformUtilities /// /// The transform matrix. public static bool IsDegenerate(Matrix3x2 matrix) - => IsNaN(matrix) || IsZero(matrix.GetDeterminant()); + => IsNaN(matrix) || matrix.GetDeterminant() == 0F; /// /// Returns a value that indicates whether the specified matrix is degenerate @@ -28,11 +28,7 @@ internal static class TransformUtilities /// /// The transform matrix. public static bool IsDegenerate(Matrix4x4 matrix) - => IsNaN(matrix) || IsZero(matrix.GetDeterminant()); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsZero(float a) - => a > -Constants.EpsilonSquared && a < Constants.EpsilonSquared; + => IsNaN(matrix) || matrix.GetDeterminant() == 0F; /// /// Returns a value that indicates whether the specified matrix contains any values @@ -69,11 +65,17 @@ internal static class TransformUtilities [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) { - // The w component (v4.W) resulting from the transformation can be less than 0 in certain cases, - // such as when the point is transformed behind the camera in a perspective projection. - // However, in many 2D contexts, negative w values are not meaningful and could cause issues - // like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure - // we don't divide by a very small or negative number, effectively treating any negative w as epsilon. + // Transforms the 2D point (x, y) as the homogeneous coordinate (x, y, 0, 1) and + // performs the perspective divide (X/W, Y/W) to project back into Cartesian 2D space. + // + // For affine matrices (M14=0, M24=0, M34=0, M44=1) W is always 1 and the divide + // is a no-op, producing the same result as Vector2.Transform(v, Matrix4x4).AsVector2() + // (the approach used by .NET 10+). + // + // For projective matrices (taper, quad distortion) W varies per point and the divide + // is essential for correct perspective mapping. W <= 0 means the point has crossed the + // vanishing line of the projection; clamping to epsilon avoids division by zero or + // negative values that would flip/mirror the output. const float epsilon = 0.0000001F; Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix); return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon); diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index e40a307a3..74da44040 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -397,6 +397,16 @@ public class ProjectiveTransformBuilder internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix) => TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size); + /// + /// Clears all accumulated transform matrices, resetting the builder to its initial state. + /// + /// The . + public ProjectiveTransformBuilder Clear() + { + this.transformMatrixFactories.Clear(); + return this; + } + private static void CheckDegenerate(Matrix4x4 matrix) { if (TransformUtilities.IsDegenerate(matrix)) diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 8c88ff647..71f2bc333 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -19,12 +19,7 @@ - - - + diff --git a/tests/ImageSharp.Benchmarks/Bulk/FromVector4.cs b/tests/ImageSharp.Benchmarks/Bulk/FromVector4.cs index 80b096344..dcd26c328 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/FromVector4.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/FromVector4.cs @@ -14,7 +14,6 @@ using SixLabors.ImageSharp.PixelFormats; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Benchmarks.Bulk; -[Config(typeof(Config.Short))] public abstract class FromVector4 where TPixel : unmanaged, IPixel { @@ -62,14 +61,21 @@ public abstract class FromVector4 => PixelOperations.Instance.FromVector4Destructive(this.Configuration, this.Source.GetSpan(), this.Destination.GetSpan()); } +[Config(typeof(Config.Short))] public class FromVector4Rgba32 : FromVector4 { + /// + /// Measures the raw SIMD kernel that converts normalized RGBA vector components to bytes. + /// [Benchmark] public void UseHwIntrinsics() { Span sBytes = MemoryMarshal.Cast(this.Source.GetSpan()); Span dFloats = MemoryMarshal.Cast(this.Destination.GetSpan()); + // Four components per pixel make every configured count divisible by the supported 128-, 256-, and 512-bit + // byte-vector widths. Calling the block kernel directly therefore excludes PixelOperations contract handling + // and the general conversion wrapper's remainder dispatch, establishing the lower bound for the SIMD conversion. SimdUtils.HwIntrinsics.NormalizedFloatToByteSaturate(sBytes, dFloats); } @@ -121,36 +127,45 @@ public class FromVector4Rgba32 : FromVector4 } /* - BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.3085/23H2/2023Update/SunValley3) - 11th Gen Intel Core i7-11370H 3.30GHz, 1 CPU, 8 logical and 4 physical cores - .NET SDK 8.0.200-preview.23624.5 - [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 - Job-YJYLLR : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 - - Runtime=.NET 8.0 Arguments=/p:DebugType=portable IterationCount=3 - LaunchCount=1 WarmupCount=3 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |---------------------------- |------ |------------:|-------------:|-----------:|------:|--------:|----------:|------------:| - | PixelOperations_Base | 64 | 114.80 ns | 16.459 ns | 0.902 ns | 1.00 | 0.00 | - | NA | - | PixelOperations_Specialized | 64 | 28.91 ns | 80.482 ns | 4.411 ns | 0.25 | 0.04 | - | NA | - | FallbackIntrinsics128 | 64 | 133.60 ns | 23.750 ns | 1.302 ns | 1.16 | 0.02 | - | NA | - | ExtendedIntrinsic | 64 | 40.11 ns | 10.183 ns | 0.558 ns | 0.35 | 0.01 | - | NA | - | UseHwIntrinsics | 64 | 14.71 ns | 4.860 ns | 0.266 ns | 0.13 | 0.00 | - | NA | - | UseAvx2_Grouped | 64 | 20.23 ns | 11.619 ns | 0.637 ns | 0.18 | 0.00 | - | NA | - | | | | | | | | | | - | PixelOperations_Base | 256 | 387.94 ns | 31.591 ns | 1.732 ns | 1.00 | 0.00 | - | NA | - | PixelOperations_Specialized | 256 | 50.93 ns | 22.388 ns | 1.227 ns | 0.13 | 0.00 | - | NA | - | FallbackIntrinsics128 | 256 | 509.72 ns | 249.926 ns | 13.699 ns | 1.31 | 0.04 | - | NA | - | ExtendedIntrinsic | 256 | 140.32 ns | 9.353 ns | 0.513 ns | 0.36 | 0.00 | - | NA | - | UseHwIntrinsics | 256 | 41.99 ns | 16.000 ns | 0.877 ns | 0.11 | 0.00 | - | NA | - | UseAvx2_Grouped | 256 | 63.81 ns | 2.360 ns | 0.129 ns | 0.16 | 0.00 | - | NA | - | | | | | | | | | | - | PixelOperations_Base | 2048 | 2,979.49 ns | 2,023.706 ns | 110.926 ns | 1.00 | 0.00 | - | NA | - | PixelOperations_Specialized | 2048 | 326.19 ns | 19.077 ns | 1.046 ns | 0.11 | 0.00 | - | NA | - | FallbackIntrinsics128 | 2048 | 3,885.95 ns | 411.078 ns | 22.533 ns | 1.31 | 0.05 | - | NA | - | ExtendedIntrinsic | 2048 | 1,078.58 ns | 136.960 ns | 7.507 ns | 0.36 | 0.01 | - | NA | - | UseHwIntrinsics | 2048 | 312.07 ns | 68.662 ns | 3.764 ns | 0.10 | 0.00 | - | NA | - | UseAvx2_Grouped | 2048 | 451.83 ns | 41.742 ns | 2.288 ns | 0.15 | 0.01 | - | NA | + BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8737/25H2/2025Update/HudsonValley2) + AMD RYZEN AI MAX+ 395 w/ Radeon 8060S 3.00GHz, 1 CPU, 32 logical and 16 physical cores + .NET 8.0.28, X64 RyuJIT x86-64-v4 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | + |---------------------------- |------ |------------:|-----------:|----------:|------:|--------:|----------:| + | PixelOperations_Base | 64 | 60.19 ns | 32.376 ns | 1.775 ns | 1.00 | 0.04 | - | + | PixelOperations_Specialized | 64 | 22.94 ns | 66.082 ns | 3.622 ns | 0.38 | 0.05 | - | + | UseHwIntrinsics | 64 | 10.64 ns | 9.535 ns | 0.523 ns | 0.18 | 0.01 | - | + | UseAvx2_Grouped | 64 | 11.69 ns | 0.997 ns | 0.055 ns | 0.19 | 0.01 | - | + | PixelOperations_Base | 256 | 232.65 ns | 37.948 ns | 2.080 ns | 1.00 | 0.01 | - | + | PixelOperations_Specialized | 256 | 33.75 ns | 3.906 ns | 0.214 ns | 0.15 | 0.00 | - | + | UseHwIntrinsics | 256 | 21.91 ns | 2.732 ns | 0.150 ns | 0.09 | 0.00 | - | + | UseAvx2_Grouped | 256 | 24.84 ns | 5.333 ns | 0.292 ns | 0.11 | 0.00 | - | + | PixelOperations_Base | 2048 | 1,500.98 ns | 951.510 ns | 52.155 ns | 1.00 | 0.04 | - | + | PixelOperations_Specialized | 2048 | 156.21 ns | 65.074 ns | 3.567 ns | 0.10 | 0.00 | - | + | UseHwIntrinsics | 2048 | 144.38 ns | 40.947 ns | 2.244 ns | 0.10 | 0.00 | - | + | UseAvx2_Grouped | 2048 | 189.80 ns | 54.098 ns | 2.965 ns | 0.13 | 0.00 | - | + */ +} + +/// +/// Measures bulk conversion from values to premultiplied pixels. +/// +[Config(typeof(Config.Analysis))] +public class FromVector4Bgra32P : FromVector4 +{ + /* + BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8737/25H2/2025Update/HudsonValley2) + AMD RYZEN AI MAX+ 395 w/ Radeon 8060S 3.00GHz, 1 CPU, 32 logical and 16 physical cores + .NET 8.0.28, X64 RyuJIT x86-64-v4 + + | Method | Count | Mean | Error | StdDev | Ratio | Code Size | Allocated | + |---------------------------- |------ |------------:|---------:|----------:|------:|----------:|----------:| + | PixelOperations_Base | 64 | 53.35 ns | 0.208 ns | 0.311 ns | 1.00 | 1,152 B | - | + | PixelOperations_Specialized | 64 | 46.61 ns | 0.195 ns | 0.280 ns | 0.87 | 2,975 B | - | + | PixelOperations_Base | 256 | 212.11 ns | 0.719 ns | 1.076 ns | 1.00 | 1,152 B | - | + | PixelOperations_Specialized | 256 | 77.12 ns | 0.824 ns | 1.233 ns | 0.36 | 2,992 B | - | + | PixelOperations_Base | 2048 | 1,435.87 ns | 9.402 ns | 13.781 ns | 1.00 | 1,152 B | - | + | PixelOperations_Specialized | 2048 | 250.89 ns | 9.354 ns | 13.416 ns | 0.17 | 2,992 B | - | */ } diff --git a/tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs index 8728dd671..16529306a 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs @@ -8,7 +8,6 @@ namespace SixLabors.ImageSharp.Benchmarks.Bulk; [Config(typeof(Config.HwIntrinsics_SSE_AVX))] public class Pad3Shuffle4Channel { - private static readonly DefaultPad3Shuffle4 Control = new(SimdUtils.Shuffle.MMShuffle1032); private byte[] source; private byte[] destination; @@ -25,11 +24,11 @@ public class Pad3Shuffle4Channel [Benchmark] public void Pad3Shuffle4() - => SimdUtils.Pad3Shuffle4(this.source, this.destination, Control); + => SimdUtils.Pad3Shuffle4(this.source, this.destination); [Benchmark] public void Pad3Shuffle4FastFallback() - => SimdUtils.Pad3Shuffle4(this.source, this.destination, default(XYZWPad3Shuffle4)); + => SimdUtils.Pad3Shuffle4(this.source, this.destination); } // 2020-10-30 diff --git a/tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.cs index e4c12900f..e09c4ce6c 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.cs @@ -8,7 +8,6 @@ namespace SixLabors.ImageSharp.Benchmarks.Bulk; [Config(typeof(Config.HwIntrinsics_SSE_AVX))] public class Shuffle3Channel { - private static readonly DefaultShuffle3 Control = new(SimdUtils.Shuffle.MMShuffle3102); private byte[] source; private byte[] destination; @@ -25,7 +24,7 @@ public class Shuffle3Channel [Benchmark] public void Shuffle3() - => SimdUtils.Shuffle3(this.source, this.destination, Control); + => SimdUtils.Shuffle3(this.source, this.destination); } // 2020-11-02 diff --git a/tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.cs index 579e2c54d..4b694768d 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.cs @@ -8,7 +8,6 @@ namespace SixLabors.ImageSharp.Benchmarks.Bulk; [Config(typeof(Config.HwIntrinsics_SSE_AVX))] public class Shuffle4Slice3Channel { - private static readonly DefaultShuffle4Slice3 Control = new(SimdUtils.Shuffle.MMShuffle1032); private byte[] source; private byte[] destination; @@ -25,11 +24,11 @@ public class Shuffle4Slice3Channel [Benchmark] public void Shuffle4Slice3() - => SimdUtils.Shuffle4Slice3(this.source, this.destination, Control); + => SimdUtils.Shuffle4Slice3(this.source, this.destination); [Benchmark] public void Shuffle4Slice3FastFallback() - => SimdUtils.Shuffle4Slice3(this.source, this.destination, default(XYZWShuffle4Slice3)); + => SimdUtils.Shuffle4Slice3(this.source, this.destination); } // 2020-10-29 diff --git a/tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.cs b/tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.cs index 6a16bb571..f3ad974c4 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.cs @@ -24,7 +24,7 @@ public class ShuffleByte4Channel [Benchmark] public void Shuffle4Channel() - => SimdUtils.Shuffle4(this.source, this.destination, default); + => SimdUtils.Shuffle4(this.source, this.destination); } // 2020-10-29 diff --git a/tests/ImageSharp.Benchmarks/Bulk/ToVector4_Bgra32.cs b/tests/ImageSharp.Benchmarks/Bulk/ToVector4_Bgra32.cs index 6499632b6..38a09483b 100644 --- a/tests/ImageSharp.Benchmarks/Bulk/ToVector4_Bgra32.cs +++ b/tests/ImageSharp.Benchmarks/Bulk/ToVector4_Bgra32.cs @@ -20,24 +20,48 @@ public class ToVector4_Bgra32 : ToVector4 this.Destination.GetSpan()); } - // RESULTS: - // Method | Runtime | Count | Mean | Error | StdDev | Scaled | ScaledSD | Gen 0 | Allocated | - // ---------------------------- |-------- |------ |-----------:|------------:|-----------:|-------:|---------:|-------:|----------:| - // PixelOperations_Base | Clr | 64 | 339.9 ns | 138.30 ns | 7.8144 ns | 1.00 | 0.00 | 0.0072 | 24 B | - // PixelOperations_Specialized | Clr | 64 | 338.1 ns | 13.30 ns | 0.7515 ns | 0.99 | 0.02 | - | 0 B | - // | | | | | | | | | | - // PixelOperations_Base | Core | 64 | 245.6 ns | 29.05 ns | 1.6413 ns | 1.00 | 0.00 | 0.0072 | 24 B | - // PixelOperations_Specialized | Core | 64 | 257.1 ns | 37.89 ns | 2.1407 ns | 1.05 | 0.01 | - | 0 B | - // | | | | | | | | | | - // PixelOperations_Base | Clr | 256 | 972.7 ns | 61.98 ns | 3.5020 ns | 1.00 | 0.00 | 0.0057 | 24 B | - // PixelOperations_Specialized | Clr | 256 | 882.9 ns | 126.21 ns | 7.1312 ns | 0.91 | 0.01 | - | 0 B | - // | | | | | | | | | | - // PixelOperations_Base | Core | 256 | 910.0 ns | 90.87 ns | 5.1346 ns | 1.00 | 0.00 | 0.0067 | 24 B | - // PixelOperations_Specialized | Core | 256 | 448.4 ns | 15.77 ns | 0.8910 ns | 0.49 | 0.00 | - | 0 B | - // | | | | | | | | | | - // PixelOperations_Base | Clr | 2048 | 6,951.8 ns | 1,299.01 ns | 73.3963 ns | 1.00 | 0.00 | - | 24 B | - // PixelOperations_Specialized | Clr | 2048 | 5,852.3 ns | 630.56 ns | 35.6279 ns | 0.84 | 0.01 | - | 0 B | - // | | | | | | | | | | - // PixelOperations_Base | Core | 2048 | 6,937.5 ns | 1,692.19 ns | 95.6121 ns | 1.00 | 0.00 | - | 24 B | - // PixelOperations_Specialized | Core | 2048 | 2,994.5 ns | 1,126.65 ns | 63.6578 ns | 0.43 | 0.01 | - | 0 B | + // BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8737/25H2/2025Update/HudsonValley2) + // AMD RYZEN AI MAX+ 395 w/ Radeon 8060S 3.00GHz, 1 CPU, 32 logical and 16 physical cores + // .NET 8.0.28, X64 RyuJIT x86-64-v4 + // + // | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | + // |---------------------------- |------ |------------:|------------:|----------:|------:|--------:|----------:| + // | PixelOperations_Base | 64 | 58.30 ns | 20.13 ns | 1.103 ns | 1.00 | 0.02 | - | + // | PixelOperations_Specialized | 64 | 58.95 ns | 23.68 ns | 1.298 ns | 1.01 | 0.03 | - | + // | PixelOperations_Base | 256 | 226.95 ns | 84.03 ns | 4.606 ns | 1.00 | 0.02 | - | + // | PixelOperations_Specialized | 256 | 229.70 ns | 40.00 ns | 2.192 ns | 1.01 | 0.02 | - | + // | PixelOperations_Base | 2048 | 1,795.42 ns | 1,465.95 ns | 80.354 ns | 1.00 | 0.05 | - | + // | PixelOperations_Specialized | 2048 | 291.89 ns | 109.98 ns | 6.028 ns | 0.16 | 0.01 | - | +} + +/// +/// Measures bulk conversion from premultiplied pixels to values. +/// +[Config(typeof(Config.Analysis))] +public class ToVector4_Bgra32P : ToVector4 +{ + /// + /// Measures the scalar implementation used as the comparison baseline. + /// + [Benchmark(Baseline = true)] + public void PixelOperations_Base() + { + new PixelOperations().ToVector4( + this.Configuration, + this.Source.GetSpan(), + this.Destination.GetSpan()); + } + + // BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8737/25H2/2025Update/HudsonValley2) + // AMD RYZEN AI MAX+ 395 w/ Radeon 8060S 3.00GHz, 1 CPU, 32 logical and 16 physical cores + // .NET 8.0.28, X64 RyuJIT x86-64-v4 + // + // | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Code Size | Allocated | + // |---------------------------- |------ |------------:|---------:|---------:|------:|--------:|----------:|----------:| + // | PixelOperations_Base | 64 | 58.05 ns | 3.204 ns | 4.795 ns | 1.01 | 0.11 | 932 B | - | + // | PixelOperations_Specialized | 64 | 84.78 ns | 0.467 ns | 0.669 ns | 1.47 | 0.12 | 3,731 B | - | + // | PixelOperations_Base | 256 | 208.93 ns | 0.345 ns | 0.484 ns | 1.00 | 0.00 | 958 B | - | + // | PixelOperations_Specialized | 256 | 106.50 ns | 0.280 ns | 0.410 ns | 0.51 | 0.00 | 3,738 B | - | + // | PixelOperations_Base | 2048 | 1,617.39 ns | 3.380 ns | 4.848 ns | 1.00 | 0.00 | 958 B | - | + // | PixelOperations_Specialized | 2048 | 269.90 ns | 0.666 ns | 0.976 ns | 0.17 | 0.00 | 3,735 B | - | } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs new file mode 100644 index 000000000..45021cadc --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; + +using ImageMagick; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs; + +[MarkdownExporter] +[HtmlExporter] +[Config(typeof(Config.Short))] +public class DecodeExr +{ + private Configuration configuration; + + private byte[] imageBytes; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Exr.Benchmark)] + public string TestImage { get; set; } + + [GlobalSetup] + public void ReadImages() + { + this.configuration = Configuration.CreateDefaultInstance(); + new ExrConfigurationModule().Configure(this.configuration); + + this.imageBytes ??= File.ReadAllBytes(this.TestImageFullPath); + } + + [Benchmark(Description = "Magick Exr")] + public uint ExrImageMagick() + { + MagickReadSettings settings = new() { Format = MagickFormat.Exr }; + using MemoryStream memoryStream = new(this.imageBytes); + using MagickImage image = new(memoryStream, settings); + return image.Width; + } + + [Benchmark(Description = "ImageSharp Exr")] + public int ExrImageSharp() + { + using MemoryStream memoryStream = new(this.imageBytes); + using Image image = Image.Load(memoryStream); + return image.Height; + } + + /* Results 27.03.2026 + BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8037/25H2/2025Update/HudsonValley2) + Intel Core i7-14700T 1.30GHz, 1 CPU, 28 logical and 20 physical cores + .NET SDK 10.0.201 + [Host] : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3 + Job-VDWIGO : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3 + + Runtime=.NET 8.0 Arguments=/p:DebugType=portable IterationCount=3 + LaunchCount=1 WarmupCount=3 + + | Method | TestImage | Mean | Error | StdDev | Allocated | + |----------------- |----------------------------- |---------:|---------:|---------:|----------:| + | 'Magick Exr' | Exr/Calliphora_benchmark.exr | 20.37 ms | 0.790 ms | 0.043 ms | 12.98 KB | + | 'ImageSharp Exr' | Exr/Calliphora_benchmark.exr | 45.68 ms | 4.999 ms | 0.274 ms | 34.09 KB | + */ +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs index a1ba3fb8d..ae4229807 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs @@ -9,40 +9,24 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; [Config(typeof(Config.Short))] public class CmykColorConversion : ColorConversionBenchmark { + private readonly JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Cmyk, 8); + + /// + /// Initializes a new instance of the class. + /// public CmykColorConversion() : base(4) { } - [Benchmark(Baseline = true)] - public void Scalar() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.CmykScalar(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector128() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.CmykVector128(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector256() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.CmykVector256(8).ConvertToRgbInPlace(values); - } - + /// + /// Converts one CMYK component row through the adaptive operator traversal. + /// [Benchmark] - public void SimdVector512() + public void ConvertToRgb() { JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - new JpegColorConverterBase.CmykVector512(8).ConvertToRgbInPlace(values); + this.converter.ConvertToRgbInPlace(values); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs index 436aa9bcd..106bb5ff6 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.cs @@ -51,6 +51,7 @@ public abstract class ColorConversionBenchmark // no need to dispose when buffer is not array owner buffers[i] = Configuration.Default.MemoryAllocator.Allocate2D(values.Length, 1); + values.CopyTo(buffers[i].DangerousGetRowSpan(0)); } return buffers; diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/GrayscaleColorConversion.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/GrayscaleColorConversion.cs index 3ade4279f..9e5bfed12 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/GrayscaleColorConversion.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/GrayscaleColorConversion.cs @@ -9,40 +9,24 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; [Config(typeof(Config.Short))] public class GrayScaleColorConversion : ColorConversionBenchmark { + private readonly JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, 8); + + /// + /// Initializes a new instance of the class. + /// public GrayScaleColorConversion() : base(1) { } - [Benchmark(Baseline = true)] - public void Scalar() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.GrayScaleScalar(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector128() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.GrayScaleVector128(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector256() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.GrayScaleVector256(8).ConvertToRgbInPlace(values); - } - + /// + /// Converts one grayscale component row through the adaptive operator traversal. + /// [Benchmark] - public void SimdVector512() + public void ConvertToRgb() { JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - new JpegColorConverterBase.GrayScaleVector512(8).ConvertToRgbInPlace(values); + this.converter.ConvertToRgbInPlace(values); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorPacking.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorPacking.cs new file mode 100644 index 000000000..5c36ae59f --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorPacking.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using SixLabors.ImageSharp.Formats.Jpeg.Components; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; + +/// +/// Compares the previous scalar JPEG packing loops with the SIMD register-transpose implementation. +/// +[Config(typeof(Config.Short))] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class JpegColorPacking +{ + private const float MaximumValue = 255F; + private const float Scale = 1F / MaximumValue; + + private float[] x = null!; + private float[] y = null!; + private float[] z = null!; + private float[] w = null!; + private Vector3[] packed3 = null!; + private float[] destination3 = null!; + private float[] destination4 = null!; + + /// + /// Gets or sets the number of pixels transformed by each benchmark invocation. + /// + [Params(128, 1024, 4096)] + public int Length { get; set; } + + /// + /// Creates deterministic source and destination buffers outside the measured operations. + /// + [GlobalSetup] + public void Setup() + { + this.x = CreateSamples(this.Length, 1); + this.y = CreateSamples(this.Length, 2); + this.z = CreateSamples(this.Length, 3); + this.w = CreateSamples(this.Length, 4); + this.packed3 = new Vector3[this.Length]; + this.destination3 = new float[this.Length * 3]; + this.destination4 = new float[this.Length * 4]; + + for (int i = 0; i < this.packed3.Length; i++) + { + this.packed3[i] = new Vector3(this.x[i], this.y[i], this.z[i]); + } + } + + /// + /// Measures the previous scalar three-plane normalization and interleave loop. + /// + /// The last destination value, keeping the writes observable. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Pack3")] + public float PackedNormalizeInterleave3Scalar() + { + JpegColorPackingScalar.PackedNormalizeInterleave3(this.x, this.y, this.z, this.destination3, Scale); + + return this.destination3[^1]; + } + + /// + /// Measures the SIMD three-plane normalization and interleave implementation. + /// + /// The last destination value, keeping the writes observable. + [Benchmark] + [BenchmarkCategory("Pack3")] + public float PackedNormalizeInterleave3Simd() + { + JpegColorConverterBase.PackedNormalizeInterleave3(this.x, this.y, this.z, this.destination3, Scale); + + return this.destination3[^1]; + } + + /// + /// Measures the previous scalar packed-three-channel deinterleave loop. + /// + /// A checksum containing the last value written to every destination plane. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Unpack3")] + public float UnpackDeinterleave3Scalar() + { + JpegColorPackingScalar.UnpackDeinterleave3(this.packed3, this.x, this.y, this.z); + + return this.x[^1] + this.y[^1] + this.z[^1]; + } + + /// + /// Measures the SIMD packed-three-channel deinterleave implementation. + /// + /// A checksum containing the last value written to every destination plane. + [Benchmark] + [BenchmarkCategory("Unpack3")] + public float UnpackDeinterleave3Simd() + { + JpegColorConverterBase.UnpackDeinterleave3(this.packed3, this.x, this.y, this.z); + + return this.x[^1] + this.y[^1] + this.z[^1]; + } + + /// + /// Measures the previous scalar four-plane normalization and interleave loop. + /// + /// The last destination value, keeping the writes observable. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Pack4")] + public float PackedNormalizeInterleave4Scalar() + { + JpegColorPackingScalar.PackedNormalizeInterleave4(this.x, this.y, this.z, this.w, this.destination4, MaximumValue); + + return this.destination4[^1]; + } + + /// + /// Measures the SIMD four-plane normalization and interleave implementation. + /// + /// The last destination value, keeping the writes observable. + [Benchmark] + [BenchmarkCategory("Pack4")] + public float PackedNormalizeInterleave4Simd() + { + JpegColorConverterBase.PackedNormalizeInterleave4(this.x, this.y, this.z, this.w, this.destination4, MaximumValue); + + return this.destination4[^1]; + } + + /// + /// Measures the previous scalar inverted four-plane normalization and interleave loop. + /// + /// The last destination value, keeping the writes observable. + [Benchmark(Baseline = true)] + [BenchmarkCategory("InvertPack4")] + public float PackedInvertNormalizeInterleave4Scalar() + { + JpegColorPackingScalar.PackedInvertNormalizeInterleave4(this.x, this.y, this.z, this.w, this.destination4, MaximumValue); + + return this.destination4[^1]; + } + + /// + /// Measures the SIMD inverted four-plane normalization and interleave implementation. + /// + /// The last destination value, keeping the writes observable. + [Benchmark] + [BenchmarkCategory("InvertPack4")] + public float PackedInvertNormalizeInterleave4Simd() + { + JpegColorConverterBase.PackedInvertNormalizeInterleave4(this.x, this.y, this.z, this.w, this.destination4, MaximumValue); + + return this.destination4[^1]; + } + + /// + /// Creates deterministic, non-integral samples for one component plane. + /// + /// The number of samples to create. + /// The one-based component number used to distinguish the plane. + /// The generated samples. + private static float[] CreateSamples(int length, int component) + { + float[] samples = new float[length]; + + for (int i = 0; i < samples.Length; i++) + { + samples[i] = (((i * 37) + (component * 53)) % 251) + (component * 0.125F); + } + + return samples; + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorPackingScalar.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorPackingScalar.cs new file mode 100644 index 000000000..511abc0db --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorPackingScalar.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; + +/// +/// Preserves the scalar JPEG packing loops that preceded the SIMD investigation. +/// +internal static class JpegColorPackingScalar +{ + /// + /// Normalizes and interleaves three planar component lanes using the previous scalar implementation. + /// + /// The planar X components. + /// The planar Y components. + /// The planar Z components. + /// The destination ordered as consecutive XYZ triples. + /// The normalization factor applied to every component. + public static void PackedNormalizeInterleave3(ReadOnlySpan xLane, ReadOnlySpan yLane, ReadOnlySpan zLane, Span packed, float scale) + { + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + + for (nuint i = 0; i < (nuint)xLane.Length; i++) + { + nuint packedOffset = i * 3; + Unsafe.Add(ref packedRef, packedOffset) = Unsafe.Add(ref xLaneRef, i) * scale; + Unsafe.Add(ref packedRef, packedOffset + 1) = Unsafe.Add(ref yLaneRef, i) * scale; + Unsafe.Add(ref packedRef, packedOffset + 2) = Unsafe.Add(ref zLaneRef, i) * scale; + } + } + + /// + /// Deinterleaves packed XYZ values using the previous scalar implementation. + /// + /// The source ordered as consecutive XYZ triples. + /// The destination X components. + /// The destination Y components. + /// The destination Z components. + public static void UnpackDeinterleave3(ReadOnlySpan packed, Span xLane, Span yLane, Span zLane) + { + ref float packedRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(packed)); + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + + for (nuint i = 0; i < (nuint)packed.Length; i++) + { + nuint packedOffset = i * 3; + Unsafe.Add(ref xLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset); + Unsafe.Add(ref yLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset + 1); + Unsafe.Add(ref zLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset + 2); + } + } + + /// + /// Normalizes and interleaves four planar component lanes using the previous scalar implementation. + /// + /// The planar X components. + /// The planar Y components. + /// The planar Z components. + /// The planar W components. + /// The destination ordered as consecutive XYZW groups. + /// The maximum component value used to normalize each component. + public static void PackedNormalizeInterleave4(ReadOnlySpan xLane, ReadOnlySpan yLane, ReadOnlySpan zLane, ReadOnlySpan wLane, Span packed, float maximumValue) + { + float scale = 1F / maximumValue; + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + + for (nuint i = 0; i < (nuint)xLane.Length; i++) + { + nuint packedOffset = i * 4; + Unsafe.Add(ref packedRef, packedOffset) = Unsafe.Add(ref xLaneRef, i) * scale; + Unsafe.Add(ref packedRef, packedOffset + 1) = Unsafe.Add(ref yLaneRef, i) * scale; + Unsafe.Add(ref packedRef, packedOffset + 2) = Unsafe.Add(ref zLaneRef, i) * scale; + Unsafe.Add(ref packedRef, packedOffset + 3) = Unsafe.Add(ref wLaneRef, i) * scale; + } + } + + /// + /// Inverts, normalizes, and interleaves four planar lanes using the previous scalar implementation. + /// + /// The inverted planar X components. + /// The inverted planar Y components. + /// The inverted planar Z components. + /// The inverted planar W components. + /// The destination ordered as consecutive conventional XYZW groups. + /// The maximum component value used for inversion and normalization. + public static void PackedInvertNormalizeInterleave4(ReadOnlySpan xLane, ReadOnlySpan yLane, ReadOnlySpan zLane, ReadOnlySpan wLane, Span packed, float maximumValue) + { + float scale = 1F / maximumValue; + ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); + ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); + ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); + ref float wLaneRef = ref MemoryMarshal.GetReference(wLane); + ref float packedRef = ref MemoryMarshal.GetReference(packed); + + for (nuint i = 0; i < (nuint)xLane.Length; i++) + { + nuint packedOffset = i * 4; + Unsafe.Add(ref packedRef, packedOffset) = (maximumValue - Unsafe.Add(ref xLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, packedOffset + 1) = (maximumValue - Unsafe.Add(ref yLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, packedOffset + 2) = (maximumValue - Unsafe.Add(ref zLaneRef, i)) * scale; + Unsafe.Add(ref packedRef, packedOffset + 3) = (maximumValue - Unsafe.Add(ref wLaneRef, i)) * scale; + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/RgbColorConversion.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/RgbColorConversion.cs index 2916dcdce..b1d72e3ca 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/RgbColorConversion.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/RgbColorConversion.cs @@ -9,40 +9,24 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; [Config(typeof(Config.Short))] public class RgbColorConversion : ColorConversionBenchmark { + private readonly JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, 8); + + /// + /// Initializes a new instance of the class. + /// public RgbColorConversion() : base(3) { } - [Benchmark(Baseline = true)] - public void Scalar() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.RgbScalar(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector128() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.RgbVector128(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector256() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.RgbVector256(8).ConvertToRgbInPlace(values); - } - + /// + /// Converts one RGB component row through the adaptive operator traversal. + /// [Benchmark] - public void SimdVector512() + public void ConvertToRgb() { JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - new JpegColorConverterBase.RgbVector512(8).ConvertToRgbInPlace(values); + this.converter.ConvertToRgbInPlace(values); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.cs index fbd762af4..1355aecd1 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.cs @@ -9,40 +9,24 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; [Config(typeof(Config.Short))] public class YCbCrColorConversion : ColorConversionBenchmark { + private readonly JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, 8); + + /// + /// Initializes a new instance of the class. + /// public YCbCrColorConversion() : base(3) { } + /// + /// Converts one YCbCr component row through the adaptive operator traversal. + /// [Benchmark] - public void Scalar() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.YCbCrScalar(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector128() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.YCbCrVector128(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector256() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.YCbCrVector256(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector512() + public void ConvertToRgb() { JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - new JpegColorConverterBase.YCbCrVector512(8).ConvertToRgbInPlace(values); + this.converter.ConvertToRgbInPlace(values); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.cs index e6b04c152..261711d0b 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.cs @@ -9,40 +9,24 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg; [Config(typeof(Config.Short))] public class YccKColorConverter : ColorConversionBenchmark { + private readonly JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Ycck, 8); + + /// + /// Initializes a new instance of the class. + /// public YccKColorConverter() : base(4) { } - [Benchmark(Baseline = true)] - public void Scalar() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.YccKScalar(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector128() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.YccKVector128(8).ConvertToRgbInPlace(values); - } - - [Benchmark] - public void SimdVector256() - { - JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - - new JpegColorConverterBase.YccKVector256(8).ConvertToRgbInPlace(values); - } - + /// + /// Converts one YccK component row through the adaptive operator traversal. + /// [Benchmark] - public void SimdVector512() + public void ConvertToRgb() { JpegColorConverterBase.ComponentValues values = new(this.Input, 0); - new JpegColorConverterBase.YccKVector512(8).ConvertToRgbInPlace(values); + this.converter.ConvertToRgbInPlace(values); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs index 125b42680..69779731b 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs @@ -40,19 +40,19 @@ public class EncodeIndexedPng this.bmpCore.Dispose(); } - [Benchmark(Baseline = true, Description = "ImageSharp Octree Png")] - public void PngCoreOctree() + [Benchmark(Baseline = true, Description = "ImageSharp Hexadecatree Png")] + public void PngCoreHexadecatree() { using MemoryStream memoryStream = new(); - PngEncoder options = new() { Quantizer = KnownQuantizers.Octree }; + PngEncoder options = new() { Quantizer = KnownQuantizers.Hexadecatree }; this.bmpCore.SaveAsPng(memoryStream, options); } - [Benchmark(Description = "ImageSharp Octree NoDither Png")] - public void PngCoreOctreeNoDither() + [Benchmark(Description = "ImageSharp Hexadecatree NoDither Png")] + public void PngCoreHexadecatreeNoDither() { using MemoryStream memoryStream = new(); - PngEncoder options = new() { Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) }; + PngEncoder options = new() { Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }) }; this.bmpCore.SaveAsPng(memoryStream, options); } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncode.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncode.cs new file mode 100644 index 000000000..e5a434067 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncode.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Filters; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Png; + +/// +/// Measures the shared PNG filter map/reduce traversal. +/// +[Config(typeof(Config.Short))] +public class PngFilterEncode +{ + private const int BytesPerPixel = 4; + + private byte[] scanline; + private byte[] previousScanline; + private byte[] result; + + /// + /// Gets or sets the filter evaluated by each invocation. + /// + [Params(PngFilterMethod.Sub, PngFilterMethod.Up, PngFilterMethod.Average, PngFilterMethod.Paeth)] + public PngFilterMethod Filter { get; set; } + + /// + /// Gets or sets the number of scanline bytes. + /// + [Params(64, 1024, 16384)] + public int Count { get; set; } + + /// + /// Creates deterministic non-uniform inputs and a result buffer. + /// + [GlobalSetup] + public void Setup() + { + this.scanline = new byte[this.Count]; + this.previousScanline = new byte[this.Count]; + this.result = new byte[this.Count + 1]; + + Random random = new(12345678); + random.NextBytes(this.scanline); + random.NextBytes(this.previousScanline); + } + + /// + /// Executes the shared operator-driven map/reduce traversal. + /// + /// The filter variance sum. + [Benchmark] + public int Encode() + => this.Filter switch + { + PngFilterMethod.Sub => this.EncodeSub(), + PngFilterMethod.Up => this.EncodeUp(), + PngFilterMethod.Average => this.EncodeAverage(), + PngFilterMethod.Paeth => this.EncodePaeth(), + _ => throw new InvalidOperationException() + }; + + /// + /// Executes the current Sub encoder. + /// + private int EncodeSub() + { + SubFilter.Encode(this.scanline, this.result, BytesPerPixel, out int sum); + return sum; + } + + /// + /// Executes the current Up encoder. + /// + private int EncodeUp() + { + UpFilter.Encode(this.scanline, this.previousScanline, this.result, out int sum); + return sum; + } + + /// + /// Executes the current Average encoder. + /// + private int EncodeAverage() + { + AverageFilter.Encode(this.scanline, this.previousScanline, this.result, BytesPerPixel, out int sum); + return sum; + } + + /// + /// Executes the current Paeth encoder. + /// + private int EncodePaeth() + { + PaethFilter.Encode(this.scanline, this.previousScanline, this.result, BytesPerPixel, out int sum); + return sum; + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs index d6a6cf1fb..4c81aee6d 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs @@ -27,7 +27,7 @@ public class DecodeTga => this.data = File.ReadAllBytes(this.TestImageFullPath); [Benchmark(Baseline = true, Description = "ImageMagick Tga")] - public int TgaImageMagick() + public uint TgaImageMagick() { MagickReadSettings settings = new() { Format = MagickFormat.Tga }; using MagickImage image = new(new MemoryStream(this.data), settings); diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs index bba1bc187..a10f1527f 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs @@ -42,7 +42,7 @@ public class DecodeWebp } [Benchmark(Description = "Magick Lossy Webp")] - public int WebpLossyMagick() + public uint WebpLossyMagick() { MagickReadSettings settings = new() { Format = MagickFormat.WebP }; using MemoryStream memoryStream = new(this.webpLossyBytes); @@ -59,7 +59,7 @@ public class DecodeWebp } [Benchmark(Description = "Magick Lossless Webp")] - public int WebpLosslessMagick() + public uint WebpLosslessMagick() { MagickReadSettings settings = new() { Format = MagickFormat.WebP }; diff --git a/tests/ImageSharp.Benchmarks/Config.cs b/tests/ImageSharp.Benchmarks/Config.cs index 9231fc8b4..0c46de470 100644 --- a/tests/ImageSharp.Benchmarks/Config.cs +++ b/tests/ImageSharp.Benchmarks/Config.cs @@ -8,6 +8,7 @@ using BenchmarkDotNet.Diagnostics.Windows; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Exporters.Json; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Toolchains.InProcess.Emit; @@ -46,6 +47,34 @@ public partial class Config : ManualConfig .WithArguments([new MsBuildArgument("/p:DebugType=portable")])); } + /// + /// Configures a short diagnostic run that exports memory usage, generated code, and full measurement data. + /// Elevated Windows runs also include branch-instruction counters. + /// + public class Analysis : Config + { + public Analysis() + { + this.AddJob(Job.ShortRun.WithRuntime(CoreRuntime.Core80).WithArguments([new MsBuildArgument("/p:DebugType=portable")])); + + this.AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig( + maxDepth: 3, + printSource: true, + exportGithubMarkdown: true, + exportCombinedDisassemblyReport: true))); + + this.AddExporter(JsonExporter.Full); + +#if OS_WINDOWS + if (this.IsElevated) + { + // ETW hardware counters require elevation, so ordinary benchmark runs omit them instead of requesting more access. + this.AddHardwareCounters(HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions); + } +#endif + } + } + public class StandardInProcess : Config { public StandardInProcess() => this.AddJob( diff --git a/tests/ImageSharp.Benchmarks/General/PixelConversion/PackedPixelConversion.cs b/tests/ImageSharp.Benchmarks/General/PixelConversion/PackedPixelConversion.cs new file mode 100644 index 000000000..a7dcb9021 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/PixelConversion/PackedPixelConversion.cs @@ -0,0 +1,222 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.Benchmarks.General.PixelConversion; + +/// +/// Measures every optimized conversion between the six byte-packed RGB and RGBA pixel layouts. +/// +[Config(typeof(Config.Short))] +public class PackedPixelConversion +{ + private byte[] source3; + private byte[] source4; + private byte[] destination3; + private byte[] destination4; + + /// + /// Gets or sets the number of pixels converted by each invocation. + /// + [Params(7, 256, 4096)] + public int Count { get; set; } + + /// + /// Creates deterministic source buffers and correctly sized destination buffers. + /// + [GlobalSetup] + public void Setup() + { + this.source3 = new byte[this.Count * 3]; + this.source4 = new byte[this.Count * 4]; + this.destination3 = new byte[this.Count * 3]; + this.destination4 = new byte[this.Count * 4]; + + // Non-repeating channel values prevent the JIT or hardware from benefiting from + // zero-filled inputs while keeping both benchmark revisions byte-for-byte identical. + new Random(42).NextBytes(this.source3); + new Random(42).NextBytes(this.source4); + } + + /// + /// Converts RGBA pixels to ARGB pixels. + /// + [Benchmark] + public void Rgba32ToArgb32() => PixelConverter.FromRgba32.ToArgb32(this.source4, this.destination4); + + /// + /// Converts RGBA pixels to ABGR pixels. + /// + [Benchmark] + public void Rgba32ToAbgr32() => PixelConverter.FromRgba32.ToAbgr32(this.source4, this.destination4); + + /// + /// Converts RGBA pixels to BGRA pixels. + /// + [Benchmark] + public void Rgba32ToBgra32() => PixelConverter.FromRgba32.ToBgra32(this.source4, this.destination4); + + /// + /// Converts RGBA pixels to RGB pixels. + /// + [Benchmark] + public void Rgba32ToRgb24() => PixelConverter.FromRgba32.ToRgb24(this.source4, this.destination3); + + /// + /// Converts RGBA pixels to BGR pixels. + /// + [Benchmark] + public void Rgba32ToBgr24() => PixelConverter.FromRgba32.ToBgr24(this.source4, this.destination3); + + /// + /// Converts ARGB pixels to RGBA pixels. + /// + [Benchmark] + public void Argb32ToRgba32() => PixelConverter.FromArgb32.ToRgba32(this.source4, this.destination4); + + /// + /// Converts ARGB pixels to ABGR pixels. + /// + [Benchmark] + public void Argb32ToAbgr32() => PixelConverter.FromArgb32.ToAbgr32(this.source4, this.destination4); + + /// + /// Converts ARGB pixels to BGRA pixels. + /// + [Benchmark] + public void Argb32ToBgra32() => PixelConverter.FromArgb32.ToBgra32(this.source4, this.destination4); + + /// + /// Converts ARGB pixels to RGB pixels. + /// + [Benchmark] + public void Argb32ToRgb24() => PixelConverter.FromArgb32.ToRgb24(this.source4, this.destination3); + + /// + /// Converts ARGB pixels to BGR pixels. + /// + [Benchmark] + public void Argb32ToBgr24() => PixelConverter.FromArgb32.ToBgr24(this.source4, this.destination3); + + /// + /// Converts ABGR pixels to RGBA pixels. + /// + [Benchmark] + public void Abgr32ToRgba32() => PixelConverter.FromAbgr32.ToRgba32(this.source4, this.destination4); + + /// + /// Converts ABGR pixels to ARGB pixels. + /// + [Benchmark] + public void Abgr32ToArgb32() => PixelConverter.FromAbgr32.ToArgb32(this.source4, this.destination4); + + /// + /// Converts ABGR pixels to BGRA pixels. + /// + [Benchmark] + public void Abgr32ToBgra32() => PixelConverter.FromAbgr32.ToBgra32(this.source4, this.destination4); + + /// + /// Converts ABGR pixels to RGB pixels. + /// + [Benchmark] + public void Abgr32ToRgb24() => PixelConverter.FromAbgr32.ToRgb24(this.source4, this.destination3); + + /// + /// Converts ABGR pixels to BGR pixels. + /// + [Benchmark] + public void Abgr32ToBgr24() => PixelConverter.FromAbgr32.ToBgr24(this.source4, this.destination3); + + /// + /// Converts BGRA pixels to RGBA pixels. + /// + [Benchmark] + public void Bgra32ToRgba32() => PixelConverter.FromBgra32.ToRgba32(this.source4, this.destination4); + + /// + /// Converts BGRA pixels to ARGB pixels. + /// + [Benchmark] + public void Bgra32ToArgb32() => PixelConverter.FromBgra32.ToArgb32(this.source4, this.destination4); + + /// + /// Converts BGRA pixels to ABGR pixels. + /// + [Benchmark] + public void Bgra32ToAbgr32() => PixelConverter.FromBgra32.ToAbgr32(this.source4, this.destination4); + + /// + /// Converts BGRA pixels to RGB pixels. + /// + [Benchmark] + public void Bgra32ToRgb24() => PixelConverter.FromBgra32.ToRgb24(this.source4, this.destination3); + + /// + /// Converts BGRA pixels to BGR pixels. + /// + [Benchmark] + public void Bgra32ToBgr24() => PixelConverter.FromBgra32.ToBgr24(this.source4, this.destination3); + + /// + /// Converts RGB pixels to RGBA pixels. + /// + [Benchmark] + public void Rgb24ToRgba32() => PixelConverter.FromRgb24.ToRgba32(this.source3, this.destination4); + + /// + /// Converts RGB pixels to ARGB pixels. + /// + [Benchmark] + public void Rgb24ToArgb32() => PixelConverter.FromRgb24.ToArgb32(this.source3, this.destination4); + + /// + /// Converts RGB pixels to ABGR pixels. + /// + [Benchmark] + public void Rgb24ToAbgr32() => PixelConverter.FromRgb24.ToAbgr32(this.source3, this.destination4); + + /// + /// Converts RGB pixels to BGRA pixels. + /// + [Benchmark] + public void Rgb24ToBgra32() => PixelConverter.FromRgb24.ToBgra32(this.source3, this.destination4); + + /// + /// Converts RGB pixels to BGR pixels. + /// + [Benchmark] + public void Rgb24ToBgr24() => PixelConverter.FromRgb24.ToBgr24(this.source3, this.destination3); + + /// + /// Converts BGR pixels to RGBA pixels. + /// + [Benchmark] + public void Bgr24ToRgba32() => PixelConverter.FromBgr24.ToRgba32(this.source3, this.destination4); + + /// + /// Converts BGR pixels to ARGB pixels. + /// + [Benchmark] + public void Bgr24ToArgb32() => PixelConverter.FromBgr24.ToArgb32(this.source3, this.destination4); + + /// + /// Converts BGR pixels to ABGR pixels. + /// + [Benchmark] + public void Bgr24ToAbgr32() => PixelConverter.FromBgr24.ToAbgr32(this.source3, this.destination4); + + /// + /// Converts BGR pixels to BGRA pixels. + /// + [Benchmark] + public void Bgr24ToBgra32() => PixelConverter.FromBgr24.ToBgra32(this.source3, this.destination4); + + /// + /// Converts BGR pixels to RGB pixels. + /// + [Benchmark] + public void Bgr24ToRgb24() => PixelConverter.FromBgr24.ToRgb24(this.source3, this.destination3); +} diff --git a/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs b/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs index 232d8b3e2..c69510bc4 100644 --- a/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs +++ b/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs @@ -7,7 +7,6 @@ using System.Runtime.InteropServices; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tuples; namespace SixLabors.ImageSharp.Benchmarks.General.PixelConversion; @@ -225,8 +224,8 @@ public class PixelConversion_Rgba32_To_Bgra32 // [Benchmark] public void Bitops_Simd() { - ref Octet sBase = ref Unsafe.As>(ref this.source[0]); - ref Octet dBase = ref Unsafe.As>(ref this.dest[0]); + ref InlineArray8 sBase = ref Unsafe.As>(ref this.source[0]); + ref InlineArray8 dBase = ref Unsafe.As>(ref this.dest[0]); for (nuint i = 0; i < (uint)this.Count / 8; i++) { @@ -249,9 +248,9 @@ public class PixelConversion_Rgba32_To_Bgra32 #pragma warning restore SA1132 // Do not combine fields [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void BitopsSimdImpl(ref Octet s, ref Octet d) + private static void BitopsSimdImpl(ref InlineArray8 s, ref InlineArray8 d) { - Vector sVec = Unsafe.As, Vector>(ref s); + Vector sVec = Unsafe.As, Vector>(ref s); Vector aMask = new(0xFF00FF00); Vector bMask = new(0x00FF00FF); @@ -274,7 +273,7 @@ public class PixelConversion_Rgba32_To_Bgra32 Vector cc = Unsafe.As>(ref c); Vector dd = aa + cc; - d = Unsafe.As, Octet>(ref dd); + d = Unsafe.As, InlineArray8>(ref dd); } // [Benchmark] diff --git a/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransform.cs b/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransform.cs new file mode 100644 index 000000000..d671117b6 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransform.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.Benchmarks.General.PixelConversion; + +/// +/// Measures the operator-driven affine vector transforms. +/// +[Config(typeof(Config.Short))] +public class Vector4AffineTransform +{ + private static readonly Vector4 Multiplier = new(255F, 2F, 65535F, .5F); + private static readonly Vector4 Offset = new(17F, -1F, 32768F, 3F); + private static readonly Vector4 Divisor = new(255F, 2F, 65535F, .5F); + + private Vector4[] current; + + /// + /// Gets or sets the number of vectors transformed by each invocation. + /// + [Params(1, 3, 4, 17, 256, 4096)] + public int Count { get; set; } + + /// + /// Creates a non-uniform input buffer. + /// + [GlobalSetup] + public void Setup() + { + this.current = new Vector4[this.Count]; + + for (int i = 0; i < this.current.Length; i++) + { + this.current[i] = new Vector4(i + .25F, i + .5F, i + .75F, i + 1F); + } + } + + /// + /// Executes the operator-driven multiply-then-add traversal. + /// + [Benchmark] + public void MultiplyThenAdd() + => Vector4Converters.MultiplyThenAdd(this.current, Multiplier, Offset); + + /// + /// Executes the operator-driven add-then-divide traversal. + /// + [Benchmark] + public void AddThenDivide() + => Vector4Converters.AddThenDivide(this.current, Offset, Divisor); +} diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs index da7ddae41..fd17262de 100644 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs +++ b/tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs @@ -5,8 +5,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Tuples; - namespace SixLabors.ImageSharp.Benchmarks.General.Vectorization; [Config(typeof(Config.Short))] @@ -30,12 +28,22 @@ public class WidenBytesToUInt32 { const int N = Count / 8; - ref Octet sBase = ref Unsafe.As>(ref this.source[0]); - ref Octet dBase = ref Unsafe.As>(ref this.dest[0]); + ref InlineArray8 sBase = ref Unsafe.As>(ref this.source[0]); + ref InlineArray8 dBase = ref Unsafe.As>(ref this.dest[0]); for (nuint i = 0; i < N; i++) { - Unsafe.Add(ref dBase, i).LoadFrom(ref Unsafe.Add(ref sBase, i)); + ref InlineArray8 source = ref Unsafe.Add(ref sBase, i); + ref InlineArray8 destination = ref Unsafe.Add(ref dBase, i); + + destination[0] = source[0]; + destination[1] = source[1]; + destination[2] = source[2]; + destination[3] = source[3]; + destination[4] = source[4]; + destination[5] = source[5]; + destination[6] = source[6]; + destination[7] = source[7]; } } diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index fa5fdd816..0351ecae9 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -69,6 +69,7 @@ + diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index f8bf19d57..815a145f7 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -6,6 +6,7 @@ using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using ImageMagick; using PhotoSauce.MagicScaler; using SixLabors.ImageSharp.Formats; @@ -27,6 +28,7 @@ public enum JpegKind Any = Baseline | Progressive } +[SupportedOSPlatform("windows")] public class LoadResizeSaveStressRunner { private const int Quality = 75; @@ -158,7 +160,7 @@ public class LoadResizeSaveStressRunner this.outputDirectory, Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath)); - private (int Width, int Height) ScaledSize(int inWidth, int inHeight, int outSize) + private static (int Width, int Height) ScaledSize(int inWidth, int inHeight, int outSize) { int width, height; if (inWidth > inHeight) @@ -180,7 +182,7 @@ public class LoadResizeSaveStressRunner using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true); this.LogImageProcessed(image.Width, image.Height); - (int width, int height) = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize); + (int width, int height) = ScaledSize(image.Width, image.Height, this.ThumbnailSize); Bitmap resized = new(width, height); using Graphics graphics = Graphics.FromImage(resized); using ImageAttributes attributes = new(); @@ -248,10 +250,10 @@ public class LoadResizeSaveStressRunner public void MagickResize(string input) { using MagickImage image = new(input); - this.LogImageProcessed(image.Width, image.Height); + this.LogImageProcessed((int)image.Width, (int)image.Height); // Resize it to fit a 150x150 square - image.Resize(this.ThumbnailSize, this.ThumbnailSize); + image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize); // Reduce the size of the file image.Strip(); @@ -282,7 +284,7 @@ public class LoadResizeSaveStressRunner { using SKBitmap original = SKBitmap.Decode(input); this.LogImageProcessed(original.Width, original.Height); - (int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); + (int width, int height) = ScaledSize(original.Width, original.Height, this.ThumbnailSize); using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType)); using SKPaint paint = new() { FilterQuality = SKFilterQuality.High }; SKCanvas canvas = surface.Canvas; @@ -300,7 +302,7 @@ public class LoadResizeSaveStressRunner { using SKBitmap original = SKBitmap.Decode(input); this.LogImageProcessed(original.Width, original.Height); - (int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); + (int width, int height) = ScaledSize(original.Width, original.Height, this.ThumbnailSize); using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High); if (resized == null) { @@ -319,7 +321,7 @@ public class LoadResizeSaveStressRunner SKImageInfo info = codec.Info; this.LogImageProcessed(info.Width, info.Height); - (int width, int height) = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize); + (int width, int height) = ScaledSize(info.Width, info.Height, this.ThumbnailSize); SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width); using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height)); diff --git a/tests/ImageSharp.Benchmarks/PixelBlenders/AssociatedAlphaPixelBlenderBenchmark.cs b/tests/ImageSharp.Benchmarks/PixelBlenders/AssociatedAlphaPixelBlenderBenchmark.cs new file mode 100644 index 000000000..02f25f2ea --- /dev/null +++ b/tests/ImageSharp.Benchmarks/PixelBlenders/AssociatedAlphaPixelBlenderBenchmark.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Benchmarks.PixelBlenders; + +/// +/// Compares straight-alpha and associated-alpha bulk pixel blending. +/// +[Config(typeof(Config.Short))] +public class AssociatedAlphaPixelBlenderBenchmark +{ + private const int PixelCount = 1024; + + private readonly Rgba32[] unassociatedDestination = new Rgba32[PixelCount]; + private readonly Rgba32[] unassociatedBackground = new Rgba32[PixelCount]; + private readonly Rgba32[] unassociatedSource = new Rgba32[PixelCount]; + private readonly Rgba32P[] associatedDestination = new Rgba32P[PixelCount]; + private readonly Rgba32P[] associatedBackground = new Rgba32P[PixelCount]; + private readonly Rgba32P[] associatedSource = new Rgba32P[PixelCount]; + private readonly float[] amounts = new float[PixelCount]; + private readonly Vector4[] unassociatedWorkingBuffer = new Vector4[PixelCount * 3]; + private readonly Vector4[] associatedWorkingBuffer = new Vector4[PixelCount * 3]; + private PixelBlender unassociatedBlender; + private PixelBlender associatedBlender; + + /// + /// Gets or sets the color-blending mode measured by the benchmark. + /// + [ParamsAllValues] + public PixelColorBlendingMode ColorMode { get; set; } + + /// + /// Gets or sets the alpha-composition mode measured by the benchmark. + /// + [ParamsAllValues] + public PixelAlphaCompositionMode AlphaMode { get; set; } + + /// + /// Initializes equivalent straight-alpha and associated-alpha rows. + /// + [GlobalSetup] + public void Setup() + { + this.unassociatedBlender = PixelOperations.Instance.GetPixelBlender(this.ColorMode, this.AlphaMode); + this.associatedBlender = PixelOperations.Instance.GetPixelBlender(this.ColorMode, this.AlphaMode); + + Random random = new(42); + + for (int i = 0; i < PixelCount; i++) + { + Rgba32 background = new((byte)random.Next(256), (byte)random.Next(256), (byte)random.Next(256), (byte)random.Next(64, 256)); + Rgba32 source = new((byte)random.Next(256), (byte)random.Next(256), (byte)random.Next(256), (byte)random.Next(64, 256)); + + this.unassociatedBackground[i] = background; + this.unassociatedSource[i] = source; + this.associatedBackground[i] = Rgba32P.FromRgba32(background); + this.associatedSource[i] = Rgba32P.FromRgba32(source); + this.amounts[i] = random.NextSingle(); + } + } + + /// + /// Blends one row stored with straight alpha. + /// + /// The last destination pixel. + [Benchmark(Description = "Straight alpha", Baseline = true)] + public Rgba32 BlendUnassociated() + { + this.unassociatedBlender.Blend( + Configuration.Default, + this.unassociatedDestination, + this.unassociatedBackground, + this.unassociatedSource, + this.amounts, + this.unassociatedWorkingBuffer); + + return this.unassociatedDestination[^1]; + } + + /// + /// Blends one row stored with associated alpha. + /// + /// The last destination pixel. + [Benchmark(Description = "Associated alpha")] + public Rgba32P BlendAssociated() + { + this.associatedBlender.Blend( + Configuration.Default, + this.associatedDestination, + this.associatedBackground, + this.associatedSource, + this.amounts, + this.associatedWorkingBuffer); + + return this.associatedDestination[^1]; + } +} diff --git a/tests/ImageSharp.Benchmarks/PixelBlenders/PorterDuffBulkVsSingleVector.cs b/tests/ImageSharp.Benchmarks/PixelBlenders/PorterDuffBulkVsSingleVector.cs index ecf8b125f..fe43ce5e7 100644 --- a/tests/ImageSharp.Benchmarks/PixelBlenders/PorterDuffBulkVsSingleVector.cs +++ b/tests/ImageSharp.Benchmarks/PixelBlenders/PorterDuffBulkVsSingleVector.cs @@ -18,8 +18,8 @@ public class PorterDuffBulkVsSingleVector [GlobalSetup] public void Setup() { - this.backdrop = new Vector4[8 * 20]; - this.source = new Vector4[8 * 20]; + this.backdrop = new Vector4[8 * 40]; + this.source = new Vector4[8 * 40]; FillRandom(this.backdrop); FillRandom(this.source); @@ -49,7 +49,7 @@ public class PorterDuffBulkVsSingleVector return result; } - [Benchmark(Description = "Avx")] + [Benchmark(Description = "Avx2")] public Vector256 OverlayValueFunction_Avx() { ref Vector256 backdrop = ref Unsafe.As>(ref MemoryMarshal.GetReference(this.backdrop)); @@ -65,4 +65,21 @@ public class PorterDuffBulkVsSingleVector return result; } + + [Benchmark(Description = "Avx512")] + public Vector512 OverlayValueFunction_Avx512() + { + ref Vector512 backdrop = ref Unsafe.As>(ref MemoryMarshal.GetReference(this.backdrop)); + ref Vector512 source = ref Unsafe.As>(ref MemoryMarshal.GetReference(this.source)); + + Vector512 result = default; + Vector512 opacity = Vector512.Create(.5F); + int count = this.backdrop.Length / 4; + for (nuint i = 0; i < (uint)count; i++) + { + result = PorterDuffFunctions.NormalSrcOver(Unsafe.Add(ref backdrop, i), Unsafe.Add(ref source, i), opacity); + } + + return result; + } } diff --git a/tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs b/tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs new file mode 100644 index 000000000..dbaf21a8e --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs @@ -0,0 +1,302 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Benchmarks.Processing; + +[Config(typeof(Config.Standard))] +public class ColorMatchingCaches +{ + // IterationSetup forces BenchmarkDotNet to use a single benchmark invocation per iteration. + // Repeated lookups can safely replay a smaller working set because that workload is explicitly + // meant to model steady-state cache hits after warmup. + private const int RepeatedLookupCount = 262_144; + + // DitherLike should avoid replaying the same stream across multiple passes because that warms + // the caches in a way real high-churn input usually does not. Make the single pass larger instead. + private const int DitherLikeLookupCount = 1_048_576; + private const int RepeatedPassCount = 16; + + private Rgba32[] palette; + private Rgba32[] repeatedSeedColors; + private Rgba32[] repeatedLookups; + private Rgba32[] ditherLookups; + + private PixelMap coarse; + private PixelMap legacyCoarse; + private PixelMap exact; + private PixelMap uncached; + + [Params(16, 256)] + public int PaletteSize { get; set; } + + [Params(CacheWorkload.Repeated, CacheWorkload.DitherLike)] + public CacheWorkload Workload { get; set; } + + [GlobalSetup] + public void Setup() + { + this.palette = CreatePalette(this.PaletteSize); + this.repeatedSeedColors = CreateRepeatedSeedColors(this.palette); + this.repeatedLookups = CreateRepeatedLookups(this.repeatedSeedColors); + this.ditherLookups = CreateDitherLikeLookups(); + + this.coarse = CreatePixelMap(this.palette); + this.legacyCoarse = CreatePixelMap(this.palette); + this.exact = CreatePixelMap(this.palette); + this.uncached = CreatePixelMap(this.palette); + } + + [IterationSetup] + public void ResetCaches() + { + // Each benchmark iteration should start from the same cache state so we measure + // the cache policy itself rather than warm state carried over from a previous iteration. + this.coarse.Clear(this.palette); + this.legacyCoarse.Clear(this.palette); + this.exact.Clear(this.palette); + this.uncached.Clear(this.palette); + + if (this.Workload == CacheWorkload.Repeated) + { + // Prime the repeated workload so the benchmark reflects steady-state hit behavior + // instead of mostly measuring the first-wave fill cost. + Prime(this.coarse, this.repeatedSeedColors); + Prime(this.legacyCoarse, this.repeatedSeedColors); + Prime(this.exact, this.repeatedSeedColors); + Prime(this.uncached, this.repeatedSeedColors); + } + } + + [GlobalCleanup] + public void Cleanup() + { + this.coarse.Dispose(); + this.legacyCoarse.Dispose(); + this.exact.Dispose(); + this.uncached.Dispose(); + } + + [Benchmark(Baseline = true, Description = "Coarse")] + public int Coarse() => this.Run(this.coarse); + + [Benchmark(Description = "Legacy Coarse")] + public int LegacyCoarse() => this.Run(this.legacyCoarse); + + [Benchmark(Description = "Exact Cached")] + public int Exact() => this.Run(this.exact); + + [Benchmark(Description = "Exact Uncached")] + public int Uncached() => this.Run(this.uncached); + + public enum CacheWorkload + { + // A small working set that is intentionally reused after priming to measure hit-heavy behavior. + Repeated, + + // A deterministic high-churn stream intended to resemble dithered lookups where exact repeats are rare. + DitherLike + } + + private int Run(PixelMap map) + { + Rgba32[] lookups = this.Workload == CacheWorkload.Repeated ? this.repeatedLookups : this.ditherLookups; + int passCount = this.Workload == CacheWorkload.Repeated ? RepeatedPassCount : 1; + int checksum = 0; + + // Repeated intentionally replays the same lookup stream to measure steady-state hit behavior. + // DitherLike runs as a single larger pass so we do not turn a churn-heavy workload into an + // artificially warmed cache benchmark by replaying the exact same sequence. + for (int pass = 0; pass < passCount; pass++) + { + for (int i = 0; i < lookups.Length; i++) + { + checksum = unchecked((checksum * 31) + map.GetClosestColor(lookups[i], out _)); + } + } + + return checksum; + } + + private static PixelMap CreatePixelMap(Rgba32[] palette) + where TCache : struct, IColorIndexCache + => new EuclideanPixelMap(Configuration.Default, palette); + + private static void Prime(PixelMap map, Rgba32[] colors) + { + for (int i = 0; i < colors.Length; i++) + { + map.GetClosestColor(colors[i], out _); + } + } + + private static Rgba32[] CreatePalette(int count) + { + Rgba32[] result = new Rgba32[count]; + + for (int i = 0; i < result.Length; i++) + { + // Use the Knuth/golden-ratio multiplicative hash constant to spread colors across + // RGBA space without clustering into a gradient. That keeps the benchmark from + // accidentally favoring any cache because the palette itself is too regular. + uint value = unchecked((uint)(i + 1) * 2654435761U); + result[i] = new( + (byte)value, + (byte)(value >> 8), + (byte)(value >> 16), + (byte)((value >> 24) | 0x80)); + } + + return result; + } + + private static Rgba32[] CreateRepeatedSeedColors(Rgba32[] palette) + { + // Reuse colors derived from the palette but perturb them slightly so the workload still + // exercises nearest-color matching rather than only exact palette-entry hits. + int count = Math.Min(64, palette.Length * 2); + Rgba32[] result = new Rgba32[count]; + + for (int i = 0; i < result.Length; i++) + { + Rgba32 source = palette[(i * 17) % palette.Length]; + result[i] = new( + (byte)(source.R + ((i * 3) & 0x07)), + (byte)(source.G + ((i * 5) & 0x07)), + (byte)(source.B + ((i * 7) & 0x07)), + source.A); + } + + return result; + } + + private static Rgba32[] CreateRepeatedLookups(Rgba32[] seedColors) + { + Rgba32[] result = new Rgba32[RepeatedLookupCount]; + + // Cycle a small seed set to produce a stable, hit-heavy stream after priming. + for (int i = 0; i < result.Length; i++) + { + result[i] = seedColors[i % seedColors.Length]; + } + + return result; + } + + private static Rgba32[] CreateDitherLikeLookups() + { + Rgba32[] result = new Rgba32[DitherLikeLookupCount]; + + // Generate a deterministic pseudo-image signal with independent channel slopes so nearby + // samples are correlated but exact repeats are uncommon, which is closer to dithered input. + for (int i = 0; i < result.Length; i++) + { + int x = i & 511; + int y = i >> 9; + + result[i] = new( + (byte)((x * 17) + (y * 13)), + (byte)((x * 29) + (y * 7)), + (byte)((x * 11) + (y * 23)), + (byte)(255 - ((x * 3) + (y * 5)))); + } + + return result; + } + + /// + /// Preserves the original direct-mapped coarse cache implementation for side-by-side benchmarks. + /// + private unsafe struct LegacyCoarseCache : IColorIndexCache + { + private const int IndexRBits = 5; + private const int IndexGBits = 5; + private const int IndexBBits = 5; + private const int IndexABits = 6; + private const int IndexRCount = 1 << IndexRBits; + private const int IndexGCount = 1 << IndexGBits; + private const int IndexBCount = 1 << IndexBBits; + private const int IndexACount = 1 << IndexABits; + private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount; + + private readonly IMemoryOwner binsOwner; + private readonly short* binsPointer; + private MemoryHandle binsHandle; + + private LegacyCoarseCache(MemoryAllocator allocator) + { + this.binsOwner = allocator.Allocate(TotalBins); + this.binsOwner.GetSpan().Fill(-1); + this.binsHandle = this.binsOwner.Memory.Pin(); + this.binsPointer = (short*)this.binsHandle.Pointer; + } + + public static LegacyCoarseCache Create(MemoryAllocator allocator) => new(allocator); + + [MethodImpl(InliningOptions.ShortMethod)] + public readonly bool TryAdd(Rgba32 color, short value) + { + this.binsPointer[GetCoarseIndex(color)] = value; + return true; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public readonly bool TryGetValue(Rgba32 color, out short value) + { + value = this.binsPointer[GetCoarseIndex(color)]; + return value > -1; + } + + public readonly void Clear() => this.binsOwner.GetSpan().Fill(-1); + + public void Dispose() + { + this.binsHandle.Dispose(); + this.binsOwner.Dispose(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetCoarseIndex(Rgba32 color) + { + int rIndex = color.R >> (8 - IndexRBits); + int gIndex = color.G >> (8 - IndexGBits); + int bIndex = color.B >> (8 - IndexBBits); + int aIndex = color.A >> (8 - IndexABits); + + return (aIndex * IndexRCount * IndexGCount * IndexBCount) + + (rIndex * IndexGCount * IndexBCount) + + (gIndex * IndexBCount) + + bIndex; + } + } + + /// + /// Preserves the uncached path for exact-cache comparison benchmarks. + /// + private readonly struct UncachedCache : IColorIndexCache + { + public static UncachedCache Create(MemoryAllocator allocator) => default; + + public bool TryAdd(Rgba32 color, short value) => true; + + public bool TryGetValue(Rgba32 color, out short value) + { + value = -1; + return false; + } + + public void Clear() + { + } + + public void Dispose() + { + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs b/tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs new file mode 100644 index 000000000..4f2089713 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Processing/ParallelProcessing.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks; + +public class ParallelProcessing +{ + private Image image; + private Image foreground; + private Configuration configuration; + + public static IEnumerable MaxDegreeOfParallelismValues() + { + int processorCount = Environment.ProcessorCount; + for (int p = 1; p <= processorCount; p *= 2) + { + yield return p; + } + + if ((processorCount & (processorCount - 1)) != 0) + { + yield return processorCount; + } + } + + [ParamsSource(nameof(MaxDegreeOfParallelismValues))] + public int MaxDegreeOfParallelism { get; set; } + + [GlobalSetup] + public void Setup() + { + this.image = new Image(2048, 2048); + this.foreground = new Image(2048, 2048); + this.configuration = Configuration.Default.Clone(); + this.configuration.MaxDegreeOfParallelism = this.MaxDegreeOfParallelism; + } + + [Benchmark] + public void DetectEdges() => this.image.Mutate(this.configuration, x => x.DetectEdges()); + + [Benchmark] + public void DrawImage() => this.image.Mutate(this.configuration, x => x.DrawImage(this.foreground, 0.5f)); + + [Benchmark] + public void Crop() + { + Rectangle bounds = this.image.Bounds; + bounds = new Rectangle(1, 1, bounds.Width - 2, bounds.Height - 2); + this.image + .Clone(this.configuration, x => x.Crop(bounds)) + .Dispose(); + } + + [GlobalCleanup] + public void Cleanup() + { + this.image.Dispose(); + this.foreground.Dispose(); + } +} diff --git a/tests/ImageSharp.PublicApi.Tests/ImageSharp.PublicApi.Tests.csproj b/tests/ImageSharp.PublicApi.Tests/ImageSharp.PublicApi.Tests.csproj new file mode 100644 index 000000000..33bbc1b94 --- /dev/null +++ b/tests/ImageSharp.PublicApi.Tests/ImageSharp.PublicApi.Tests.csproj @@ -0,0 +1,23 @@ + + + + + SixLabors.ImageSharp.PublicApi.Tests + SixLabors.ImageSharp.PublicApi.Tests + Debug;Release + + + + + + net8.0;net10.0 + + + + + net8.0 + + + + + diff --git a/tests/ImageSharp.PublicApi.Tests/PixelOperationsExtensibilityTests.cs b/tests/ImageSharp.PublicApi.Tests/PixelOperationsExtensibilityTests.cs new file mode 100644 index 000000000..a1916831f --- /dev/null +++ b/tests/ImageSharp.PublicApi.Tests/PixelOperationsExtensibilityTests.cs @@ -0,0 +1,163 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.PublicApi.Tests; + +/// +/// Verifies that representation-aware pixel operations can be implemented outside the ImageSharp assembly. +/// +public class PixelOperationsExtensibilityTests +{ + /// + /// Verifies that an external subclass can override and invoke every representation-aware hook. + /// + [Fact] + public void PixelOperationsExposesAllRepresentationHooksToExternalSubclasses() + { + ExternalRgba32PixelOperations operations = new(); + + operations.InvokeRepresentationHooks(); + + Assert.Equal(RepresentationHooks.All, operations.InvokedHooks); + } + + /// + /// Verifies that an external subclass can override and invoke every representation-aware hook. + /// + [Fact] + public void AssociatedAlphaPixelOperationsExposesAllRepresentationHooksToExternalSubclasses() + { + ExternalRgba32PPixelOperations operations = new(); + + operations.InvokeRepresentationHooks(); + + Assert.Equal(RepresentationHooks.All, operations.InvokedHooks); + } + + [Flags] + private enum RepresentationHooks + { + None = 0, + ToUnassociatedVector4 = 1 << 0, + ToAssociatedVector4 = 1 << 1, + ToUnassociatedScaledVector4 = 1 << 2, + ToAssociatedScaledVector4 = 1 << 3, + FromUnassociatedVector4Destructive = 1 << 4, + FromAssociatedVector4Destructive = 1 << 5, + FromUnassociatedScaledVector4Destructive = 1 << 6, + FromAssociatedScaledVector4Destructive = 1 << 7, + All = ToUnassociatedVector4 + | ToAssociatedVector4 + | ToUnassociatedScaledVector4 + | ToAssociatedScaledVector4 + | FromUnassociatedVector4Destructive + | FromAssociatedVector4Destructive + | FromUnassociatedScaledVector4Destructive + | FromAssociatedScaledVector4Destructive + } + + /// + /// Exercises the representation-aware hooks inherited directly from . + /// + private sealed class ExternalRgba32PixelOperations : PixelOperations + { + /// + /// Gets the hooks invoked through this external subclass. + /// + public RepresentationHooks InvokedHooks { get; private set; } + + /// + /// Invokes every representation-aware operation exposed to derived classes. + /// + public void InvokeRepresentationHooks() + { + // Empty spans isolate accessibility and virtual dispatch; conversion behavior is covered by the pixel-operation test suite. + this.ToUnassociatedVector4(Configuration.Default, ReadOnlySpan.Empty, Span.Empty); + this.ToAssociatedVector4(Configuration.Default, ReadOnlySpan.Empty, Span.Empty); + this.ToUnassociatedScaledVector4(Configuration.Default, ReadOnlySpan.Empty, Span.Empty); + this.ToAssociatedScaledVector4(Configuration.Default, ReadOnlySpan.Empty, Span.Empty); + this.FromUnassociatedVector4Destructive(Configuration.Default, Span.Empty, Span.Empty); + this.FromAssociatedVector4Destructive(Configuration.Default, Span.Empty, Span.Empty); + this.FromUnassociatedScaledVector4Destructive(Configuration.Default, Span.Empty, Span.Empty); + this.FromAssociatedScaledVector4Destructive(Configuration.Default, Span.Empty, Span.Empty); + } + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.InvokedHooks |= RepresentationHooks.ToUnassociatedVector4; + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.InvokedHooks |= RepresentationHooks.ToAssociatedVector4; + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.InvokedHooks |= RepresentationHooks.ToUnassociatedScaledVector4; + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.InvokedHooks |= RepresentationHooks.ToAssociatedScaledVector4; + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) => this.InvokedHooks |= RepresentationHooks.FromUnassociatedVector4Destructive; + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) => this.InvokedHooks |= RepresentationHooks.FromAssociatedVector4Destructive; + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) => this.InvokedHooks |= RepresentationHooks.FromUnassociatedScaledVector4Destructive; + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) => this.InvokedHooks |= RepresentationHooks.FromAssociatedScaledVector4Destructive; + } + + /// + /// Exercises the representation-aware hooks inherited through . + /// + private sealed class ExternalRgba32PPixelOperations : AssociatedAlphaPixelOperations + { + /// + /// Gets the hooks invoked through this external subclass. + /// + public RepresentationHooks InvokedHooks { get; private set; } + + /// + /// Invokes every representation-aware operation exposed to derived classes. + /// + public void InvokeRepresentationHooks() + { + // Empty spans isolate accessibility and virtual dispatch; conversion behavior is covered by the pixel-operation test suite. + this.ToUnassociatedVector4(Configuration.Default, ReadOnlySpan.Empty, Span.Empty); + this.ToAssociatedVector4(Configuration.Default, ReadOnlySpan.Empty, Span.Empty); + this.ToUnassociatedScaledVector4(Configuration.Default, ReadOnlySpan.Empty, Span.Empty); + this.ToAssociatedScaledVector4(Configuration.Default, ReadOnlySpan.Empty, Span.Empty); + this.FromUnassociatedVector4Destructive(Configuration.Default, Span.Empty, Span.Empty); + this.FromAssociatedVector4Destructive(Configuration.Default, Span.Empty, Span.Empty); + this.FromUnassociatedScaledVector4Destructive(Configuration.Default, Span.Empty, Span.Empty); + this.FromAssociatedScaledVector4Destructive(Configuration.Default, Span.Empty, Span.Empty); + } + + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.InvokedHooks |= RepresentationHooks.ToUnassociatedVector4; + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.InvokedHooks |= RepresentationHooks.ToAssociatedVector4; + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.InvokedHooks |= RepresentationHooks.ToUnassociatedScaledVector4; + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.InvokedHooks |= RepresentationHooks.ToAssociatedScaledVector4; + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) => this.InvokedHooks |= RepresentationHooks.FromUnassociatedVector4Destructive; + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) => this.InvokedHooks |= RepresentationHooks.FromAssociatedVector4Destructive; + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) => this.InvokedHooks |= RepresentationHooks.FromUnassociatedScaledVector4Destructive; + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) => this.InvokedHooks |= RepresentationHooks.FromAssociatedScaledVector4Destructive; + } +} diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index bc52610d2..f3aa910b9 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -8,7 +8,6 @@ false SixLabors.ImageSharp.Tests.ProfilingSandbox win-x64 - SixLabors.ImageSharp.Tests.ProfilingSandbox.Program false false diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs index 6850756df..5e21b7cc1 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Globalization; +using System.Runtime.Versioning; using System.Text; using CommandLine; using CommandLine.Text; @@ -13,7 +14,8 @@ using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; // See ImageSharp.Benchmarks/LoadResizeSave/README.md -internal class LoadResizeSaveParallelMemoryStress +[SupportedOSPlatform("windows")] +internal sealed class LoadResizeSaveParallelMemoryStress { private LoadResizeSaveParallelMemoryStress() { @@ -206,14 +208,15 @@ internal class LoadResizeSaveParallelMemoryStress StringBuilder bld = new(); bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |"); bld.AppendLine( + CultureInfo.InvariantCulture, $"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |"); bld.Append("| "); - bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds); + bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.TotalSeconds)), this.TotalSeconds); bld.Append(" | "); - bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec); + bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec); bld.Append(" | "); - bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu); + bld.AppendFormat(CultureInfo.InvariantCulture, F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu); bld.AppendLine(" |"); return bld.ToString(); @@ -223,7 +226,7 @@ internal class LoadResizeSaveParallelMemoryStress } } - private class CommandLineOptions + private sealed class CommandLineOptions { [Option('a', "async-imagesharp", Required = false, Default = false, HelpText = "Async ImageSharp without benchmark switching")] public bool AsyncImageSharp { get; set; } diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputBenchmark.cs b/tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputBenchmark.cs new file mode 100644 index 000000000..e85a3ee97 --- /dev/null +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ProcessorThroughputBenchmark.cs @@ -0,0 +1,262 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using CommandLine; +using CommandLine.Text; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; + +public sealed class ProcessorThroughputBenchmark +{ + private readonly CommandLineOptions options; + private readonly Configuration configuration; + private ulong totalProcessedPixels; + + private ProcessorThroughputBenchmark(CommandLineOptions options) + { + this.options = options; + this.configuration = Configuration.Default.Clone(); + this.configuration.MaxDegreeOfParallelism = options.ProcessorParallelism > 0 + ? options.ProcessorParallelism + : Environment.ProcessorCount; + } + + public static Task RunAsync(string[] args) + { + CommandLineOptions options = null; + if (args.Length > 0) + { + options = CommandLineOptions.Parse(args); + if (options == null) + { + return Task.CompletedTask; + } + } + + options ??= new CommandLineOptions(); + return new ProcessorThroughputBenchmark(options.Normalize()) + .RunAsync(); + } + + private async Task RunAsync() + { + SemaphoreSlim semaphore = new(this.options.ConcurrentRequests); + Console.WriteLine(this.options.Method); + Func action = this.options.Method switch + { + Method.Crop => this.Crop, + Method.Edges => this.DetectEdges, + Method.EdgesCompass => this.EdgesCompass, + Method.DrawImage => this.DrawImage, + Method.BinaryThreshold => this.BinaryThreshold, + Method.Histogram => this.Histogram, + Method.OilPaint => this.OilPaint, + Method.GaussianBlur => this.GaussianBlur, + _ => throw new NotImplementedException(), + }; + + Console.WriteLine(this.options); + Console.WriteLine($"Running {this.options.Method} for {this.options.Seconds} seconds ..."); + TimeSpan runFor = TimeSpan.FromSeconds(this.options.Seconds); + + // inFlight starts at 1 to represent the dispatch loop itself + int inFlight = 1; + TaskCompletionSource drainTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + + Stopwatch stopwatch = Stopwatch.StartNew(); + while (stopwatch.Elapsed < runFor && !drainTcs.Task.IsCompleted) + { + await semaphore.WaitAsync(); + + if (stopwatch.Elapsed >= runFor) + { + semaphore.Release(); + break; + } + + Interlocked.Increment(ref inFlight); + + _ = ProcessImage(); + + async Task ProcessImage() + { + try + { + if (stopwatch.Elapsed >= runFor || drainTcs.Task.IsCompleted) + { + return; + } + + await Task.Yield(); // "emulate IO", i.e., make sure the processing code is async + ulong pixels = (ulong)action(); + Interlocked.Add(ref this.totalProcessedPixels, pixels); + } + catch (Exception ex) + { + Console.WriteLine(ex); + drainTcs.TrySetException(ex); + } + finally + { + semaphore.Release(); + if (Interlocked.Decrement(ref inFlight) == 0) + { + drainTcs.TrySetResult(); + } + } + } + } + + // Release the dispatch loop's own count; if no work is in flight, this completes immediately + if (Interlocked.Decrement(ref inFlight) == 0) + { + drainTcs.TrySetResult(); + } + + await drainTcs.Task; + stopwatch.Stop(); + + double totalMegaPixels = this.totalProcessedPixels / 1_000_000.0; + double totalSeconds = stopwatch.ElapsedMilliseconds / 1000.0; + double megapixelsPerSec = totalMegaPixels / totalSeconds; + Console.WriteLine($"TotalSeconds: {totalSeconds:F2}"); + Console.WriteLine($"MegaPixelsPerSec: {megapixelsPerSec:F2}"); + } + + private int OilPaint() + { + using Image image = new(this.options.Width, this.options.Height); + image.Mutate(this.configuration, x => x.OilPaint()); + return image.Width * image.Height; + } + + private int DetectEdges() + { + using Image image = new(this.options.Width, this.options.Height); + image.Mutate(this.configuration, x => x.DetectEdges()); + return image.Width * image.Height; + } + + private int EdgesCompass() + { + using Image image = new(this.options.Width, this.options.Height); + image.Mutate(this.configuration, x => x.DetectEdges(EdgeDetectorCompassKernel.Kirsch)); + return image.Width * image.Height; + } + + private int Crop() + { + using Image image = new(this.options.Width, this.options.Height); + Rectangle bounds = image.Bounds; + bounds = new Rectangle(1, 1, bounds.Width - 2, bounds.Height - 2); + image.Clone(this.configuration, x => x.Crop(bounds)).Dispose(); + return image.Width * image.Height; + } + + private int DrawImage() + { + using Image image = new(this.options.Width, this.options.Height); + using Image foreground = new(this.options.Width, this.options.Height); + image.Mutate(this.configuration, c => c.DrawImage(foreground, 0.5f)); + return image.Width * image.Height; + } + + private int BinaryThreshold() + { + using Image image = new(this.options.Width, this.options.Height); + image.Mutate(this.configuration, c => c.BinaryThreshold(0.5f)); + return image.Width * image.Height; + } + + private int Histogram() + { + using Image image = new(this.options.Width, this.options.Height); + image.Mutate(this.configuration, c => c.HistogramEqualization()); + return image.Width * image.Height; + } + + private int GaussianBlur() + { + using Image image = new(this.options.Width, this.options.Height); + image.Mutate(this.configuration, c => c.GaussianBlur()); + return image.Width * image.Height; + } + + private enum Method + { + Edges, + EdgesCompass, + Crop, + DrawImage, + BinaryThreshold, + Histogram, + OilPaint, + GaussianBlur, + } + + private sealed class CommandLineOptions + { + [Option('m', "method", Required = false, Default = Method.Edges, HelpText = "The stress test method to run (Edges, Crop)")] + public Method Method { get; set; } = Method.Edges; + + [Option('p', "processor-parallelism", Required = false, Default = -1, HelpText = "Level of parallelism for the image processor")] + public int ProcessorParallelism { get; set; } = -1; + + [Option('c', "concurrent-requests", Required = false, Default = -1, HelpText = "Number of concurrent in-flight requests")] + public int ConcurrentRequests { get; set; } = -1; + + [Option('w', "width", Required = false, Default = 4000, HelpText = "Width of the test image")] + public int Width { get; set; } = 4000; + + [Option('h', "height", Required = false, Default = 4000, HelpText = "Height of the test image")] + public int Height { get; set; } = 4000; + + [Option('s', "seconds", Required = false, Default = 5, HelpText = "Duration of the stress test in seconds")] + public int Seconds { get; set; } = 5; + + public override string ToString() => string.Join( + "|", + $"method: {this.Method}", + $"processor-parallelism: {this.ProcessorParallelism}", + $"concurrent-requests: {this.ConcurrentRequests}", + $"width: {this.Width}", + $"height: {this.Height}", + $"seconds: {this.Seconds}"); + + public CommandLineOptions Normalize() + { + if (this.ProcessorParallelism < 0) + { + this.ProcessorParallelism = Environment.ProcessorCount; + } + + if (this.ConcurrentRequests < 0) + { + this.ConcurrentRequests = Environment.ProcessorCount; + } + + return this; + } + + public static CommandLineOptions Parse(string[] args) + { + CommandLineOptions result = null; + using Parser parser = new(settings => settings.CaseInsensitiveEnumValues = true); + ParserResult parserResult = parser.ParseArguments(args).WithParsed(o => + { + result = o; + }); + + if (result == null) + { + Console.WriteLine(HelpText.RenderUsageText(parserResult)); + } + + return result; + } + } +} diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs index 8ba862560..db8892cd7 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs @@ -1,76 +1,38 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Reflection; using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; +using SixLabors.ImageSharp.Tests.ProfilingSandbox; using Xunit.Abstractions; // in this file, comments are used for disabling stuff for local execution #pragma warning disable SA1515 #pragma warning disable SA1512 -namespace SixLabors.ImageSharp.Tests.ProfilingSandbox; +// LoadResizeSaveParallelMemoryStress.Run(args); +// ParallelProcessingStress.RunExperiment(args); +// ParallelProcessingStress.Run(args); +await ProcessorThroughputBenchmark.RunAsync(args); -public class Program -{ - private class ConsoleOutput : ITestOutputHelper - { - public void WriteLine(string message) => Console.WriteLine(message); - - public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args); - } - - /// - /// The main entry point. Useful for executing benchmarks and performance unit tests manually, - /// when the IDE test runners lack some of the functionality. Eg.: it's not possible to run JetBrains memory profiler for unit tests. - /// - /// - /// The arguments to pass to the program. - /// - public static void Main(string[] args) - { - try - { - LoadResizeSaveParallelMemoryStress.Run(args); - } - catch (Exception ex) - { - Console.WriteLine(ex); - } - - // RunJpegEncoderProfilingTests(); - // RunJpegColorProfilingTests(); - // RunDecodeJpegProfilingTests(); - // RunToVector4ProfilingTest(); - // RunResizeProfilingTest(); - - // Console.ReadLine(); - } +// RunToVector4ProfilingTest(); +// RunResizeProfilingTest(); - private static Version GetNetCoreVersion() - { - Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - Console.WriteLine(assembly.Location); - string[] assemblyPath = assembly.Location.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries); - int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); - if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) - { - return Version.Parse(assemblyPath[netCoreAppIndex + 1]); - } +static void RunResizeProfilingTest() +{ + ResizeProfilingBenchmarks test = new(new ConsoleOutput()); + test.ResizeBicubic(4000, 4000); +} - return null; - } +static void RunToVector4ProfilingTest() +{ + PixelOperationsTests.Rgba32_OperationsTests tests = new(new ConsoleOutput()); + tests.Benchmark_ToVector4(); +} - private static void RunResizeProfilingTest() - { - ResizeProfilingBenchmarks test = new(new ConsoleOutput()); - test.ResizeBicubic(4000, 4000); - } +sealed class ConsoleOutput : ITestOutputHelper +{ + public void WriteLine(string message) => Console.WriteLine(message); - private static void RunToVector4ProfilingTest() - { - PixelOperationsTests.Rgba32_OperationsTests tests = new(new ConsoleOutput()); - tests.Benchmark_ToVector4(); - } + public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args); } diff --git a/tests/ImageSharp.Tests/Color/ColorTests.CastFrom.cs b/tests/ImageSharp.Tests/Color/ColorTests.CastFrom.cs index 2eb821b61..889746453 100644 --- a/tests/ImageSharp.Tests/Color/ColorTests.CastFrom.cs +++ b/tests/ImageSharp.Tests/Color/ColorTests.CastFrom.cs @@ -35,6 +35,20 @@ public partial class ColorTests Assert.Equal(source, data); } + [Fact] + public void Rgba32P() + { + Rgba32P source = new(64, 32, 16, 128); + + // Act: + Color color = Color.FromPixel(source); + + // Assert: + Assert.Equal(PixelAlphaRepresentation.Associated, color.AlphaRepresentation); + Assert.Equal(source.ToScaledVector4(), color.ToScaledVector4()); + Assert.Equal(source, color.ToPixel()); + } + [Fact] public void Argb32() { diff --git a/tests/ImageSharp.Tests/Color/ColorTests.CastTo.cs b/tests/ImageSharp.Tests/Color/ColorTests.CastTo.cs index 2e2f0d07d..66537007f 100644 --- a/tests/ImageSharp.Tests/Color/ColorTests.CastTo.cs +++ b/tests/ImageSharp.Tests/Color/ColorTests.CastTo.cs @@ -114,6 +114,65 @@ public partial class ColorTests Assert.Equal(new L8(255), color.ToPixel()); } + [Fact] + public void AssociatedVectorConstructor() + { + Rgba32P expected = new(64, 32, 16, 128); + + // Act: + Color color = Color.FromScaledVector(expected.ToScaledVector4(), PixelAlphaRepresentation.Associated); + + // Assert: + Assert.Equal(PixelAlphaRepresentation.Associated, color.AlphaRepresentation); + Assert.Equal(expected.ToScaledVector4(), color.ToScaledVector4()); + Assert.Equal(expected, color.ToPixel()); + Assert.Equal(new Rgba32(128, 64, 32, 128), color.ToPixel()); + } + + [Fact] + public void UnassociatedPixelToAssociatedPixel() + { + Color color = Color.FromPixel(new Rgba32(128, 64, 32, 128)); + + // Act: + Rgba32P actual = color.ToPixel(); + + // Assert: + Assert.Equal(new Rgba32P(64, 32, 16, 128), actual); + } + + [Fact] + public void AssociatedVectorSpanConstructor() + { + Rgba32P expected = new(64, 32, 16, 128); + Vector4[] source = [expected.ToScaledVector4()]; + Color[] destination = new Color[source.Length]; + + // Act: + Color.FromScaledVector(source, destination, PixelAlphaRepresentation.Associated); + + // Assert: + Assert.Equal(PixelAlphaRepresentation.Associated, destination[0].AlphaRepresentation); + Assert.Equal(expected.ToScaledVector4(), destination[0].ToScaledVector4()); + Assert.Equal(expected, destination[0].ToPixel()); + } + + [Fact] + public void AssociatedPixelSpanRoundTrip() + { + Rgba32P[] source = [new(64, 32, 16, 128), new(24, 12, 6, 96)]; + Color[] colors = new Color[source.Length]; + Rgba32P[] destination = new Rgba32P[source.Length]; + + // Act: + Color.FromPixel(source, colors); + Color.ToPixel(colors, destination); + + // Assert: + Assert.Equal(source, destination); + Assert.All(colors, color => Assert.Equal(PixelAlphaRepresentation.Associated, color.AlphaRepresentation)); + } + [Fact] public void GenericPixelRoundTrip() { diff --git a/tests/ImageSharp.Tests/Color/ColorTests.cs b/tests/ImageSharp.Tests/Color/ColorTests.cs index c482fc998..6c205888a 100644 --- a/tests/ImageSharp.Tests/Color/ColorTests.cs +++ b/tests/ImageSharp.Tests/Color/ColorTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests; @@ -18,6 +19,58 @@ public partial class ColorTests Assert.Equal(expected, c2.ToPixel()); } + [Fact] + public void WithAlphaPreservesAssociatedRepresentation() + { + Color color = Color.FromPixel(new Rgba32P(64, 32, 16, 128)); + + // Act: + Color actual = color.WithAlpha(64F / 255F); + + // Assert: + Assert.Equal(PixelAlphaRepresentation.Associated, actual.AlphaRepresentation); + Assert.Equal(new Rgba32P(32, 16, 8, 64), actual.ToPixel()); + Assert.Equal(new Rgba32(128, 64, 32, 64), actual.ToPixel()); + } + + [Fact] + public void ToHexConvertsAssociatedRepresentation() + { + Color color = Color.FromPixel(new Rgba32P(64, 32, 16, 128)); + + Assert.Equal("80402080", color.ToHex()); + } + + [Fact] + public void ToScaledVector4ConvertsUnassociatedToAssociated() + { + Color color = Color.FromScaledVector(new Vector4(1F, 0.5F, 0.25F, 0.5F)); + + Vector4 actual = color.ToScaledVector4(PixelAlphaRepresentation.Associated); + + Assert.Equal(new Vector4(0.5F, 0.25F, 0.125F, 0.5F), actual); + } + + [Fact] + public void ToScaledVector4ConvertsAssociatedToUnassociated() + { + Color color = Color.FromScaledVector(new Vector4(0.5F, 0.25F, 0.125F, 0.5F), PixelAlphaRepresentation.Associated); + + Vector4 actual = color.ToScaledVector4(PixelAlphaRepresentation.Unassociated); + + Assert.Equal(new Vector4(1F, 0.5F, 0.25F, 0.5F), actual); + } + + [Fact] + public void EqualityDoesNotDependOnInternalAssociationStorage() + { + Color fromPixel = Color.FromPixel(new Rgba32P(64, 32, 16, 128)); + Color fromVector = Color.FromScaledVector(fromPixel.ToScaledVector4(), PixelAlphaRepresentation.Associated); + + Assert.Equal(fromPixel, fromVector); + Assert.Equal(fromPixel.GetHashCode(), fromVector.GetHashCode()); + } + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/tests/ImageSharp.Tests/Color/RgbaDouble.cs b/tests/ImageSharp.Tests/Color/RgbaDouble.cs index 76fdf365c..2487d5fde 100644 --- a/tests/ImageSharp.Tests/Color/RgbaDouble.cs +++ b/tests/ImageSharp.Tests/Color/RgbaDouble.cs @@ -106,6 +106,47 @@ public struct RgbaDouble : IPixel /// public static PixelOperations CreatePixelOperations() => new(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() + { + Vector4 vector = this.ToScaledVector4(); + Numerics.Premultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToAssociatedScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaDouble FromUnassociatedScaledVector4(Vector4 source) => FromScaledVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaDouble FromAssociatedScaledVector4(Vector4 source) + { + Numerics.UnPremultiply(ref source); + return FromScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaDouble FromUnassociatedVector4(Vector4 source) => FromVector4(source); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RgbaDouble FromAssociatedVector4(Vector4 source) => FromAssociatedScaledVector4(source); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RgbaDouble FromScaledVector4(Vector4 source) => FromVector4(source); diff --git a/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs b/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs new file mode 100644 index 000000000..8a38ea11f --- /dev/null +++ b/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +namespace SixLabors.ImageSharp.Tests.ColorProfiles; + +/// +/// Tests the class. +/// +[Trait("Color", "Conversion")] +public class RgbWorkingSpaceTests +{ + private static readonly ApproximateFloatComparer TolerantComparer = new(1e-5F); + + public static readonly TheoryData WorkingSpaces = new() + { + KnownRgbWorkingSpaces.SRgb, + KnownRgbWorkingSpaces.SRgbSimplified, + KnownRgbWorkingSpaces.Rec709, + KnownRgbWorkingSpaces.Rec2020, + KnownRgbWorkingSpaces.ECIRgbv2, + KnownRgbWorkingSpaces.AdobeRgb1998, + KnownRgbWorkingSpaces.ApplesRgb, + KnownRgbWorkingSpaces.BestRgb, + KnownRgbWorkingSpaces.BetaRgb, + KnownRgbWorkingSpaces.BruceRgb, + KnownRgbWorkingSpaces.CIERgb, + KnownRgbWorkingSpaces.ColorMatchRgb, + KnownRgbWorkingSpaces.DonRgb4, + KnownRgbWorkingSpaces.EktaSpacePS5, + KnownRgbWorkingSpaces.NTSCRgb, + KnownRgbWorkingSpaces.PALSECAMRgb, + KnownRgbWorkingSpaces.ProPhotoRgb, + KnownRgbWorkingSpaces.SMPTECRgb, + KnownRgbWorkingSpaces.WideGamutRgb + }; + + [Fact] + public void RgbWorkingSpaceEqualityRequiresSameConcreteType() + { + RgbWorkingSpace sRgb = KnownRgbWorkingSpaces.SRgb; + RgbWorkingSpace sRgbSimplified = KnownRgbWorkingSpaces.SRgbSimplified; + RgbWorkingSpace rec709 = KnownRgbWorkingSpaces.Rec709; + + Assert.False(sRgb.Equals(sRgbSimplified)); + Assert.False(sRgbSimplified.Equals(sRgb)); + Assert.False(sRgb.Equals(rec709)); + Assert.False(rec709.Equals(sRgb)); + + Assert.NotEqual(sRgb.GetHashCode(), sRgbSimplified.GetHashCode()); + Assert.NotEqual(sRgb.GetHashCode(), rec709.GetHashCode()); + } + + [Fact] + public void RgbWorkingSpaceEqualityMatchesSameConcreteTypeValues() + { + RgbWorkingSpace x = new SRgbWorkingSpace( + KnownRgbWorkingSpaces.SRgb.WhitePoint, + KnownRgbWorkingSpaces.SRgb.ChromaticityCoordinates); + + RgbWorkingSpace y = new SRgbWorkingSpace( + KnownRgbWorkingSpaces.SRgb.WhitePoint, + KnownRgbWorkingSpaces.SRgb.ChromaticityCoordinates); + + Assert.Equal(x, y); + Assert.Equal(x.GetHashCode(), y.GetHashCode()); + } + + [Fact] + public void GammaWorkingSpaceEqualityIncludesGamma() + { + GammaWorkingSpace x = new( + 2.2F, + KnownRgbWorkingSpaces.SRgbSimplified.WhitePoint, + KnownRgbWorkingSpaces.SRgbSimplified.ChromaticityCoordinates); + + GammaWorkingSpace y = new( + 1.8F, + KnownRgbWorkingSpaces.SRgbSimplified.WhitePoint, + KnownRgbWorkingSpaces.SRgbSimplified.ChromaticityCoordinates); + + Assert.NotEqual(x, y); + Assert.NotEqual(x.GetHashCode(), y.GetHashCode()); + } + + [Theory] + [MemberData(nameof(WorkingSpaces))] + public void CompressAndExpand_RoundTripsWithTolerance(RgbWorkingSpace workingSpace) + { + Vector4[] linear = + [ + new(0F, .001F, .18F, 1F), // Endpoint, below the sRGB breakpoint, common middle gray, and opaque alpha. + new(.0031308F, .25F, .5F, .75F), // sRGB linear/gamma breakpoint with interior values and partial alpha. + new(.75F, .5F, .25F, .5F), // Reversed interior values ensure channel order does not hide errors. + new(1F, .8F, .2F, .25F) // Upper endpoint, high/low interior values, and low alpha. + ]; + + Vector4[] expectedCompressed = new Vector4[linear.Length]; + + for (int i = 0; i < linear.Length; i++) + { + Vector4 compressed = workingSpace.Compress(linear[i]); + Vector4 expanded = workingSpace.Expand(compressed); + + expectedCompressed[i] = compressed; + + Assert.Equal(linear[i], expanded, TolerantComparer); + } + + Vector4[] actualCompressed = linear.ToArray(); + workingSpace.Compress(actualCompressed); + + Assert.Equal(expectedCompressed, actualCompressed, TolerantComparer); + + workingSpace.Expand(actualCompressed); + + Assert.Equal(linear, actualCompressed, TolerantComparer); + } +} diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs index 81daac3e0..cbe2e4066 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs @@ -292,7 +292,7 @@ public partial class SimdUtilsTests FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, count, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } [Theory] @@ -303,50 +303,15 @@ public partial class SimdUtilsTests { int size = FeatureTestRunner.Deserialize(serialized); - // These cannot be expressed as a theory as you cannot - // use RemoteExecutor within generic methods nor pass - // IShuffle4 to the generic utils method. - WXYZShuffle4 wxyz = default; - TestShuffleByte4Channel( - size, - (s, d) => SimdUtils.Shuffle4(s.Span, d.Span, wxyz), - SimdUtils.Shuffle.MMShuffle2103); - - WZYXShuffle4 wzyx = default; - TestShuffleByte4Channel( - size, - (s, d) => SimdUtils.Shuffle4(s.Span, d.Span, wzyx), - SimdUtils.Shuffle.MMShuffle0123); + TestShuffleByte4Channel(size, (s, d) => SimdUtils.Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle2103); - YZWXShuffle4 yzwx = default; - TestShuffleByte4Channel( - size, - (s, d) => SimdUtils.Shuffle4(s.Span, d.Span, yzwx), - SimdUtils.Shuffle.MMShuffle0321); - - ZYXWShuffle4 zyxw = default; - TestShuffleByte4Channel( - size, - (s, d) => SimdUtils.Shuffle4(s.Span, d.Span, zyxw), - SimdUtils.Shuffle.MMShuffle3012); + TestShuffleByte4Channel(size, (s, d) => SimdUtils.Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle0123); - DefaultShuffle4 xwyz = new(SimdUtils.Shuffle.MMShuffle2130); - TestShuffleByte4Channel( - size, - (s, d) => SimdUtils.Shuffle4(s.Span, d.Span, xwyz), - xwyz.Control); + TestShuffleByte4Channel(size, (s, d) => SimdUtils.Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle0321); - DefaultShuffle4 yyyy = new(SimdUtils.Shuffle.MMShuffle1111); - TestShuffleByte4Channel( - size, - (s, d) => SimdUtils.Shuffle4(s.Span, d.Span, yyyy), - yyyy.Control); + TestShuffleByte4Channel(size, (s, d) => SimdUtils.Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle3012); - DefaultShuffle4 wwww = new(SimdUtils.Shuffle.MMShuffle3333); - TestShuffleByte4Channel( - size, - (s, d) => SimdUtils.Shuffle4(s.Span, d.Span, wwww), - wwww.Control); + TestShuffleByte4Channel(size, (s, d) => SimdUtils.Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle1230); } FeatureTestRunner.RunWithHwIntrinsicsFeature( @@ -363,32 +328,7 @@ public partial class SimdUtilsTests { int size = FeatureTestRunner.Deserialize(serialized); - // These cannot be expressed as a theory as you cannot - // use RemoteExecutor within generic methods nor pass - // IShuffle3 to the generic utils method. - DefaultShuffle3 zyx = new(SimdUtils.Shuffle.MMShuffle3012); - TestShuffleByte3Channel( - size, - (s, d) => SimdUtils.Shuffle3(s.Span, d.Span, zyx), - zyx.Control); - - DefaultShuffle3 xyz = new(SimdUtils.Shuffle.MMShuffle3210); - TestShuffleByte3Channel( - size, - (s, d) => SimdUtils.Shuffle3(s.Span, d.Span, xyz), - xyz.Control); - - DefaultShuffle3 yyy = new(SimdUtils.Shuffle.MMShuffle3111); - TestShuffleByte3Channel( - size, - (s, d) => SimdUtils.Shuffle3(s.Span, d.Span, yyy), - yyy.Control); - - DefaultShuffle3 zzz = new(SimdUtils.Shuffle.MMShuffle3222); - TestShuffleByte3Channel( - size, - (s, d) => SimdUtils.Shuffle3(s.Span, d.Span, zzz), - zzz.Control); + TestShuffleByte3Channel(size, (s, d) => SimdUtils.Shuffle3(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle3012); } FeatureTestRunner.RunWithHwIntrinsicsFeature( @@ -405,32 +345,13 @@ public partial class SimdUtilsTests { int size = FeatureTestRunner.Deserialize(serialized); - // These cannot be expressed as a theory as you cannot - // use RemoteExecutor within generic methods nor pass - // IPad3Shuffle4 to the generic utils method. - XYZWPad3Shuffle4 xyzw = default; - TestPad3Shuffle4Channel( - size, - (s, d) => SimdUtils.Pad3Shuffle4(s.Span, d.Span, xyzw), - SimdUtils.Shuffle.MMShuffle3210); + TestPad3Shuffle4Channel(size, (s, d) => SimdUtils.Pad3Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle3210); - DefaultPad3Shuffle4 xwyz = new(SimdUtils.Shuffle.MMShuffle2130); - TestPad3Shuffle4Channel( - size, - (s, d) => SimdUtils.Pad3Shuffle4(s.Span, d.Span, xwyz), - xwyz.Control); + TestPad3Shuffle4Channel(size, (s, d) => SimdUtils.Pad3Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle2103); - DefaultPad3Shuffle4 yyyy = new(SimdUtils.Shuffle.MMShuffle1111); - TestPad3Shuffle4Channel( - size, - (s, d) => SimdUtils.Pad3Shuffle4(s.Span, d.Span, yyyy), - yyyy.Control); + TestPad3Shuffle4Channel(size, (s, d) => SimdUtils.Pad3Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle0123); - DefaultPad3Shuffle4 wwww = new(SimdUtils.Shuffle.MMShuffle3333); - TestPad3Shuffle4Channel( - size, - (s, d) => SimdUtils.Pad3Shuffle4(s.Span, d.Span, wwww), - wwww.Control); + TestPad3Shuffle4Channel(size, (s, d) => SimdUtils.Pad3Shuffle4(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle3012); } FeatureTestRunner.RunWithHwIntrinsicsFeature( @@ -447,38 +368,19 @@ public partial class SimdUtilsTests { int size = FeatureTestRunner.Deserialize(serialized); - // These cannot be expressed as a theory as you cannot - // use RemoteExecutor within generic methods nor pass - // IShuffle4Slice3 to the generic utils method. - XYZWShuffle4Slice3 xyzw = default; - TestShuffle4Slice3Channel( - size, - (s, d) => SimdUtils.Shuffle4Slice3(s.Span, d.Span, xyzw), - SimdUtils.Shuffle.MMShuffle3210); + TestShuffle4Slice3Channel(size, (s, d) => SimdUtils.Shuffle4Slice3(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle3210); - DefaultShuffle4Slice3 xwyz = new(SimdUtils.Shuffle.MMShuffle2130); - TestShuffle4Slice3Channel( - size, - (s, d) => SimdUtils.Shuffle4Slice3(s.Span, d.Span, xwyz), - xwyz.Control); + TestShuffle4Slice3Channel(size, (s, d) => SimdUtils.Shuffle4Slice3(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle0321); - DefaultShuffle4Slice3 yyyy = new(SimdUtils.Shuffle.MMShuffle1111); - TestShuffle4Slice3Channel( - size, - (s, d) => SimdUtils.Shuffle4Slice3(s.Span, d.Span, yyyy), - yyyy.Control); + TestShuffle4Slice3Channel(size, (s, d) => SimdUtils.Shuffle4Slice3(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle0123); - DefaultShuffle4Slice3 wwww = new(SimdUtils.Shuffle.MMShuffle3333); - TestShuffle4Slice3Channel( - size, - (s, d) => SimdUtils.Shuffle4Slice3(s.Span, d.Span, wwww), - wwww.Control); + TestShuffle4Slice3Channel(size, (s, d) => SimdUtils.Shuffle4Slice3(s.Span, d.Span), SimdUtils.Shuffle.MMShuffle3012); } FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, count, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } private static void TestShuffleFloat4Channel( diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs index e39f9456f..71335032c 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs @@ -3,8 +3,10 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit.Abstractions; @@ -133,7 +135,7 @@ public partial class SimdUtilsTests FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, count, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2); + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX); } [Theory] @@ -142,17 +144,41 @@ public partial class SimdUtilsTests count, (s, d) => SimdUtils.ByteToNormalizedFloat(s.Span, d.Span)); + [Fact] + public void VectorMultiplyAddUsesRuntimeOrderAndFusedContract() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunVectorMultiplyAddUsesRuntimeOrderAndFusedContract, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void RunVectorMultiplyAddUsesRuntimeOrderAndFusedContract() + { + Assert.Equal(Vector128.Create(11F), Vector128_.MultiplyAddEstimate(Vector128.Create(2F), Vector128.Create(3F), Vector128.Create(5F))); + Assert.Equal(Vector256.Create(11F), Vector256_.MultiplyAddEstimate(Vector256.Create(2F), Vector256.Create(3F), Vector256.Create(5F))); + Assert.Equal(Vector512.Create(11F), Vector512_.MultiplyAddEstimate(Vector512.Create(2F), Vector512.Create(3F), Vector512.Create(5F))); + + // These associated-alpha components produce an exact midpoint that a separate multiply and add rounds incorrectly. + float left = (68F / byte.MaxValue) * .625F; + float right = 1F - (160F / byte.MaxValue); + float addend = ((50F / byte.MaxValue) + (50F / byte.MaxValue)) * left; + float expected = MathF.FusedMultiplyAdd(left, right, addend); + + Assert.Equal(Vector128.Create(expected), Vector128_.FusedMultiplyAdd(Vector128.Create(left), Vector128.Create(right), Vector128.Create(addend))); + Assert.Equal(Vector256.Create(expected), Vector256_.FusedMultiplyAdd(Vector256.Create(left), Vector256.Create(right), Vector256.Create(addend))); + Assert.Equal(Vector512.Create(expected), Vector512_.FusedMultiplyAdd(Vector512.Create(left), Vector512.Create(right), Vector512.Create(addend))); + } + private static void TestImpl_BulkConvertByteToNormalizedFloat( int count, Action, Memory> convert) { - byte[] source = new Random(count).GenerateRandomByteArray(count); + byte[] source = Enumerable.Range(0, count).Select(i => (byte)i).ToArray(); float[] result = new float[count]; - float[] expected = source.Select(b => b / 255f).ToArray(); + // A double-precision oracle keeps this expectation independent from either production implementation. + float[] expected = source.Select(b => (float)(b / (double)byte.MaxValue)).ToArray(); convert(source, result); - Assert.Equal(expected, result, new ApproximateFloatComparer(1e-5f)); + Assert.Equal(expected, result); } [Theory] @@ -171,7 +197,7 @@ public partial class SimdUtilsTests FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, count, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2); + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2); } [Theory] @@ -193,6 +219,45 @@ public partial class SimdUtilsTests } } + [Fact] + public void BulkConvertNormalizedFloatToByteRoundsMidpointsAwayFromZero() + { + float[] midpointValues = Enumerable.Range(0, byte.MaxValue).Select(x => (x + 0.5F) / byte.MaxValue).ToArray(); + float[] source = new float[1024]; + byte[] expected = new byte[source.Length]; + byte[] actual = new byte[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + int value = i % midpointValues.Length; + source[i] = midpointValues[value]; + expected[i] = (byte)(value + 1); + } + + SimdUtils.NormalizedFloatToByteSaturate(source, actual); + + Assert.Equal(expected, actual); + } + + [Fact] + public void BulkConvertFloatToByteRoundsMidpointsAwayFromZeroAndClampsOverflows() + { + float[] source = new float[1027]; + byte[] expected = new byte[source.Length]; + byte[] actual = new byte[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + float value = (i % 258) - .5F; + source[i] = value; + expected[i] = (byte)Math.Min(byte.MaxValue, Math.Max(0, value + .5F)); + } + + SimdUtils.FloatToByteSaturate(source, actual); + + Assert.Equal(expected, actual); + } + [Theory] [MemberData(nameof(ArbitraryArraySizes))] public void PackFromRgbPlanes_Rgb24(int count) => TestPackFromRgbPlanes( diff --git a/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs b/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs new file mode 100644 index 000000000..b2f4dfe76 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs @@ -0,0 +1,580 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Common; + +public class TensorPrimitivesTests +{ + private static readonly int[] SpanLengthValues = + [ + 0, + 1, + 3, + 4, + 5, + 7, + 8, + 9, + 15, + 16, + 17, + 31, + 32, + 33, + 63, + 64, + 65, + 127, + 128, + 129, + 2048 + ]; + + /// + /// Gets lengths that exercise scalar execution, every SIMD width, overlapping tails, and the unrolled loop. + /// + public static TheoryData SpanLengths => new(SpanLengthValues); + + /// + /// Verifies every compatibility operation while forcing the supported SIMD feature tiers in isolated processes. + /// + [Fact] + public void OperationsMatchScalarFormulasAcrossHardwareIntrinsicFeatures() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunOperationsAcrossHardwareIntrinsicFeatures, + HwIntrinsics.AllowAll + | HwIntrinsics.DisableAVX512F + | HwIntrinsics.DisableAVX + | HwIntrinsics.DisableArm64Sve + | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies that span-to-span operations require equal input lengths before accessing either input. + /// + [Fact] + public void AddSpanSpanRejectsMismatchedInputLengths() + { + ArgumentException exception = Assert.Throws( + () => TensorPrimitives_.Add(new int[4], new int[3], new int[4])); + + Assert.Null(exception.ParamName); + } + + /// + /// Verifies that every operation rejects a destination that cannot hold all input elements. + /// + [Fact] + public void OperationsRejectShortDestinations() + { + int[] integers = new int[4]; + float[] singles = new float[4]; + + Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Add(integers, integers, new int[3])).ParamName); + Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Add(integers, 1, new int[3])).ParamName); + Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Multiply(singles, 2F, new float[3])).ParamName); + Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Divide(singles, 2F, new float[3])).ParamName); + Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Max(singles, 2F, new float[3])).ParamName); + Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Clamp(singles, 0F, 1F, new float[3])).ParamName); + Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Negate(singles, new float[3])).ParamName); + } + + /// + /// Verifies that every operation rejects shifted input and destination overlap. + /// + [Fact] + public void OperationsRejectShiftedOverlap() + { + int[] integers = new int[5]; + int[] separateIntegers = new int[4]; + float[] singles = new float[5]; + + // Both inputs need independent validation because either one may alias a shifted destination. + Assert.Equal( + "destination", + Assert.Throws( + () => TensorPrimitives_.Add(integers.AsSpan(0, 4), separateIntegers, integers.AsSpan(1, 4))).ParamName); + + Assert.Equal( + "destination", + Assert.Throws( + () => TensorPrimitives_.Add(separateIntegers, integers.AsSpan(0, 4), integers.AsSpan(1, 4))).ParamName); + + Assert.Equal( + "destination", + Assert.Throws( + () => TensorPrimitives_.Add(integers.AsSpan(0, 4), 1, integers.AsSpan(1, 4))).ParamName); + + Assert.Equal( + "destination", + Assert.Throws( + () => TensorPrimitives_.Multiply(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName); + + Assert.Equal( + "destination", + Assert.Throws( + () => TensorPrimitives_.Divide(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName); + + Assert.Equal( + "destination", + Assert.Throws( + () => TensorPrimitives_.Max(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName); + + Assert.Equal( + "destination", + Assert.Throws( + () => TensorPrimitives_.Clamp(singles.AsSpan(0, 4), 0F, 1F, singles.AsSpan(1, 4))).ParamName); + + Assert.Equal( + "destination", + Assert.Throws( + () => TensorPrimitives_.Negate(singles.AsSpan(0, 4), singles.AsSpan(1, 4))).ParamName); + } + + /// + /// Runs the TensorPrimitives compatibility assertions inside a process configured for one hardware-intrinsic tier. + /// + private static void RunOperationsAcrossHardwareIntrinsicFeatures() + { + TensorPrimitivesTests tests = new(); + + // Reuse the focused assertions so the remote feature matrix cannot drift from the normal test coverage. + foreach (int length in SpanLengthValues) + { + tests.AddByteMatchesScalarFormula(length); + tests.AddUInt32MatchesScalarFormula(length); + tests.AddScalarInt32MatchesScalarFormula(length); + tests.NegateSingleMatchesScalarFormula(length); + tests.NegateDoubleMatchesScalarFormula(length); + tests.ClampInt32MatchesScalarFormula(length); + tests.ClampSingleMatchesRuntimeFormula(length); + tests.DivideSingleMatchesScalarFormula(length); + tests.MaxSingleMatchesRuntimeFormula(length); + tests.MultiplySingleMatchesScalarFormula(length); + tests.NormalizeMatchesScalarFormula(length); + } + + tests.ClampSinglePreservesRuntimeSpecialValueSemantics(); + tests.ClampDoublePreservesRuntimeSpecialValueSemantics(); + } + + /// + /// Verifies that byte addition wraps modulo 256 and supports either input as the in-place destination. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void AddByteMatchesScalarFormula(int length) + { + byte[] x = new byte[length]; + byte[] y = new byte[length]; + byte[] expected = new byte[length]; + + for (int i = 0; i < length; i++) + { + x[i] = (byte)((i * 23) + 197); + y[i] = (byte)((i * 41) + 113); + expected[i] = unchecked((byte)(x[i] + y[i])); + } + + byte[] destination = new byte[length]; + TensorPrimitives_.Add(x, y, destination); + Assert.Equal(expected, destination); + + byte[] xInPlace = (byte[])x.Clone(); + TensorPrimitives_.Add(xInPlace, y, xInPlace); + Assert.Equal(expected, xInPlace); + + byte[] yInPlace = (byte[])y.Clone(); + TensorPrimitives_.Add(x, yInPlace, yInPlace); + Assert.Equal(expected, yInPlace); + } + + /// + /// Verifies that unsigned integer addition preserves unchecked histogram accumulation semantics. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void AddUInt32MatchesScalarFormula(int length) + { + uint[] x = new uint[length]; + uint[] y = new uint[length]; + uint[] expected = new uint[length]; + + for (int i = 0; i < length; i++) + { + x[i] = ((uint)i * 1_234_567U) + 0xF0000000U; + y[i] = ((uint)i * 7_654_321U) + 0x30000000U; + expected[i] = unchecked(x[i] + y[i]); + } + + TensorPrimitives_.Add(x, y, x); + Assert.Equal(expected, x); + } + + /// + /// Verifies that scalar integer addition produces identical results for separate and in-place destinations. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void AddScalarInt32MatchesScalarFormula(int length) + { + int[] source = new int[length]; + int[] expected = new int[length]; + const int addend = 17; + + for (int i = 0; i < length; i++) + { + source[i] = (i * 37) - 200; + expected[i] = source[i] + addend; + } + + int[] destination = new int[length]; + TensorPrimitives_.Add(source, addend, destination); + Assert.Equal(expected, destination); + + int[] inPlace = (int[])source.Clone(); + TensorPrimitives_.Add(inPlace, addend, inPlace); + Assert.Equal(expected, inPlace); + } + + /// + /// Verifies that floating-point negation preserves the scalar operator's exact bit-level behavior. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void NegateSingleMatchesScalarFormula(int length) + { + float[] values = + { + float.NaN, + -0F, + 0F, + -1F, + 1F, + float.NegativeInfinity, + float.PositiveInfinity + }; + + float[] source = new float[length]; + float[] expected = new float[length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = values[i % values.Length]; + expected[i] = -source[i]; + } + + float[] destination = new float[length]; + TensorPrimitives_.Negate(source, destination); + AssertSingleBitsEqual(expected, destination); + + TensorPrimitives_.Negate(source, source); + AssertSingleBitsEqual(expected, source); + } + + /// + /// Verifies that double-precision negation preserves the scalar operator's exact bit-level behavior. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void NegateDoubleMatchesScalarFormula(int length) + { + double[] values = + { + double.NaN, + -0D, + 0D, + -1D, + 1D, + double.NegativeInfinity, + double.PositiveInfinity + }; + + double[] source = new double[length]; + double[] expected = new double[length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = values[i % values.Length]; + expected[i] = -source[i]; + } + + double[] destination = new double[length]; + TensorPrimitives_.Negate(source, destination); + AssertDoubleBitsEqual(expected, destination); + + TensorPrimitives_.Negate(source, source); + AssertDoubleBitsEqual(expected, source); + } + + /// + /// Verifies that integer clamping produces identical results for separate and in-place destinations. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void ClampInt32MatchesScalarFormula(int length) + { + int[] source = new int[length]; + int[] expected = new int[length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = ((i * 37) % 401) - 200; + expected[i] = Math.Clamp(source[i], -73, 91); + } + + int[] destination = new int[length]; + TensorPrimitives_.Clamp(source, -73, 91, destination); + Assert.Equal(expected, destination); + + int[] inPlace = (int[])source.Clone(); + TensorPrimitives_.Clamp(inPlace, -73, 91, inPlace); + Assert.Equal(expected, inPlace); + } + + /// + /// Verifies that floating-point clamping matches the runtime tensor formula for special values and unordered bounds. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void ClampSingleMatchesRuntimeFormula(int length) + { + float[] values = + { + float.NaN, + -0F, + 0F, + -1F, + 1F, + float.NegativeInfinity, + float.PositiveInfinity + }; + + float[] source = new float[length]; + float[] expected = new float[length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = values[i % values.Length]; + + // Runtime main follows Min(Max(x, min), max) for vectorizable types, including unordered bounds. + expected[i] = float.Min(float.Max(source[i], 2F), -2F); + } + + TensorPrimitives_.Clamp(source, 2F, -2F, source); + AssertSingleBitsEqual(expected, source); + } + + /// + /// Verifies that single-precision clamping preserves the runtime's signed-zero and NaN behavior. + /// + [Fact] + public void ClampSinglePreservesRuntimeSpecialValueSemantics() + { + float[] values = + { + float.NaN, + float.NegativeInfinity, + -0F, + 0F, + float.PositiveInfinity + }; + + float[] actual = new float[129]; + float[] expected = new float[actual.Length]; + + for (int i = 0; i < actual.Length; i++) + { + actual[i] = values[i % values.Length]; + expected[i] = float.Min(float.Max(actual[i], -0F), 0F); + } + + TensorPrimitives_.Clamp(actual, -0F, 0F, actual); + AssertSingleBitsEqual(expected, actual); + } + + /// + /// Verifies that double-precision clamping preserves the runtime's signed-zero and NaN behavior. + /// + [Fact] + public void ClampDoublePreservesRuntimeSpecialValueSemantics() + { + double[] values = + { + double.NaN, + double.NegativeInfinity, + -0D, + 0D, + double.PositiveInfinity + }; + + double[] actual = new double[65]; + double[] expected = new double[actual.Length]; + + for (int i = 0; i < actual.Length; i++) + { + actual[i] = values[i % values.Length]; + expected[i] = double.Min(double.Max(actual[i], -0D), 0D); + } + + TensorPrimitives_.Clamp(actual, -0D, 0D, actual); + AssertDoubleBitsEqual(expected, actual); + } + + /// + /// Verifies that division produces identical results for separate and in-place destinations. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void DivideSingleMatchesScalarFormula(int length) + { + float[] source = new float[length]; + float[] expected = new float[length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = (i - 65.25F) * 1.75F; + expected[i] = source[i] / 3.25F; + } + + float[] destination = new float[length]; + TensorPrimitives_.Divide(source, 3.25F, destination); + AssertSingleBitsEqual(expected, destination); + + float[] inPlace = (float[])source.Clone(); + TensorPrimitives_.Divide(inPlace, 3.25F, inPlace); + AssertSingleBitsEqual(expected, inPlace); + } + + /// + /// Verifies that maximum selection preserves the runtime's NaN and signed-zero semantics. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void MaxSingleMatchesRuntimeFormula(int length) + { + float[] values = + { + float.NaN, + float.NegativeInfinity, + -1F, + -0F, + 0F, + 1F, + float.PositiveInfinity + }; + + float[] actual = new float[length]; + float[] expected = new float[length]; + + for (int i = 0; i < length; i++) + { + actual[i] = values[i % values.Length]; + expected[i] = float.Max(actual[i], -0F); + } + + TensorPrimitives_.Max(actual, -0F, actual); + AssertSingleBitsEqual(expected, actual); + } + + /// + /// Verifies that multiplication produces identical results for separate and in-place destinations. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void MultiplySingleMatchesScalarFormula(int length) + { + float[] source = new float[length]; + float[] expected = new float[length]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = (i - 65.25F) * 1.75F; + expected[i] = source[i] * 0.375F; + } + + float[] destination = new float[length]; + TensorPrimitives_.Multiply(source, 0.375F, destination); + AssertSingleBitsEqual(expected, destination); + + TensorPrimitives_.Multiply(source, 0.375F, source); + AssertSingleBitsEqual(expected, source); + } + + /// + /// Verifies that the normalization compatibility call preserves its element-wise division contract. + /// + /// The input length. + [Theory] + [MemberData(nameof(SpanLengths))] + public void NormalizeMatchesScalarFormula(int length) + { + float[] actual = new float[length]; + float[] expected = new float[length]; + + for (int i = 0; i < actual.Length; i++) + { + actual[i] = (i + 1) * 0.125F; + expected[i] = actual[i] / 7.5F; + } + + Numerics.Normalize(actual, 7.5F); + AssertSingleBitsEqual(expected, actual); + } + + /// + /// Compares floating-point results while preserving signed-zero behavior. + /// + /// The expected values. + /// The actual values. + private static void AssertSingleBitsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + + for (int i = 0; i < expected.Length; i++) + { + if (float.IsNaN(expected[i])) + { + Assert.True(float.IsNaN(actual[i])); + } + else + { + Assert.Equal(BitConverter.SingleToInt32Bits(expected[i]), BitConverter.SingleToInt32Bits(actual[i])); + } + } + } + + /// + /// Compares double-precision results while preserving signed-zero behavior. + /// + /// The expected values. + /// The actual values. + private static void AssertDoubleBitsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + + for (int i = 0; i < expected.Length; i++) + { + if (double.IsNaN(expected[i])) + { + Assert.True(double.IsNaN(actual[i])); + } + else + { + Assert.Equal(BitConverter.DoubleToInt64Bits(expected[i]), BitConverter.DoubleToInt64Bits(actual[i])); + } + } + } +} diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 3c6c759f8..bd6520084 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -20,7 +20,7 @@ public class ConfigurationTests public Configuration DefaultConfiguration { get; } - private readonly int expectedDefaultConfigurationCount = 11; + private readonly int expectedDefaultConfigurationCount = 12; public ConfigurationTests() { diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 94cfe85ee..e85c6bcdf 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -91,13 +91,22 @@ public class BmpDecoderTests { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToReferenceOutput(provider); } [Theory] [WithFile(Bit16Inverted, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecode_16Bit_Inverted(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(BmpDecoder.Instance); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + + [Theory] [WithFile(Bit8Inverted, PixelTypes.Rgba32)] - public void BmpDecoder_CanDecode_Inverted(TestImageProvider provider) + public void BmpDecoder_CanDecode_8Bit_Inverted(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(BmpDecoder.Instance); @@ -156,7 +165,7 @@ public class BmpDecoderTests { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder(BmpFormat.Instance)); } [Theory] @@ -186,12 +195,12 @@ public class BmpDecoderTests public void BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta(TestImageProvider provider) where TPixel : unmanaged, IPixel { - RleSkippedPixelHandling skippedPixelHandling = TestEnvironment.IsWindows ? RleSkippedPixelHandling.Black : RleSkippedPixelHandling.FirstColorOfPalette; + RleSkippedPixelHandling skippedPixelHandling = RleSkippedPixelHandling.Black; BmpDecoderOptions options = new() { RleSkippedPixelHandling = skippedPixelHandling }; using Image image = provider.GetImage(BmpDecoder.Instance, options); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToReferenceOutput(provider); } [Theory] @@ -224,8 +233,8 @@ public class BmpDecoderTests } } + // An RLE-compressed image that uses “delta” codes, to skip over some pixels. [Theory] - [WithFile(RLE8Cut, PixelTypes.Rgba32)] [WithFile(RLE8Delta, PixelTypes.Rgba32)] public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_MagickRefDecoder(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -236,11 +245,21 @@ public class BmpDecoderTests image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } + // An RLE-compressed image that uses “delta” codes, and early EOL & EOBMP markers, to skip over some pixels. + [Theory] + [WithFile(RLE8Cut, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette }; + using Image image = provider.GetImage(BmpDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + [Theory] [WithFile(RLE8, PixelTypes.Rgba32, false)] - [WithFile(RLE8Inverted, PixelTypes.Rgba32, false)] [WithFile(RLE8, PixelTypes.Rgba32, true)] - [WithFile(RLE8Inverted, PixelTypes.Rgba32, true)] public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider provider, bool enforceDiscontiguousBuffers) where TPixel : unmanaged, IPixel { @@ -255,6 +274,25 @@ public class BmpDecoderTests image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } + [Theory] + [WithFile(RLE8Inverted, PixelTypes.Rgba32, false)] + [WithFile(RLE8Inverted, PixelTypes.Rgba32, true)] + public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted(TestImageProvider provider, bool enforceDiscontiguousBuffers) + where TPixel : unmanaged, IPixel + { + if (enforceDiscontiguousBuffers) + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(400); + } + + BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette }; + using Image image = provider.GetImage(BmpDecoder.Instance, options); + image.DebugSave(provider); + + // The Reference decoder does not support decoding compressed bmp which are inverted (with negative height). + image.CompareToReferenceOutput(provider); + } + [Theory] [WithFile(RLE24, PixelTypes.Rgba32, false)] [WithFile(RLE24Cut, PixelTypes.Rgba32, false)] @@ -571,4 +609,56 @@ public class BmpDecoderTests }); Assert.IsType(ex.InnerException); } + + // https://github.com/SixLabors/ImageSharp/issues/3067 + [Fact] + public void BmpDecoder_ThrowsException_Issue3067() + { + // Construct minimal BMP with bitsPerPixel = 0 + byte[] bmp = new byte[54]; + bmp[0] = (byte)'B'; + bmp[1] = (byte)'M'; + BitConverter.GetBytes(54).CopyTo(bmp, 2); + BitConverter.GetBytes(54).CopyTo(bmp, 10); + BitConverter.GetBytes(40).CopyTo(bmp, 14); + BitConverter.GetBytes(1).CopyTo(bmp, 18); + BitConverter.GetBytes(1).CopyTo(bmp, 22); + BitConverter.GetBytes((short)1).CopyTo(bmp, 26); + BitConverter.GetBytes((short)0).CopyTo(bmp, 28); // bitsPerPixel = 0 + + using MemoryStream stream = new(bmp); + + Assert.Throws(() => + { + using Image image = BmpDecoder.Instance.Decode(DecoderOptions.Default, stream); + }); + } + + // https://github.com/SixLabors/ImageSharp/issues/3074 + [Fact] + public void BmpDecoder_ThrowsException_Issue3074() + { + // Crafted BMP: pixel data offset = 0x7FFFFFFF, actual file = 35 bytes + byte[] data = + [ + 0x42, 0x4D, // "BM" signature + 0x3A, 0x00, 0x00, 0x00, // file size: 58 + 0x00, 0x00, 0x00, 0x00, // reserved + 0xFF, 0xFF, 0xFF, 0x7F, // pixel offset: 0x7FFFFFFF (2,147,483,647) + 0x28, 0x00, 0x00, 0x00, // DIB header size: 40 + 0x01, 0x00, 0x00, 0x00, // width: 1 + 0x01, 0xFF, 0x00, 0x00, // height: 65281 + 0x01, 0x00, // color planes: 1 + 0x08, 0x00, // bits per pixel: 8 + 0x00, 0x00, 0x00, 0x00, // compression: RGB + 0x00, 0x00, 0x00 // (truncated) + ]; + + using MemoryStream stream = new(data); + + Assert.Throws(() => + { + using Image image = Image.Load(stream); + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 5ebcc8bb9..6bd7e0103 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -292,7 +292,7 @@ public class BmpEncoderTests [Theory] [WithFile(Bit32Rgb, PixelTypes.Rgba32)] - public void Encode_8BitColor_WithOctreeQuantizer(TestImageProvider provider) + public void Encode_8BitColor_WithHexadecatreeQuantizer(TestImageProvider provider) where TPixel : unmanaged, IPixel { if (!TestEnvironment.Is64BitProcess) @@ -304,7 +304,7 @@ public class BmpEncoderTests BmpEncoder encoder = new() { BitsPerPixel = BmpBitsPerPixel.Bit8, - Quantizer = new OctreeQuantizer() + Quantizer = new HexadecatreeQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); @@ -385,7 +385,7 @@ public class BmpEncoderTests { BitsPerPixel = bitsPerPixel, SupportTransparency = false, - Quantizer = KnownQuantizers.Octree + Quantizer = KnownQuantizers.Hexadecatree }; image.SaveAsBmp(reencodedStream, encoder); reencodedStream.Seek(0, SeekOrigin.Begin); @@ -478,7 +478,7 @@ public class BmpEncoderTests { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency, - Quantizer = quantizer ?? KnownQuantizers.Octree + Quantizer = quantizer ?? KnownQuantizers.Hexadecatree }; // Does DebugSave & load reference CompareToReferenceInput(): diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs index 64564ae1d..bbfa4ac59 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs @@ -1,8 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using static SixLabors.ImageSharp.Tests.TestImages.Bmp; // ReSharper disable InconsistentNaming @@ -46,7 +49,7 @@ public class BmpMetadataTests } [Theory] - [WithFile(IccProfile, PixelTypes.Rgba32)] + [WithFile(TestImages.Bmp.IccProfile, PixelTypes.Rgba32)] public void Decoder_CanReadColorProfile(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -56,4 +59,40 @@ public class BmpMetadataTests Assert.NotNull(metaData.IccProfile); Assert.Equal(16, metaData.IccProfile.Entries.Length); } + + [Fact] + public void Identify_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault() + { + ImageInfo info = Image.Identify(CreateBmpWithMalformedIccProfile()); + Assert.Equal(1, info.Width); + Assert.Equal(1, info.Height); + } + + [Fact] + public void Decode_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault() + { + using Image image = Image.Load(CreateBmpWithMalformedIccProfile()); + Assert.Equal(1, image.Width); + Assert.Equal(1, image.Height); + } + + [Fact] + public void Identify_MalformedIccProfile_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => Image.Identify(options, CreateBmpWithMalformedIccProfile())); + } + + [Fact] + public void Decode_MalformedIccProfile_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => + { + using Image image = Image.Load(options, CreateBmpWithMalformedIccProfile()); + }); + } + + private static byte[] CreateBmpWithMalformedIccProfile() + => CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new BmpEncoder()); } diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs new file mode 100644 index 000000000..2d59c0235 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs @@ -0,0 +1,274 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +/// +/// Security regression tests for the EXR decoder (Findings EXR-1, EXR-2, EXR-3). +/// The EXR decoder was merged to main but not yet included in a tagged NuGet release. +/// Each test demonstrates a crafted-input crash present in the unfixed code. +/// +[Trait("Format", "Exr")] +[ValidateDisposedMemoryAllocations] +public class ExrDecoderSecurityTests +{ + /// + /// EXR-1 — EXR DataWindow Integer Overflow Produces Negative Image Dimensions (DoS) + /// + /// Width and Height are computed from attacker-controlled DataWindow attributes + /// using unchecked int subtraction: + /// this.Width = XMax - XMin + 1 // overflows to -2147483647 + /// + /// The negative Width is then passed to the Image<TPixel> constructor, which calls + /// Guard.MustBeGreaterThan(width, 0) → ArgumentOutOfRangeException. + /// + /// After a fix this should throw InvalidImageContentException instead. + /// + /// Affected file: + /// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 600–601 + /// + [Fact] + public void Decode_DataWindowOverflow_NegativeWidth_Throws() + { + // XMin = -1073741825, XMax = 1073741823 + // Width = 1073741823 - (-1073741825) + 1 = 2^31 + 1 → wraps to -2147483647 + byte[] data = BuildMinimalExr(xMin: -1073741825, yMin: 0, xMax: 1073741823, yMax: 0); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + /// + /// EXR-2 — EXR Row Offset Table Unvalidated Seek (DoS) + /// + /// Row offsets are read from the file and used unconditionally to seek the stream: + /// ulong rowOffset = this.ReadUnsignedLong(stream); + /// stream.Position = (long)rowOffset; // no bounds check + /// + /// A crafted offset of 0xFFFFFFFFFFFFFFFF casts to −1 as long, causing + /// an ArgumentOutOfRangeException when setting stream.Position. + /// + /// After a fix this should throw InvalidImageContentException instead. + /// + /// Affected file: + /// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 170–175 (scanline) + /// lines 243–248 (tile) + /// + [Fact] + public void Decode_CraftedRowOffsets_OutOfBounds_Throws() + { + // Valid 2×2 image (XMin=0,YMin=0,XMax=1,YMax=1 → Width=2,Height=2). + // Row offset table immediately follows the header null byte: + // 2 rows × 8 bytes each, all set to 0xFFFFFFFFFFFFFFFF. + byte[] invalidOffsets = new byte[16]; + BinaryPrimitives.WriteUInt64LittleEndian(invalidOffsets, 0xFFFFFFFFFFFFFFFF); + BinaryPrimitives.WriteUInt64LittleEndian(invalidOffsets.AsSpan(8), 0xFFFFFFFFFFFFFFFF); + + byte[] data = BuildMinimalExr( + xMin: 0, yMin: 0, xMax: 1, yMax: 1, + rowOffsetTableAppend: invalidOffsets); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + [Fact] + public void Decode_CraftedRowOffsets_IntoHeader_Throws() + { + // Offset 0 points back into the EXR file header and must be rejected + // before the decoder seeks to attacker-controlled non-pixel data. + byte[] headerOffsets = new byte[16]; + + byte[] data = BuildMinimalExr( + xMin: 0, yMin: 0, xMax: 1, yMax: 1, + rowOffsetTableAppend: headerOffsets); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + [Fact] + public void Decode_CraftedRowOffsets_IntoOffsetTable_Throws() + { + byte[] data = BuildMinimalExr( + xMin: 0, yMin: 0, xMax: 1, yMax: 1, + rowOffsetTableAppend: new byte[16]); + + // Point the first row offset at the second row offset entry. + BinaryPrimitives.WriteUInt64LittleEndian(data.AsSpan(data.Length - 16), (ulong)(data.Length - 8)); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + /// + /// EXR-3 — Oversized EXR RGBA row sizing is rejected as invalid image content. + /// + /// With 4 RGBA HALF channels and Width = 2^29, the decoded row staging and + /// bytes-per-row arithmetic both exceed the supported buffer sizing limits. + /// The decoder must reject this as InvalidImageContentException before any allocation. + /// + /// Affected file: + /// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 142–150, 215–223 + /// src/ImageSharp/Formats/Exr/ExrUtils.cs CalculateBytesPerRow + /// + [Fact] + public void Decode_RgbaRowSizingExceedsBufferLimits_Throws() + { + // 4 RGBA HALF channels at this width cannot be represented by the decoder's + // int-sized row staging or block buffers. + byte[] data = BuildMinimalRgbaExr(xMin: 0, yMin: 0, xMax: 536870911, yMax: 0); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + [Fact] + public void Decode_DataWindowWidthExceedsRowBufferLimit_Throws() + { + // A single HALF channel keeps bytesPerBlock below int.MaxValue, but the decoder + // still stages four color planes and must reject widths that overflow width × 4. + byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: int.MaxValue / 4, yMax: 0); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + [Fact] + public void Identify_RowOffsetTableExceedsStream_Throws() + { + // Identify parses the header only, so this verifies the offset table bound is + // validated before scanline decoding reads from the table. + byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: 1, yMax: 1); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Identify(DecoderOptions.Default, stream)); + } + + // ------------------------------------------------------------------------- + // Helpers: construct minimal valid-enough EXR scanline files. + // + // Required attributes per ParseHeaderAttributes validation: + // channels, compression, dataWindow, displayWindow, + // lineOrder, pixelAspectRatio, screenWindowCenter, screenWindowWidth + // ------------------------------------------------------------------------- + + private static byte[] BuildMinimalExr( + int xMin, int yMin, int xMax, int yMax, + byte[] rowOffsetTableAppend = null) + { + // channels: single "R" HALF channel with xSampling=1, ySampling=1 + // Layout per ReadChannelInfo: name\0 (2) + pixelType (4) + pLinear+reserved (4) + // + xSampling (4) + ySampling (4) = 18 bytes/channel + // + list-null (1) = 19 total + byte[] channelData = + [ + 0x52, 0x00, // "R\0" + 0x01, 0x00, 0x00, 0x00, // pixelType = Half (1) + 0x00, 0x00, 0x00, 0x00, // pLinear + 3 reserved bytes + 0x01, 0x00, 0x00, 0x00, // xSampling = 1 + 0x01, 0x00, 0x00, 0x00, // ySampling = 1 + 0x00, // channel-list null terminator + ]; + + return BuildExrWithChannels(xMin, yMin, xMax, yMax, channelData, rowOffsetTableAppend); + } + + private static byte[] BuildMinimalRgbaExr(int xMin, int yMin, int xMax, int yMax) + { + // 4 HALF channels in alphabetical order (A, B, G, R) per EXR spec. + // 18 bytes per channel × 4 channels + 1 list-null = 73 bytes total. + byte[] channelData = + [ + 0x41, 0x00, // "A\0" + 0x01, 0x00, 0x00, 0x00, // pixelType = Half (1) + 0x00, 0x00, 0x00, 0x00, // pLinear + 3 reserved bytes + 0x01, 0x00, 0x00, 0x00, // xSampling = 1 + 0x01, 0x00, 0x00, 0x00, // ySampling = 1 + 0x42, 0x00, // "B\0" + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x47, 0x00, // "G\0" + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x52, 0x00, // "R\0" + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x00, // channel-list null terminator + ]; + + return BuildExrWithChannels(xMin, yMin, xMax, yMax, channelData); + } + + private static byte[] BuildExrWithChannels( + int xMin, int yMin, int xMax, int yMax, + byte[] channelData, + byte[] rowOffsetTableAppend = null) + { + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms, System.Text.Encoding.ASCII, leaveOpen: true); + + // Magic (0x01312F76 LE) + version 2, scanline (flags = 0x00) + bw.Write(new byte[] { 0x76, 0x2F, 0x31, 0x01, 0x02, 0x00, 0x00, 0x00 }); + + void WriteAttr(string name, string type, byte[] payload) + { + foreach (char c in name) bw.Write((byte)c); + bw.Write((byte)0); + foreach (char c in type) bw.Write((byte)c); + bw.Write((byte)0); + bw.Write(payload.Length); + bw.Write(payload); + } + + WriteAttr("channels", "chlist", channelData); + + WriteAttr("compression", "compression", [0x00]); // None + + byte[] dw = new byte[16]; + BinaryPrimitives.WriteInt32LittleEndian(dw, xMin); + BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(4), yMin); + BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(8), xMax); + BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(12), yMax); + WriteAttr("dataWindow", "box2i", dw); + + WriteAttr("displayWindow", "box2i", new byte[16]); // all zeros (0,0,0,0) + + WriteAttr("lineOrder", "lineOrder", [0x00]); // IncreasingY + + byte[] aspect = new byte[4]; + BinaryPrimitives.WriteSingleLittleEndian(aspect, 1.0f); + WriteAttr("pixelAspectRatio", "float", aspect); + + WriteAttr("screenWindowCenter", "v2f", new byte[8]); // (0f, 0f) + + byte[] sww = new byte[4]; + BinaryPrimitives.WriteSingleLittleEndian(sww, 1.0f); + WriteAttr("screenWindowWidth", "float", sww); + + bw.Write((byte)0x00); // end-of-header sentinel + + if (rowOffsetTableAppend is not null) + bw.Write(rowOffsetTableAppend); + + return ms.ToArray(); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs new file mode 100644 index 000000000..1ee0cc582 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +[ValidateDisposedMemoryAllocations] +public class ExrDecoderTests +{ + private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.Exr; + + [Theory] + [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Half(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + ExrMetadata exrMetaData = image.Metadata.GetExrMetadata(); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + Assert.Equal(ExrPixelType.Half, exrMetaData.PixelType); + } + + [Theory] + [WithFile(TestImages.Exr.UncompressedFloatRgb, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Float(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + ExrMetadata exrMetaData = image.Metadata.GetExrMetadata(); + image.DebugSave(provider); + + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + Assert.Equal(ExrPixelType.Float, exrMetaData.PixelType); + } + + [Theory] + [WithFile(TestImages.Exr.UncompressedUintRgb, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + ExrMetadata exrMetaData = image.Metadata.GetExrMetadata(); + image.DebugSave(provider); + + // Compare to referene output, since the reference decoder does not support this pixel type. + image.CompareToReferenceOutput(provider); + Assert.Equal(ExrPixelType.UnsignedInt, exrMetaData.PixelType); + } + + [Theory] + [WithFile(TestImages.Exr.UintRgba, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Uint(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + ExrMetadata exrMetaData = image.Metadata.GetExrMetadata(); + image.DebugSave(provider); + + // Compare to referene output, since the reference decoder does not support this pixel type. + image.CompareToReferenceOutput(provider); + Assert.Equal(ExrPixelType.UnsignedInt, exrMetaData.PixelType); + } + + [Theory] + [WithFile(TestImages.Exr.Rgb, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Rgb(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.Gray, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Gray(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.Zip, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_ZipCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.Zips, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_ZipsCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.Rle, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_RunLengthCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.Pxr24Half, PixelTypes.Rgba32)] + [WithFile(TestImages.Exr.Pxr24Float, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Pxr24Compressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.Pxr24Uint, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + + // Compare to Reference here instead using the reference decoder, since uint pixel type is not supported by the Reference decoder. + image.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestImages.Exr.B44, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_B44Compressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + + // Note: There is a 0,1190% difference to the reference decoder. + image.CompareToOriginal(provider, ImageComparer.Tolerant(0.011f), ReferenceDecoder); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs new file mode 100644 index 000000000..2f507ea51 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +[ValidateDisposedMemoryAllocations] +public class ExrEncoderTests +{ + protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(ExrFormat.Instance); + + [Theory] + [InlineData(null, ExrPixelType.Half)] + [InlineData(ExrPixelType.Float, ExrPixelType.Float)] + [InlineData(ExrPixelType.Half, ExrPixelType.Half)] + [InlineData(ExrPixelType.UnsignedInt, ExrPixelType.UnsignedInt)] + public void EncoderOptions_SetPixelType_Works(ExrPixelType? pixelType, ExrPixelType? expectedPixelType) + { + // arrange + ExrEncoder exrEncoder = new() { PixelType = pixelType }; + using Image input = new Image(10, 10); + using MemoryStream memStream = new(); + + // act + input.Save(memStream, exrEncoder); + + // assert + memStream.Position = 0; + using Image output = Image.Load(memStream); + ExrMetadata exrMetaData = output.Metadata.GetExrMetadata(); + Assert.Equal(expectedPixelType, exrMetaData.PixelType); + } + + [Theory] + [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] + public void ExrEncoder_WithNoCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "NoCompression", compression: ExrCompression.None); + + [Theory] + [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] + public void ExrEncoder_WithZipCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "ZipCompression", compression: ExrCompression.Zip); + + [Theory] + [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] + public void ExrEncoder_WithZipsCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "ZipsCompression", compression: ExrCompression.Zips); + + protected static void TestExrEncoderCore( + TestImageProvider provider, + object testOutputDetails, + ExrCompression compression = ExrCompression.None, + bool useExactComparer = true, + float compareTolerance = 0.001f, + IImageDecoder imageDecoder = null) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + ExrEncoder encoder = new() + { + Compression = compression, + }; + + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder( + provider, + "exr", + testOutputDetails: testOutputDetails, + encoder: encoder, + customComparer: useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), + referenceDecoder: imageDecoder ?? ReferenceDecoder); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs new file mode 100644 index 000000000..cb527cf3b --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +public class ExrMetadataTests +{ + [Fact] + public void CloneIsDeep() + { + ExrMetadata meta = new() + { ImageDataType = ExrImageDataType.Rgb, PixelType = ExrPixelType.Half, Compression = ExrCompression.None }; + ExrMetadata clone = (ExrMetadata)meta.DeepClone(); + + clone.ImageDataType = ExrImageDataType.Gray; + clone.PixelType = ExrPixelType.Float; + clone.Compression = ExrCompression.Zip; + + Assert.False(meta.ImageDataType.Equals(clone.ImageDataType)); + Assert.False(meta.PixelType.Equals(clone.PixelType)); + Assert.False(meta.Compression.Equals(clone.Compression)); + } + + [Theory] + [InlineData(TestImages.Exr.Uncompressed, 199, 297)] + public void Identify_DetectsCorrectWidthAndHeight(string imagePath, int expectedWidth, int expectedHeight) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + Assert.Equal(expectedWidth, imageInfo.Width); + Assert.Equal(expectedHeight, imageInfo.Height); + } + + [Theory] + [InlineData(TestImages.Exr.Uncompressed, ExrPixelType.Half)] + [InlineData(TestImages.Exr.UncompressedFloatRgb, ExrPixelType.Float)] + [InlineData(TestImages.Exr.UncompressedUintRgb, ExrPixelType.UnsignedInt)] + public void Identify_DetectsCorrectPixelType(string imagePath, ExrPixelType expectedPixelType) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata(); + Assert.NotNull(metadata); + Assert.Equal(expectedPixelType, metadata.PixelType); + } + + [Theory] + [InlineData(TestImages.Exr.UncompressedRgba, ExrImageDataType.Rgba)] + [InlineData(TestImages.Exr.Rgb, ExrImageDataType.Rgb)] + [InlineData(TestImages.Exr.Gray, ExrImageDataType.Gray)] + public void Identify_DetectsCorrectImageDataType(string imagePath, ExrImageDataType expectedImageDataType) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata(); + Assert.NotNull(metadata); + Assert.Equal(expectedImageDataType, metadata.ImageDataType); + } + + [Theory] + [InlineData(ExrImageDataType.Rgba, PixelAlphaRepresentation.Associated)] + [InlineData(ExrImageDataType.Rgb, PixelAlphaRepresentation.None)] + [InlineData(ExrImageDataType.Gray, PixelAlphaRepresentation.None)] + public void GetPixelTypeInfo_ReturnsExpectedAlphaRepresentation(ExrImageDataType imageDataType, PixelAlphaRepresentation expected) + { + ExrMetadata metadata = new() { ImageDataType = imageDataType }; + + Assert.Equal(expected, metadata.GetPixelTypeInfo().AlphaRepresentation); + } + + [Theory] + [InlineData(TestImages.Exr.UncompressedRgba, ExrCompression.None)] + [InlineData(TestImages.Exr.B44, ExrCompression.B44)] + [InlineData(TestImages.Exr.Rle, ExrCompression.RunLengthEncoded)] + public void Identify_DetectsCorrectCompression(string imagePath, ExrCompression expectedCompression) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata(); + Assert.NotNull(metadata); + Assert.Equal(expectedCompression, metadata.Compression); + } + + [Theory] + [InlineData(PixelColorType.Binary, 1, ExrImageDataType.Unknown, ExrPixelType.Half)] + [InlineData(PixelColorType.Indexed, 8, ExrImageDataType.Unknown, ExrPixelType.Half)] + [InlineData(PixelColorType.Luminance, 16, ExrImageDataType.Gray, ExrPixelType.Half)] + [InlineData(PixelColorType.RGB, 48, ExrImageDataType.Rgb, ExrPixelType.Float)] + [InlineData(PixelColorType.BGR, 48, ExrImageDataType.Rgb, ExrPixelType.Float)] + [InlineData(PixelColorType.RGB | PixelColorType.Alpha, 64, ExrImageDataType.Rgba, ExrPixelType.Float)] + [InlineData(PixelColorType.BGR | PixelColorType.Alpha, 64, ExrImageDataType.Rgba, ExrPixelType.Float)] + [InlineData(PixelColorType.Luminance | PixelColorType.Alpha, 32, ExrImageDataType.Rgba, ExrPixelType.Float)] + [InlineData(PixelColorType.YCbCr, 48, ExrImageDataType.Unknown, ExrPixelType.Float)] + [InlineData(PixelColorType.CMYK, 64, ExrImageDataType.Unknown, ExrPixelType.Float)] + [InlineData(PixelColorType.YCCK, 64, ExrImageDataType.Unknown, ExrPixelType.Float)] + public void FromFormatConnectingMetadata_ConvertColorTypeAsExpected(PixelColorType pixelColorType, int bitsPerPixel, ExrImageDataType expectedImageDataType, ExrPixelType expectedPixelType) + { + FormatConnectingMetadata formatConnectingMetadata = new() + { + PixelTypeInfo = new PixelTypeInfo(bitsPerPixel) + { + ColorType = pixelColorType, + }, + }; + + ExrMetadata actual = ExrMetadata.FromFormatConnectingMetadata(formatConnectingMetadata); + + Assert.Equal(expectedImageDataType, actual.ImageDataType); + Assert.Equal(expectedPixelType, actual.PixelType); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs new file mode 100644 index 000000000..cd23e1ccd --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +public class ImageExtensionsTest +{ + [Fact] + public void SaveAsExr_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsExr_Path.exr"); + + using (Image image = new(10, 10)) + { + image.SaveAsExr(file); + } + + IImageFormat format = Image.DetectFormat(file); + Assert.True(format is ExrFormat); + } + + [Fact] + public async Task SaveAsExrAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsExrAsync_Path.exr"); + + using (Image image = new(10, 10)) + { + await image.SaveAsExrAsync(file); + } + + IImageFormat format = Image.DetectFormat(file); + Assert.True(format is ExrFormat); + } + + [Fact] + public void SaveAsExr_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsExr_Path_Encoder.exr"); + + using (Image image = new(10, 10)) + { + image.SaveAsExr(file, new ExrEncoder()); + } + + IImageFormat format = Image.DetectFormat(file); + Assert.True(format is ExrFormat); + } + + [Fact] + public async Task SaveAsExrAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsExrAsync_Path_Encoder.exr"); + + using (Image image = new(10, 10)) + { + await image.SaveAsExrAsync(file, new ExrEncoder()); + } + + IImageFormat format = Image.DetectFormat(file); + Assert.True(format is ExrFormat); + } + + [Fact] + public void SaveAsExr_Stream() + { + using MemoryStream memoryStream = new(); + + using (Image image = new(10, 10)) + { + image.SaveAsExr(memoryStream); + } + + memoryStream.Position = 0; + + IImageFormat format = Image.DetectFormat(memoryStream); + Assert.True(format is ExrFormat); + } + + [Fact] + public async Task SaveAsExrAsync_StreamAsync() + { + using MemoryStream memoryStream = new(); + + using (Image image = new(10, 10)) + { + await image.SaveAsExrAsync(memoryStream); + } + + memoryStream.Position = 0; + + IImageFormat format = Image.DetectFormat(memoryStream); + Assert.True(format is ExrFormat); + } + + [Fact] + public void SaveAsExr_Stream_Encoder() + { + using MemoryStream memoryStream = new(); + + using (Image image = new(10, 10)) + { + image.SaveAsExr(memoryStream, new ExrEncoder()); + } + + memoryStream.Position = 0; + + IImageFormat format = Image.DetectFormat(memoryStream); + Assert.True(format is ExrFormat); + } + + [Fact] + public async Task SaveAsExrAsync_Stream_Encoder() + { + using MemoryStream memoryStream = new(); + + using (Image image = new(10, 10)) + { + await image.SaveAsExrAsync(memoryStream, new ExrEncoder()); + } + + memoryStream.Position = 0; + + IImageFormat format = Image.DetectFormat(memoryStream); + Assert.True(format is ExrFormat); + } +} diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 072b04fa0..2b91c4dbf 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -125,7 +125,7 @@ public class GeneralFormatTests public static readonly TheoryData QuantizerNames = new() { - nameof(KnownQuantizers.Octree), + nameof(KnownQuantizers.Hexadecatree), nameof(KnownQuantizers.WebSafe), nameof(KnownQuantizers.Werner), nameof(KnownQuantizers.Wu) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index febc65da3..8a8b4866a 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -408,4 +408,54 @@ public class GifDecoderTests image.DebugSaveMultiFrame(provider); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); } + + [Fact] + public void Identify_MalformedApplicationExtension_IgnoresNonCriticalErrorsByDefault() + { + ImageInfo info = Image.Identify(CreateGifWithMalformedApplicationExtension()); + Assert.Equal(1, info.Width); + Assert.Equal(1, info.Height); + } + + [Fact] + public void Decode_MalformedApplicationExtension_IgnoresNonCriticalErrorsByDefault() + { + using Image image = Image.Load(CreateGifWithMalformedApplicationExtension()); + Assert.Equal(1, image.Width); + Assert.Equal(1, image.Height); + } + + [Fact] + public void Identify_MalformedApplicationExtension_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => Image.Identify(options, CreateGifWithMalformedApplicationExtension())); + } + + [Fact] + public void Decode_MalformedApplicationExtension_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => + { + using Image image = Image.Load(options, CreateGifWithMalformedApplicationExtension()); + }); + } + + private static byte[] CreateGifWithMalformedApplicationExtension() + { + // The application extension declares a 5-byte application block (0x05), but GIF application + // extensions require the fixed identifier/authentication block to be 11 bytes long. + return + [ + (byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a', + 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, + 0x21, 0xFF, 0x05, (byte)'B', (byte)'a', (byte)'d', (byte)'A', (byte)'p', 0x00, + 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x02, 0x02, 0x44, 0x01, 0x00, + 0x3B + ]; + } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 370106ca3..a465b073b 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -115,7 +116,7 @@ public class GifEncoderTests GifEncoder encoder = new() { ColorTableMode = FrameColorTableMode.Global, - Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) + Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }) }; // Always save as we need to compare the encoded output. @@ -124,7 +125,7 @@ public class GifEncoderTests encoder = new GifEncoder { ColorTableMode = FrameColorTableMode.Local, - Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }), + Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }), }; provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local"); @@ -191,7 +192,7 @@ public class GifEncoderTests GifEncoder encoder = new() { ColorTableMode = colorMode, - Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors }) + Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { MaxColors = maxColors }) }; image.Save(outStream, encoder); @@ -349,7 +350,7 @@ public class GifEncoderTests public static string[] Animated => TestImages.Gif.Animated; - [Theory(Skip = "Enable for visual animated testing")] + [Theory]//(Skip = "Enable for visual animated testing")] [WithFileCollection(nameof(Animated), PixelTypes.Rgba32)] public void Encode_Animated_VisualTest(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -436,4 +437,21 @@ public class GifEncoderTests static bool Predicate(int i, int _) => i % 8 == 0; // Image has many frames, only compare a selection of them. image.CompareDebugOutputToReferenceOutputMultiFrame(provider, ImageComparer.Exact, extension: "gif", predicate: Predicate); } + + [Theory] + [WithFile(TestImages.Gif.Issues.Issue3142, PixelTypes.Rgba32)] + public void GifEncoder_CanDecode_AndEncode_Issue3142(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + // Save the image for visual inspection. + provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder() { Quantizer = KnownQuantizers.Wu }, "animated"); + + // Now compare the debug output with the reference output. + // We do this because the gif encoding is lossy and encoding will lead to differences in the 10s of percent. + // From the unencoded image, we can see that the image is visually the same. + static bool Predicate(int i, int _) => i % 8 == 0; // Image has many frames, only compare a selection of them. + image.CompareDebugOutputToReferenceOutputMultiFrame(provider, ImageComparer.Exact, extension: "gif", predicate: Predicate); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs index f895afbd5..c41f455cd 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs @@ -4,7 +4,6 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; -using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Cur; @@ -120,12 +119,6 @@ public class CurEncoderTests for (int x = 0; x < accessor.Width; x++) { - if (expectedColor != rowSpan[x]) - { - int xx = 0; - } - - Assert.Equal(expectedColor, rowSpan[x]); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index 85ff51b18..539826799 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -204,12 +204,19 @@ public class IcoDecoderTests } [Theory] - [WithFile(InvalidAll, PixelTypes.Rgba32)] [WithFile(InvalidBpp, PixelTypes.Rgba32)] + public void InvalidThrows_InvalidImageContentException(TestImageProvider provider) + => Assert.Throws(() => + { + using Image image = provider.GetImage(IcoDecoder.Instance); + }); + + [Theory] + [WithFile(InvalidAll, PixelTypes.Rgba32)] [WithFile(InvalidCompression, PixelTypes.Rgba32)] [WithFile(InvalidRLE4, PixelTypes.Rgba32)] [WithFile(InvalidRLE8, PixelTypes.Rgba32)] - public void InvalidTest(TestImageProvider provider) + public void InvalidThows_NotSupportedException(TestImageProvider provider) => Assert.Throws(() => { using Image image = provider.GetImage(IcoDecoder.Instance); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 368a7b369..610693c7f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -181,54 +181,6 @@ public partial class Block8x8FTests : JpegFixture } } - [Theory] - [InlineData(1)] - [InlineData(2)] - public void NormalizeColorsAndRoundVector256(int seed) - { - if (this.SkipOnNonVector256Runner()) - { - return; - } - - Block8x8F source = CreateRandomFloatBlock(-200, 200, seed); - - Block8x8F expected = source; - expected.NormalizeColorsInPlace(255); - expected.RoundInPlace(); - - Block8x8F actual = source; - actual.NormalizeColorsAndRoundInPlaceVector256(255); - - this.Output.WriteLine(expected.ToString()); - this.Output.WriteLine(actual.ToString()); - this.CompareBlocks(expected, actual, 0); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - public void NormalizeColorsAndRoundVector128(int seed) - { - if (this.SkipOnNonVector128Runner()) - { - return; - } - - Block8x8F source = CreateRandomFloatBlock(-200, 200, seed); - - Block8x8F expected = source; - expected.NormalizeColorsInPlace(255); - expected.RoundInPlace(); - - Block8x8F actual = source; - actual.NormalizeColorsAndRoundInPlaceVector128(255); - - this.Output.WriteLine(expected.ToString()); - this.Output.WriteLine(actual.ToString()); - this.CompareBlocks(expected, actual, 0); - } - [Theory] [InlineData(1, 2)] [InlineData(2, 1)] @@ -267,7 +219,7 @@ public partial class Block8x8FTests : JpegFixture RunTest, srcSeed, qtSeed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } [Fact] @@ -290,29 +242,6 @@ public partial class Block8x8FTests : JpegFixture } } - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - public void RoundInPlaceSlow(int seed) - { - Block8x8F s = CreateRandomFloatBlock(-500, 500, seed); - - Block8x8F d = s; - d.RoundInPlace(); - - this.Output.WriteLine(s.ToString()); - this.Output.WriteLine(d.ToString()); - - for (int i = 0; i < 64; i++) - { - float expected = (float)Math.Round(s[i]); - float actual = d[i]; - - Assert.Equal(expected, actual); - } - } - [Fact] public void MultiplyInPlace_ByOtherBlock() { @@ -462,7 +391,7 @@ public partial class Block8x8FTests : JpegFixture // 3. DisableAvx2 - call fallback code of float implementation FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs index 50eada4c7..b17a39f38 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs @@ -152,7 +152,7 @@ public static class DCTTests FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, seed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } [Theory] @@ -193,7 +193,7 @@ public static class DCTTests { for (int x = 0; x < 4; x++) { - AssertScaledElementEquality(expectedSpan.Slice((y * 16) + (x * 2)), actualSpan.Slice((y * 8) + x)); + AssertScaledElementEquality(expectedSpan[((y * 16) + (x * 2))..], actualSpan[((y * 8) + x)..]); } } @@ -210,7 +210,7 @@ public static class DCTTests } } - average2x2 = MathF.Round(average2x2 / 4f); + average2x2 /= 4f; Assert.Equal((int)average2x2, (int)actual[0]); } @@ -254,7 +254,7 @@ public static class DCTTests { for (int x = 0; x < 2; x++) { - AssertScaledElementEquality(expectedSpan.Slice((y * 32) + (x * 4)), actualSpan.Slice((y * 8) + x)); + AssertScaledElementEquality(expectedSpan[((y * 32) + (x * 4))..], actualSpan[((y * 8) + x)..]); } } @@ -271,7 +271,7 @@ public static class DCTTests } } - average4x4 = MathF.Round(average4x4 / 16f); + average4x4 /= 16f; Assert.Equal((int)average4x4, (int)actual[0]); } @@ -311,7 +311,7 @@ public static class DCTTests NormalizationValue, MaxOutputValue); - float expected = MathF.Round(Numerics.Clamp(expectedDest[0] + NormalizationValue, 0, MaxOutputValue)); + float expected = Numerics.Clamp(expectedDest[0] + NormalizationValue, 0, MaxOutputValue); Assert.Equal((int)actual, (int)expected); } @@ -359,7 +359,7 @@ public static class DCTTests FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, seed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index 8d94fb5cf..de42e85d7 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -2,11 +2,12 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Formats.Jpeg.Components; -using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Tests.ColorProfiles; +using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; using SixLabors.ImageSharp.Tests.TestUtilities; -using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Formats.Jpg; @@ -14,35 +15,40 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg; public class JpegColorConverterTests { private const float MaxColorChannelValue = 255F; + private const float ColorProfileTolerance = 0.1F / MaxColorChannelValue; + private const float ToRgbTolerance = 0.0001F; + private const float FromRgbTolerance = 0.01F; - private const float Precision = 0.1F / 255; - - private const int TestBufferLength = 40; - - private static readonly ApproximateColorProfileComparer ColorSpaceComparer = new(epsilon: Precision); - - public static readonly TheoryData Seeds = new() { 1, 2, 3 }; - - public JpegColorConverterTests(ITestOutputHelper output) - => this.Output = output; - - private ITestOutputHelper Output { get; } + // Independent model checks compare normalized colors at one tenth of a byte-domain sample. + private static readonly ApproximateColorProfileComparer ColorSpaceComparer = new(epsilon: ColorProfileTolerance); + /// + /// Verifies that unsupported color spaces are rejected by the converter factory. + /// [Fact] public void GetConverterThrowsExceptionOnInvalidColorSpace() { const JpegColorSpace invalidColorSpace = (JpegColorSpace)(-1); + Assert.Throws(() => JpegColorConverterBase.GetConverter(invalidColorSpace, 8)); } + /// + /// Verifies that unsupported JPEG sample precisions are rejected by the converter factory. + /// [Fact] public void GetConverterThrowsExceptionOnInvalidPrecision() { - // Valid precisions: 8 & 12 bit const int invalidPrecision = 9; + Assert.Throws(() => JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, invalidPrecision)); } + /// + /// Verifies that every supported color-space and precision pair resolves to the shared operator converter. + /// + /// The JPEG color space. + /// The JPEG sample precision. [Theory] [InlineData(JpegColorSpace.Grayscale, 8)] [InlineData(JpegColorSpace.Grayscale, 12)] @@ -54,799 +60,570 @@ public class JpegColorConverterTests [InlineData(JpegColorSpace.RGB, 12)] [InlineData(JpegColorSpace.YCbCr, 8)] [InlineData(JpegColorSpace.YCbCr, 12)] + [InlineData(JpegColorSpace.TiffCmyk, 8)] + [InlineData(JpegColorSpace.TiffCmyk, 12)] + [InlineData(JpegColorSpace.TiffYccK, 8)] + [InlineData(JpegColorSpace.TiffYccK, 12)] internal void GetConverterReturnsValidConverter(JpegColorSpace colorSpace, int precision) { JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, precision); - Assert.NotNull(converter); Assert.True(converter.IsAvailable); Assert.Equal(colorSpace, converter.ColorSpace); Assert.Equal(precision, converter.Precision); } - [Fact] - public void GetConverterReturnsCorrectConverterWithRgbColorSpace() + /// + /// Verifies that the converter factory closes the shared traversal over the matching color-model operator. + /// + /// The JPEG color space. + /// The expected closed converter type. + [Theory] + [InlineData(JpegColorSpace.Grayscale, typeof(JpegColorConverterBase.JpegColorConverter))] + [InlineData(JpegColorSpace.RGB, typeof(JpegColorConverterBase.JpegColorConverter))] + [InlineData(JpegColorSpace.Cmyk, typeof(JpegColorConverterBase.JpegColorConverter))] + [InlineData(JpegColorSpace.YCbCr, typeof(JpegColorConverterBase.JpegColorConverter))] + [InlineData(JpegColorSpace.Ycck, typeof(JpegColorConverterBase.JpegColorConverter))] + [InlineData(JpegColorSpace.TiffCmyk, typeof(JpegColorConverterBase.JpegColorConverter))] + [InlineData(JpegColorSpace.TiffYccK, typeof(JpegColorConverterBase.JpegColorConverter))] + internal void GetConverterReturnsClosedOperatorConverter(JpegColorSpace colorSpace, Type expectedType) { - FeatureTestRunner.RunWithHwIntrinsicsFeature( - RunTest, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); - - static void RunTest(string arg) - { - // arrange - Type expectedType = typeof(JpegColorConverterBase.RgbScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.RgbVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.RgbVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.RgbVector128); - } - - // act - JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, 8); - Type actualType = converter.GetType(); + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, 8); - // assert - Assert.Equal(expectedType, actualType); - } + Assert.Equal(expectedType, converter.GetType()); } - [Fact] - public void GetConverterReturnsCorrectConverterWithGrayScaleColorSpace() + /// + /// Verifies the replacement converter against independent definitions of the established JPEG color models. + /// + /// The JPEG color space. + /// The number of component planes owned by the color model. + [Theory] + [InlineData(JpegColorSpace.Grayscale, 1)] + [InlineData(JpegColorSpace.RGB, 3)] + [InlineData(JpegColorSpace.Cmyk, 4)] + [InlineData(JpegColorSpace.YCbCr, 3)] + [InlineData(JpegColorSpace.Ycck, 4)] + internal void ConvertToRgbMatchesColorModelDefinition(JpegColorSpace colorSpace, int componentCount) { - FeatureTestRunner.RunWithHwIntrinsicsFeature( - RunTest, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic); - - static void RunTest(string arg) - { - // arrange - Type expectedType = typeof(JpegColorConverterBase.GrayScaleScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.GrayScaleVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.GrayScaleVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.GrayScaleVector128); - } + const int length = 128; + JpegColorConverterBase.ComponentValues source = CreateRandomValues(length, componentCount, 8); + JpegColorConverterBase.ComponentValues actual = CreateRandomValues(length, componentCount, 8); + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, 8); - // act - JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, 8); - Type actualType = converter.GetType(); + converter.ConvertToRgbInPlace(actual); - // assert - Assert.Equal(expectedType, actualType); + for (int i = 0; i < length; i++) + { + AssertColorModelDefinition(colorSpace, source, actual, i); } } - [Fact] - public void GetConverterReturnsCorrectConverterWithCmykColorSpace() + /// + /// Verifies the adaptive traversal against each operator's scalar definition around every SIMD boundary. + /// + /// The JPEG color space. + /// The number of component planes owned by the color model. + /// The JPEG sample precision. + [Theory] + [InlineData(JpegColorSpace.Grayscale, 1, 8)] + [InlineData(JpegColorSpace.Grayscale, 1, 12)] + [InlineData(JpegColorSpace.RGB, 3, 8)] + [InlineData(JpegColorSpace.RGB, 3, 12)] + [InlineData(JpegColorSpace.Cmyk, 4, 8)] + [InlineData(JpegColorSpace.Cmyk, 4, 12)] + [InlineData(JpegColorSpace.YCbCr, 3, 8)] + [InlineData(JpegColorSpace.YCbCr, 3, 12)] + [InlineData(JpegColorSpace.Ycck, 4, 8)] + [InlineData(JpegColorSpace.Ycck, 4, 12)] + [InlineData(JpegColorSpace.TiffCmyk, 4, 8)] + [InlineData(JpegColorSpace.TiffCmyk, 4, 12)] + [InlineData(JpegColorSpace.TiffYccK, 4, 8)] + [InlineData(JpegColorSpace.TiffYccK, 4, 12)] + internal void OperatorTraversalMatchesScalarDefinition(JpegColorSpace colorSpace, int componentCount, int precision) { - FeatureTestRunner.RunWithHwIntrinsicsFeature( - RunTest, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic); - - static void RunTest(string arg) + switch (colorSpace) { - // arrange - Type expectedType = typeof(JpegColorConverterBase.CmykScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.CmykVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.CmykVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.CmykVector128); - } - - // act - JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Cmyk, 8); - Type actualType = converter.GetType(); - - // assert - Assert.Equal(expectedType, actualType); + case JpegColorSpace.Grayscale: + ValidateOperator(componentCount, precision); + break; + case JpegColorSpace.RGB: + ValidateOperator(componentCount, precision); + break; + case JpegColorSpace.Cmyk: + ValidateOperator(componentCount, precision); + break; + case JpegColorSpace.YCbCr: + ValidateOperator(componentCount, precision); + break; + case JpegColorSpace.Ycck: + ValidateOperator(componentCount, precision); + break; + case JpegColorSpace.TiffCmyk: + ValidateOperator(componentCount, precision); + break; + case JpegColorSpace.TiffYccK: + ValidateOperator(componentCount, precision); + break; + default: + Assert.Fail($"Unexpected JPEG color space: {colorSpace}."); + break; } } - [Fact] - public void GetConverterReturnsCorrectConverterWithYCbCrColorSpace() + /// + /// Verifies the flattened ICC traversal against independently composed color-model and profile conversions. + /// + /// The JPEG color space. + /// The number of component planes owned by the color model. + /// The JPEG sample precision. + [Theory] + [InlineData(JpegColorSpace.Grayscale, 1, 8)] + [InlineData(JpegColorSpace.Grayscale, 1, 12)] + [InlineData(JpegColorSpace.RGB, 3, 8)] + [InlineData(JpegColorSpace.RGB, 3, 12)] + [InlineData(JpegColorSpace.YCbCr, 3, 8)] + [InlineData(JpegColorSpace.YCbCr, 3, 12)] + [InlineData(JpegColorSpace.Cmyk, 4, 8)] + [InlineData(JpegColorSpace.Cmyk, 4, 12)] + [InlineData(JpegColorSpace.Ycck, 4, 8)] + [InlineData(JpegColorSpace.Ycck, 4, 12)] + [InlineData(JpegColorSpace.TiffCmyk, 4, 8)] + [InlineData(JpegColorSpace.TiffCmyk, 4, 12)] + [InlineData(JpegColorSpace.TiffYccK, 4, 8)] + [InlineData(JpegColorSpace.TiffYccK, 4, 12)] + internal void ConvertToRgbWithIccMatchesColorProfileDefinition(JpegColorSpace colorSpace, int componentCount, int precision) { - FeatureTestRunner.RunWithHwIntrinsicsFeature( - RunTest, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic); + const int length = 8; + JpegColorConverterBase.ComponentValues source = CreateRandomValues(length, componentCount, precision); + JpegColorConverterBase.ComponentValues actual = CreateRandomValues(length, componentCount, precision); + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, precision); - static void RunTest(string arg) + // YccK and CMYK profiles describe the conventional CMYK values produced after JPEG inversion + // and any YccK model transform. Three-component models use an RGB profile, while grayscale + // needs a one-channel profile so the profile's declared data space agrees with the Y input. + IccProfile profile = colorSpace switch { - // arrange - Type expectedType = typeof(JpegColorConverterBase.YCbCrScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.YCbCrVector512); - } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) - { - expectedType = typeof(JpegColorConverterBase.YCbCrVector256); - } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) + JpegColorSpace.Grayscale => CreateGrayProfile(), + JpegColorSpace.Cmyk or JpegColorSpace.Ycck or JpegColorSpace.TiffCmyk or JpegColorSpace.TiffYccK + => TestIccProfiles.GetProfile(TestIccProfiles.Fogra39), + _ => CompactSrgbV4Profile.Profile, + }; + + converter.ConvertToRgbInPlaceWithIcc(Configuration.Default, actual, profile); + ColorConversionOptions options = new() + { + SourceIccProfile = profile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + }; + + ColorProfileConverter profileConverter = new(options); + ColorProfileConverter modelConverter = new(); + float maximumValue = MathF.Pow(2, precision) - 1; + Rgb[] grayscaleExpected = null; + + if (colorSpace == JpegColorSpace.Grayscale) + { + // The profile converter has distinct scalar and span entry points. Production converts the complete + // grayscale row, so the independent reference must exercise that same public span contract. + Y[] luminance = new Y[length]; + grayscaleExpected = new Rgb[length]; + + for (int i = 0; i < length; i++) { - expectedType = typeof(JpegColorConverterBase.YCbCrVector128); + luminance[i] = new Y(source.Component0[i] / maximumValue); } - // act - JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, 8); - Type actualType = converter.GetType(); - - // assert - Assert.Equal(expectedType, actualType); + profileConverter.Convert(luminance, grayscaleExpected); } - } - [Fact] - public void GetConverterReturnsCorrectConverterWithYcckColorSpace() - { - FeatureTestRunner.RunWithHwIntrinsicsFeature( - RunTest, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic); - - static void RunTest(string arg) + for (int i = 0; i < length; i++) { - // arrange - Type expectedType = typeof(JpegColorConverterBase.YccKScalar); - if (JpegColorConverterBase.JpegColorConverterVector512.IsSupported) + float c0 = source.Component0[i] / maximumValue; + float c1 = componentCount >= 2 ? source.Component1[i] / maximumValue : 0; + float c2 = componentCount >= 3 ? source.Component2[i] / maximumValue : 0; + float c3 = componentCount == 4 ? source.Component3[i] / maximumValue : 0; + Rgb expected; + + switch (colorSpace) { - expectedType = typeof(JpegColorConverterBase.YccKVector512); + case JpegColorSpace.Grayscale: + expected = grayscaleExpected[i]; + break; + + case JpegColorSpace.RGB: + expected = profileConverter.Convert(new Rgb(c0, c1, c2)); + break; + + case JpegColorSpace.YCbCr: + Rgb rgb = modelConverter.Convert(new YCbCr(c0, c1, c2)); + expected = profileConverter.Convert(rgb); + break; + + case JpegColorSpace.Cmyk: + expected = profileConverter.Convert(new Cmyk(1F - c0, 1F - c1, 1F - c2, 1F - c3)); + break; + + case JpegColorSpace.Ycck: + Cmyk cmyk = modelConverter.Convert(new YccK(1F - c0, 1F - c1, 1F - c2, 1F - c3)); + expected = profileConverter.Convert(cmyk); + break; + + case JpegColorSpace.TiffCmyk: + expected = profileConverter.Convert(new Cmyk(c0, c1, c2, c3)); + break; + + case JpegColorSpace.TiffYccK: + Cmyk tiffCmyk = modelConverter.Convert(new YccK(c0, c1, c2, c3)); + expected = profileConverter.Convert(tiffCmyk); + break; + + default: + Assert.Fail($"Unexpected JPEG color space: {colorSpace}."); + return; } - else if (JpegColorConverterBase.JpegColorConverterVector256.IsSupported) + + if (colorSpace == JpegColorSpace.Grayscale) { - expectedType = typeof(JpegColorConverterBase.YccKVector256); + // Grayscale exposes one physical component plane through all three component views. Deinterleaving + // therefore leaves the final blue write in that plane, matching the established converter contract. + Assert.Equal(expected.B, actual.Component0[i], ToRgbTolerance); } - else if (JpegColorConverterBase.JpegColorConverterVector128.IsSupported) + else { - expectedType = typeof(JpegColorConverterBase.YccKVector128); + Assert.Equal(expected.R, actual.Component0[i], ToRgbTolerance); + Assert.Equal(expected.G, actual.Component1[i], ToRgbTolerance); + Assert.Equal(expected.B, actual.Component2[i], ToRgbTolerance); } - - // act - JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.Ycck, 8); - Type actualType = converter.GetType(); - - // assert - Assert.Equal(expectedType, actualType); } } - [Theory] - [InlineData(JpegColorSpace.Grayscale, 1)] - [InlineData(JpegColorSpace.Ycck, 4)] - [InlineData(JpegColorSpace.Cmyk, 4)] - [InlineData(JpegColorSpace.RGB, 3)] - [InlineData(JpegColorSpace.YCbCr, 3)] - internal void ConvertToRgbWithSelectedConverter(JpegColorSpace colorSpace, int componentCount) - { - JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(colorSpace, 8); - ValidateConversionToRgb( - converter, - componentCount, - 1); - } - - [Theory] - [MemberData(nameof(Seeds))] - public void FromYCbCrBasic(int seed) => - this.TestConversionToRgb(new JpegColorConverterBase.YCbCrScalar(8), 3, seed); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromYCbCrVector512(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.YCbCrVector512(8), - 3, - seed, - new JpegColorConverterBase.YCbCrScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromYCbCrVector256(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.YCbCrVector256(8), - 3, - seed, - new JpegColorConverterBase.YCbCrScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromYCbCrVector128(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.YCbCrVector128(8), - 3, - seed, - new JpegColorConverterBase.YCbCrScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToYCbCrVector512(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.YCbCrVector512(8), - 3, - seed, - new JpegColorConverterBase.YCbCrScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToYCbCrVector256(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.YCbCrVector256(8), - 3, - seed, - new JpegColorConverterBase.YCbCrScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToYCbCrVector128(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.YCbCrVector128(8), - 3, - seed, - new JpegColorConverterBase.YCbCrScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromCmykBasic(int seed) => - this.TestConversionToRgb(new JpegColorConverterBase.CmykScalar(8), 4, seed); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromCmykVector512(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.CmykVector512(8), - 4, - seed, - new JpegColorConverterBase.CmykScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromCmykVector256(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.CmykVector256(8), - 4, - seed, - new JpegColorConverterBase.CmykScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromCmykVector128(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.CmykVector128(8), - 4, - seed, - new JpegColorConverterBase.CmykScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToCmykVector512(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.CmykVector512(8), - 4, - seed, - new JpegColorConverterBase.CmykScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToCmykVector256(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.CmykVector256(8), - 4, - seed, - new JpegColorConverterBase.CmykScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToCmykVector128(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.CmykVector128(8), - 4, - seed, - new JpegColorConverterBase.CmykScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromGrayScaleBasic(int seed) => - this.TestConversionToRgb(new JpegColorConverterBase.GrayScaleScalar(8), 1, seed); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromGrayScaleVector512(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.GrayScaleVector512(8), - 1, - seed, - new JpegColorConverterBase.GrayScaleScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromGrayScaleVector256(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.GrayScaleVector256(8), - 1, - seed, - new JpegColorConverterBase.GrayScaleScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromGrayScaleVector128(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.GrayScaleVector128(8), - 1, - seed, - new JpegColorConverterBase.GrayScaleScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToGrayScaleVector512(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.GrayScaleVector512(8), - 1, - seed, - new JpegColorConverterBase.GrayScaleScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToGrayScaleVector256(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.GrayScaleVector256(8), - 1, - seed, - new JpegColorConverterBase.GrayScaleScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToGrayScaleVector128(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.GrayScaleVector128(8), - 1, - seed, - new JpegColorConverterBase.GrayScaleScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbBasic(int seed) => - this.TestConversionToRgb(new JpegColorConverterBase.RgbScalar(8), 3, seed); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbVector512(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.RgbVector512(8), - 3, - seed, - new JpegColorConverterBase.RgbScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbVector256(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.RgbVector256(8), - 3, - seed, - new JpegColorConverterBase.RgbScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbVector128(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.RgbVector128(8), - 3, - seed, - new JpegColorConverterBase.RgbScalar(8)); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToRgbVector512(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.RgbVector512(8), - 3, - seed, - new JpegColorConverterBase.RgbScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToRgbVector256(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.RgbVector256(8), - 3, - seed, - new JpegColorConverterBase.RgbScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToRgbVector128(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.RgbVector128(8), - 3, - seed, - new JpegColorConverterBase.RgbScalar(8), - precision: 2); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromYccKBasic(int seed) => - this.TestConversionToRgb(new JpegColorConverterBase.YccKScalar(8), 4, seed); - - [Theory] - [MemberData(nameof(Seeds))] - public void FromYccKVector512(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.YccKVector512(8), - 4, - seed, - new JpegColorConverterBase.YccKScalar(8)); + /// + /// Verifies that the shared converter retains its scalar behavior when hardware intrinsics are disabled. + /// + [Fact] + public void OperatorTraversalMatchesScalarWithoutHardwareIntrinsics() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunWithoutHardwareIntrinsics, HwIntrinsics.DisableHWIntrinsic); - [Theory] - [MemberData(nameof(Seeds))] - public void FromYccKVector256(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.YccKVector256(8), - 4, - seed, - new JpegColorConverterBase.YccKScalar(8)); + /// + /// Verifies TIFF YccK encoding against the canonical normalized color-profile conversion. + /// + [Fact] + public void TiffYccKOperatorFromRgbMatchesColorProfileDefinition() + { + const int maximumLength = 40; + const float maximumValue = 255F; + const float halfValue = 128F; + float[] rSeed = [0, 255, 255, 0, 0, 127, 32, 240, 0, 255, 255, 0, 0, 127, 32, 240, 0, 255, 64, 192]; + float[] gSeed = [0, 255, 0, 255, 0, 127, 160, 16, 0, 255, 0, 255, 0, 127, 160, 16, 255, 0, 128, 96]; + float[] bSeed = [0, 255, 0, 0, 255, 127, 224, 80, 255, 0, 255, 0, 0, 127, 224, 80, 0, 255, 192, 32]; + float[] r = new float[maximumLength]; + float[] g = new float[maximumLength]; + float[] b = new float[maximumLength]; + JpegColorConverterBase converter = JpegColorConverterBase.GetConverter(JpegColorSpace.TiffYccK, 8); + + // Repeating the color set provides enough lanes to exercise every SIMD width and mixed-width tail. + rSeed.CopyTo(r, 0); + rSeed.CopyTo(r, rSeed.Length); + gSeed.CopyTo(g, 0); + gSeed.CopyTo(g, gSeed.Length); + bSeed.CopyTo(b, 0); + bSeed.CopyTo(b, bSeed.Length); + + ColorProfileConverter reference = new(); + int[] lengths = [1, 3, 4, 7, 8, 15, 16, 31, 32, 40]; + + foreach (int length in lengths) + { + float[] y = new float[length]; + float[] cb = new float[length]; + float[] cr = new float[length]; + float[] k = new float[length]; + JpegColorConverterBase.ComponentValues values = new(4, y, cb, cr, k); - [Theory] - [MemberData(nameof(Seeds))] - public void FromYccKVector128(int seed) => - this.TestConversionToRgb( - new JpegColorConverterBase.YccKVector128(8), - 4, - seed, - new JpegColorConverterBase.YccKScalar(8)); + converter.ConvertFromRgb(values, r.AsSpan(0, length), g.AsSpan(0, length), b.AsSpan(0, length)); - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToYccKVector512(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.YccKVector512(8), - 4, - seed, - new JpegColorConverterBase.YccKScalar(8), - precision: 2); + for (int i = 0; i < length; i++) + { + Rgb rgb = new(r[i] / maximumValue, g[i] / maximumValue, b[i] / maximumValue); + YccK expected = reference.Convert(rgb); - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToYccKVector256(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.YccKVector256(8), - 4, - seed, - new JpegColorConverterBase.YccKScalar(8), - precision: 2); + Assert.Equal(expected.Y * maximumValue, y[i], ToRgbTolerance); - [Theory] - [MemberData(nameof(Seeds))] - public void FromRgbToYccKVector128(int seed) => - this.TestConversionFromRgb( - new JpegColorConverterBase.YccKVector128(8), - 4, - seed, - new JpegColorConverterBase.YccKScalar(8), - precision: 2); - - private void TestConversionToRgb( - JpegColorConverterBase converter, - int componentCount, - int seed, - JpegColorConverterBase baseLineConverter = null) - { - if (!converter.IsAvailable) - { - this.Output.WriteLine( - $"Skipping test - {converter.GetType().Name} is not supported on current hardware."); - return; + // JPEG centers chroma on the integer midpoint, while the color-profile definition uses exactly 0.5. + Assert.Equal(halfValue + ((expected.Cb - 0.5F) * maximumValue), cb[i], ToRgbTolerance); + Assert.Equal(halfValue + ((expected.Cr - 0.5F) * maximumValue), cr[i], ToRgbTolerance); + Assert.Equal(expected.K * maximumValue, k[i], ToRgbTolerance); + } } - - ValidateConversionToRgb( - converter, - componentCount, - seed, - baseLineConverter); } - private void TestConversionFromRgb( - JpegColorConverterBase converter, - int componentCount, - int seed, - JpegColorConverterBase baseLineConverter, - int precision) + /// + /// Runs the disabled-intrinsics scalar comparison inside the feature-test process. + /// + /// The unused feature-test argument. + private static void RunWithoutHardwareIntrinsics(string arg) + => ValidateOperator(3, 8); + + /// + /// Checks one closed operator converter at every scalar and SIMD transition length. + /// + /// The color-model operator under test. + /// The number of component planes owned by the operator. + /// The JPEG sample precision. + private static void ValidateOperator(int componentCount, int precision) + where TOperator : struct, JpegColorConverterBase.IJpegColorConverterOperator { - if (!converter.IsAvailable) + JpegColorConverterBase converter = new JpegColorConverterBase.JpegColorConverter(precision); + int[] lengths = [1, 3, 4, 7, 8, 15, 16, 31, 32, 40, 64, 128]; + + // Adjacent values around 4/8/16 lanes verify every prefix, mixed-width tail, and scalar remainder. + foreach (int length in lengths) { - this.Output.WriteLine( - $"Skipping test - {converter.GetType().Name} is not supported on current hardware."); - return; + ValidateConversionToRgb(converter, length, componentCount, precision); + ValidateConversionFromRgb(converter, length, componentCount, precision); } - - ValidateConversionFromRgb( - converter, - componentCount, - seed, - baseLineConverter, - precision); } - private static JpegColorConverterBase.ComponentValues CreateRandomValues( - int length, - int componentCount, - int seed) + /// + /// Compares the adaptive component-to-RGB traversal with repeated scalar operator calls. + /// + /// The color-model operator under test. + /// The adaptive converter. + /// The number of samples to convert. + /// The number of source component planes. + /// The JPEG sample precision. + private static void ValidateConversionToRgb(JpegColorConverterBase converter, int length, int componentCount, int precision) + where TOperator : struct, JpegColorConverterBase.IJpegColorConverterOperator { - Random rnd = new(seed); + JpegColorConverterBase.ComponentValues expected = CreateRandomValues(length, componentCount, precision); + JpegColorConverterBase.ComponentValues actual = CreateRandomValues(length, componentCount, precision); + float maximumValue = MathF.Pow(2, precision) - 1; + float halfValue = MathF.Ceiling(maximumValue * 0.5F); + float scale = 1F / maximumValue; - Buffer2D[] buffers = new Buffer2D[componentCount]; - for (int i = 0; i < componentCount; i++) + for (int i = 0; i < length; i++) { - float[] values = new float[length]; - - for (int j = 0; j < values.Length; j++) - { - values[j] = (float)rnd.NextDouble() * MaxColorChannelValue; - } + ref float c0 = ref expected.Component0[i]; + ref float c1 = ref expected.Component1[i]; + ref float c2 = ref expected.Component2[i]; + float c3 = componentCount == 4 ? expected.Component3[i] : 0; - // no need to dispose when buffer is not array owner - Memory memory = new(values); - MemoryGroup source = MemoryGroup.Wrap(memory); - buffers[i] = new Buffer2D(source, values.Length, 1); + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, c3, maximumValue, halfValue, scale); } - return new JpegColorConverterBase.ComponentValues(buffers, 0); - } - - private static float[] CreateRandomValues(int length, Random rnd) - { - float[] values = new float[length]; - - for (int j = 0; j < values.Length; j++) - { - values[j] = (float)rnd.NextDouble() * MaxColorChannelValue; - } + converter.ConvertToRgbInPlace(actual); - return values; + // YCbCr conversion rounds in the integer sample domain before normalization. Fused SIMD arithmetic can + // cross a half-way boundary differently from the scalar expression, so allow one source-sample quantum + // in addition to the ordinary floating-point tolerance at both supported sample precisions. + float tolerance = scale + ToRgbTolerance; + CompareSequence(expected.Component0, actual.Component0, tolerance); + CompareSequence(expected.Component1, actual.Component1, tolerance); + CompareSequence(expected.Component2, actual.Component2, tolerance); } - private static void ValidateConversionToRgb( - JpegColorConverterBase converter, - int componentCount, - int seed, - JpegColorConverterBase baseLineConverter = null) + /// + /// Compares the adaptive RGB-to-component traversal with repeated scalar operator calls. + /// + /// The color-model operator under test. + /// The adaptive converter. + /// The number of samples to convert. + /// The number of destination component planes. + /// The JPEG sample precision. + private static void ValidateConversionFromRgb(JpegColorConverterBase converter, int length, int componentCount, int precision) + where TOperator : struct, JpegColorConverterBase.IJpegColorConverterOperator { - JpegColorConverterBase.ComponentValues original = CreateRandomValues(TestBufferLength, componentCount, seed); - JpegColorConverterBase.ComponentValues actual = new( - original.ComponentCount, - original.Component0.ToArray(), - original.Component1.ToArray(), - original.Component2.ToArray(), - original.Component3.ToArray()); - - converter.ConvertToRgbInPlace(actual); - - for (int i = 0; i < TestBufferLength; i++) - { - Validate(converter.ColorSpace, original, actual, i); - } - - // Compare conversion result to a baseline, should be the scalar version. - if (baseLineConverter != null) + JpegColorConverterBase.ComponentValues expected = CreateRandomValues(length, componentCount, precision); + JpegColorConverterBase.ComponentValues actual = CreateRandomValues(length, componentCount, precision); + Random random = new(precision); + float[] r = CreateRandomValues(length, random); + float[] g = CreateRandomValues(length, random); + float[] b = CreateRandomValues(length, random); + float maximumValue = MathF.Pow(2, precision) - 1; + float halfValue = MathF.Ceiling(maximumValue * 0.5F); + float scale = 1F / maximumValue; + + for (int i = 0; i < length; i++) { - JpegColorConverterBase.ComponentValues expected = new( - original.ComponentCount, - original.Component0.ToArray(), - original.Component1.ToArray(), - original.Component2.ToArray(), - original.Component3.ToArray()); - baseLineConverter.ConvertToRgbInPlace(expected); - if (componentCount == 1) - { - Assert.True(expected.Component0.SequenceEqual(actual.Component0)); - } + TOperator.ConvertFromRgb(r[i], g[i], b[i], maximumValue, halfValue, scale, out expected.Component0[i], out float c1, out float c2, out float c3); - if (componentCount == 2) + if (componentCount >= 2) { - Assert.True(expected.Component1.SequenceEqual(actual.Component1)); + expected.Component1[i] = c1; } - if (componentCount == 3) + if (componentCount >= 3) { - Assert.True(expected.Component2.SequenceEqual(actual.Component2)); + expected.Component2[i] = c2; } if (componentCount == 4) { - Assert.True(expected.Component3.SequenceEqual(actual.Component3)); + expected.Component3[i] = c3; } } - } - private static void ValidateConversionFromRgb( - JpegColorConverterBase converter, - int componentCount, - int seed, - JpegColorConverterBase baseLineConverter, - int precision = 4) - { - // arrange - JpegColorConverterBase.ComponentValues actual = CreateRandomValues(TestBufferLength, componentCount, seed); - JpegColorConverterBase.ComponentValues expected = CreateRandomValues(TestBufferLength, componentCount, seed); - Random rnd = new(seed); - float[] rLane = CreateRandomValues(TestBufferLength, rnd); - float[] gLane = CreateRandomValues(TestBufferLength, rnd); - float[] bLane = CreateRandomValues(TestBufferLength, rnd); - - // act - converter.ConvertFromRgb(actual, rLane, gLane, bLane); - baseLineConverter.ConvertFromRgb(expected, rLane, gLane, bLane); - - // assert - if (componentCount == 1) - { - CompareSequenceWithTolerance(expected.Component0, actual.Component0, precision); - } + converter.ConvertFromRgb(actual, r, g, b); + CompareSequence(expected.Component0, actual.Component0, FromRgbTolerance); - if (componentCount == 2) + if (componentCount >= 2) { - CompareSequenceWithTolerance(expected.Component1, actual.Component1, precision); + CompareSequence(expected.Component1, actual.Component1, FromRgbTolerance); } - if (componentCount == 3) + if (componentCount >= 3) { - CompareSequenceWithTolerance(expected.Component2, actual.Component2, precision); + CompareSequence(expected.Component2, actual.Component2, FromRgbTolerance); } if (componentCount == 4) { - CompareSequenceWithTolerance(expected.Component3, actual.Component3, precision); + CompareSequence(expected.Component3, actual.Component3, FromRgbTolerance); } } - private static void CompareSequenceWithTolerance(Span expected, Span actual, int precision) + /// + /// Creates deterministic component planes in the configured JPEG sample domain. + /// + /// The number of samples in each plane. + /// The number of independent component planes. + /// The JPEG sample precision and deterministic random seed. + /// The generated component planes. + private static JpegColorConverterBase.ComponentValues CreateRandomValues(int length, int componentCount, int precision) { - for (int i = 0; i < expected.Length; i++) - { - Assert.Equal(expected[i], actual[i], precision: precision); - } + Random random = new(precision); + float maximumValue = MathF.Pow(2, precision) - 1; + float[] c0 = CreateRandomValues(length, random, maximumValue); + float[] c1 = componentCount >= 2 ? CreateRandomValues(length, random, maximumValue) : c0; + float[] c2 = componentCount >= 3 ? CreateRandomValues(length, random, maximumValue) : c0; + float[] c3 = componentCount == 4 ? CreateRandomValues(length, random, maximumValue) : []; + + return new JpegColorConverterBase.ComponentValues(componentCount, c0, c1, c2, c3); } - private static void Validate( - JpegColorSpace colorSpace, - in JpegColorConverterBase.ComponentValues original, - in JpegColorConverterBase.ComponentValues result, - int i) + /// + /// Creates deterministic RGB samples in ImageSharp's byte-scaled encoder domain. + /// + /// The number of samples. + /// The deterministic random source. + /// The generated samples. + private static float[] CreateRandomValues(int length, Random random) + => CreateRandomValues(length, random, 255F); + + /// + /// Creates deterministic samples between zero and the supplied inclusive domain maximum. + /// + /// The number of samples. + /// The deterministic random source. + /// The upper bound of the sample domain. + /// The generated samples. + private static float[] CreateRandomValues(int length, Random random, float maximumValue) { - switch (colorSpace) + float[] values = new float[length]; + + for (int i = 0; i < values.Length; i++) { - case JpegColorSpace.Grayscale: - ValidateGrayScale(original, result, i); - break; - case JpegColorSpace.Ycck: - ValidateYccK(original, result, i); - break; - case JpegColorSpace.Cmyk: - ValidateCmyk(original, result, i); - break; - case JpegColorSpace.RGB: - ValidateRgb(original, result, i); - break; - case JpegColorSpace.YCbCr: - ValidateYCbCr(original, result, i); - break; - default: - Assert.Fail($"Invalid Colorspace enum value: {colorSpace}."); - break; + values[i] = random.NextSingle() * maximumValue; } + + return values; } - private static void ValidateYCbCr(in JpegColorConverterBase.ComponentValues values, in JpegColorConverterBase.ComponentValues result, int i) + /// + /// Creates an identity-transfer grayscale profile for the one-channel ICC test path. + /// + /// The grayscale ICC profile. + private static IccProfile CreateGrayProfile() { - float y = values.Component0[i]; - float cb = values.Component1[i] - 128; - float cr = values.Component2[i] - 128; - - float r = (float)Math.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero); - float g = (float)Math.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero); - float b = (float)Math.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero); - - r /= MaxColorChannelValue; - g /= MaxColorChannelValue; - b /= MaxColorChannelValue; - - Rgb expected = Rgb.Clamp(new Rgb(r, g, b)); - Rgb actual = Rgb.Clamp(new Rgb(result.Component0[i], result.Component1[i], result.Component2[i])); - - bool equal = ColorSpaceComparer.Equals(expected, actual); - Assert.True(equal, $"Colors {expected} and {actual} are not equal at index {i}"); + IccProfileHeader header = new() + { + Class = IccProfileClass.InputDevice, + DataColorSpace = IccColorSpaceType.Gray, + ProfileConnectionSpace = IccColorSpaceType.CieXyz, + RenderingIntent = IccRenderingIntent.MediaRelativeColorimetric, + Version = new IccVersion(4, 3, 0), + }; + + // An empty ICC curve is the standard identity transfer function. Tagging it as GrayTrc gives + // the profile converter a complete one-channel device-to-PCS path without test-only LUT data. + IccTagDataEntry[] entries = [new IccCurveTagDataEntry(IccProfileTag.GrayTrc)]; + return new IccProfile(header, entries); } - private static void ValidateYccK(in JpegColorConverterBase.ComponentValues values, in JpegColorConverterBase.ComponentValues result, int i) + /// + /// Compares one converted sample with an independent definition of its JPEG color model. + /// + /// The JPEG color space. + /// The unmodified source component planes. + /// The converted RGB planes. + /// The sample index. + private static void AssertColorModelDefinition(JpegColorSpace colorSpace, in JpegColorConverterBase.ComponentValues source, in JpegColorConverterBase.ComponentValues actual, int index) { - float y = values.Component0[i]; - float cb = values.Component1[i] - 128F; - float cr = values.Component2[i] - 128F; - float k = values.Component3[i] / 255F; + float c0 = source.Component0[index]; + float c1 = source.Component1[index]; + float c2 = source.Component2[index]; + float c3 = 0; + Rgb expected; - float r = (255F - (float)Math.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero)) * k; - float g = (255F - (float)Math.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero)) * k; - float b = (255F - (float)Math.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero)) * k; - - r /= MaxColorChannelValue; - g /= MaxColorChannelValue; - b /= MaxColorChannelValue; - Rgb expected = Rgb.Clamp(new Rgb(r, g, b)); + switch (colorSpace) + { + case JpegColorSpace.Grayscale: + float luminance = c0 / MaxColorChannelValue; + expected = new Rgb(luminance, luminance, luminance); + break; + case JpegColorSpace.RGB: + expected = new Rgb(c0 / MaxColorChannelValue, c1 / MaxColorChannelValue, c2 / MaxColorChannelValue); - Rgb actual = Rgb.Clamp(new Rgb(result.Component0[i], result.Component1[i], result.Component2[i])); + break; + case JpegColorSpace.Cmyk: + c3 = source.Component3[index] / MaxColorChannelValue; + expected = new Rgb(c0 * c3 / MaxColorChannelValue, c1 * c3 / MaxColorChannelValue, c2 * c3 / MaxColorChannelValue); - bool equal = ColorSpaceComparer.Equals(expected, actual); - Assert.True(equal, $"Colors {expected} and {actual} are not equal at index {i}"); - } + break; + case JpegColorSpace.YCbCr: + c1 -= 128F; + c2 -= 128F; - private static void ValidateRgb(in JpegColorConverterBase.ComponentValues values, in JpegColorConverterBase.ComponentValues result, int i) - { - float r = values.Component0[i] / MaxColorChannelValue; - float g = values.Component1[i] / MaxColorChannelValue; - float b = values.Component2[i] / MaxColorChannelValue; - Rgb expected = Rgb.Clamp(new Rgb(r, g, b)); + // JPEG applies the BT.601 matrix in the integer sample domain and rounds before normalization. + expected = new Rgb(MathF.Round(c0 + (1.402F * c2), MidpointRounding.AwayFromZero) / MaxColorChannelValue, MathF.Round(c0 - (0.344136F * c1) - (0.714136F * c2), MidpointRounding.AwayFromZero) / MaxColorChannelValue, MathF.Round(c0 + (1.772F * c1), MidpointRounding.AwayFromZero) / MaxColorChannelValue); - Rgb actual = Rgb.Clamp(new Rgb(result.Component0[i], result.Component1[i], result.Component2[i])); + break; + case JpegColorSpace.Ycck: + c1 -= 128F; + c2 -= 128F; + c3 = source.Component3[index] / MaxColorChannelValue; - bool equal = ColorSpaceComparer.Equals(expected, actual); - Assert.True(equal, $"Colors {expected} and {actual} are not equal at index {i}"); - } + // Adobe YccK reconstructs inverted RGB first, then applies the normalized black component. + expected = new Rgb((MaxColorChannelValue - MathF.Round(c0 + (1.402F * c2), MidpointRounding.AwayFromZero)) * c3 / MaxColorChannelValue, (MaxColorChannelValue - MathF.Round(c0 - (0.344136F * c1) - (0.714136F * c2), MidpointRounding.AwayFromZero)) * c3 / MaxColorChannelValue, (MaxColorChannelValue - MathF.Round(c0 + (1.772F * c1), MidpointRounding.AwayFromZero)) * c3 / MaxColorChannelValue); - private static void ValidateGrayScale(in JpegColorConverterBase.ComponentValues values, in JpegColorConverterBase.ComponentValues result, int i) - { - float y = values.Component0[i] / MaxColorChannelValue; - Rgb expected = Rgb.Clamp(new Rgb(y, y, y)); + break; + default: + Assert.Fail($"Unexpected JPEG color space: {colorSpace}."); + return; + } - Rgb actual = Rgb.Clamp(new Rgb(result.Component0[i], result.Component0[i], result.Component0[i])); + // Color-space comparison intentionally clamps both sides because JPEG reconstruction can overshoot + // the normalized RGB gamut and saturation belongs to the eventual pixel conversion. + Rgb clampedExpected = Rgb.Clamp(expected); + Rgb clampedActual = Rgb.Clamp(new Rgb(actual.Component0[index], actual.Component1[index], actual.Component2[index])); - bool equal = ColorSpaceComparer.Equals(expected, actual); - Assert.True(equal, $"Colors {expected} and {actual} are not equal at index {i}"); + Assert.True(ColorSpaceComparer.Equals(clampedExpected, clampedActual), $"Colors {clampedExpected} and {clampedActual} are not equal at index {index}."); } - private static void ValidateCmyk(in JpegColorConverterBase.ComponentValues values, in JpegColorConverterBase.ComponentValues result, int i) + /// + /// Compares two component planes using an absolute floating-point tolerance. + /// + /// The scalar reference values. + /// The adaptive traversal values. + /// The maximum permitted absolute difference. + private static void CompareSequence(Span expected, Span actual, float tolerance) { - float c = values.Component0[i]; - float m = values.Component1[i]; - float y = values.Component2[i]; - float k = values.Component3[i] / MaxColorChannelValue; - - float r = c * k / MaxColorChannelValue; - float g = m * k / MaxColorChannelValue; - float b = y * k / MaxColorChannelValue; - Rgb expected = Rgb.Clamp(new Rgb(r, g, b)); - - Rgb actual = Rgb.Clamp(new Rgb(result.Component0[i], result.Component1[i], result.Component2[i])); + Assert.Equal(expected.Length, actual.Length); - bool equal = ColorSpaceComparer.Equals(expected, actual); - Assert.True(equal, $"Colors {expected} and {actual} are not equal at index {i}"); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i], tolerance); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorPackingTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorPackingTests.cs new file mode 100644 index 000000000..20b4f1679 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorPackingTests.cs @@ -0,0 +1,168 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg; + +/// +/// Tests the planar and packed buffer transformations used around JPEG color-profile conversion. +/// +[Trait("Format", "Jpg")] +public class JpegColorPackingTests +{ + private static readonly int[] Lengths = [0, 1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 32, 33, 129]; + + /// + /// Verifies every packing operation against its scalar definition with and without hardware intrinsics. + /// + [Fact] + public void PackingOperationsMatchScalarDefinitions() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePackingOperations, HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Exercises every SIMD transition and scalar remainder for the packing operations. + /// + private static void ValidatePackingOperations() + { + foreach (int length in Lengths) + { + ValidatePackedNormalizeInterleave3(length); + ValidateUnpackDeinterleave3(length); + ValidatePackedNormalizeInterleave4(length); + ValidatePackedInvertNormalizeInterleave4(length); + } + } + + /// + /// Compares normalized three-plane interleaving with the original scalar loop. + /// + /// The number of samples in each component plane. + private static void ValidatePackedNormalizeInterleave3(int length) + { + const float scale = 1F / 255F; + float[] x = CreateSamples(length, 1); + float[] y = CreateSamples(length, 2); + float[] z = CreateSamples(length, 3); + float[] expected = new float[length * 3]; + float[] actual = new float[length * 3]; + + for (int i = 0; i < length; i++) + { + int packedOffset = i * 3; + expected[packedOffset] = x[i] * scale; + expected[packedOffset + 1] = y[i] * scale; + expected[packedOffset + 2] = z[i] * scale; + } + + JpegColorConverterBase.PackedNormalizeInterleave3(x, y, z, actual, scale); + + Assert.Equal(expected, actual); + } + + /// + /// Compares packed three-channel deinterleaving with the original scalar loop. + /// + /// The number of packed values. + private static void ValidateUnpackDeinterleave3(int length) + { + Vector3[] packed = new Vector3[length]; + float[] expectedX = CreateSamples(length, 1); + float[] expectedY = CreateSamples(length, 2); + float[] expectedZ = CreateSamples(length, 3); + float[] actualX = new float[length]; + float[] actualY = new float[length]; + float[] actualZ = new float[length]; + + for (int i = 0; i < length; i++) + { + packed[i] = new Vector3(expectedX[i], expectedY[i], expectedZ[i]); + } + + JpegColorConverterBase.UnpackDeinterleave3(packed, actualX, actualY, actualZ); + + Assert.Equal(expectedX, actualX); + Assert.Equal(expectedY, actualY); + Assert.Equal(expectedZ, actualZ); + } + + /// + /// Compares normalized four-plane interleaving with the original scalar loop. + /// + /// The number of samples in each component plane. + private static void ValidatePackedNormalizeInterleave4(int length) + { + const float maximumValue = 255F; + const float scale = 1F / maximumValue; + float[] x = CreateSamples(length, 1); + float[] y = CreateSamples(length, 2); + float[] z = CreateSamples(length, 3); + float[] w = CreateSamples(length, 4); + float[] expected = new float[length * 4]; + float[] actual = new float[length * 4]; + + for (int i = 0; i < length; i++) + { + int packedOffset = i * 4; + expected[packedOffset] = x[i] * scale; + expected[packedOffset + 1] = y[i] * scale; + expected[packedOffset + 2] = z[i] * scale; + expected[packedOffset + 3] = w[i] * scale; + } + + JpegColorConverterBase.PackedNormalizeInterleave4(x, y, z, w, actual, maximumValue); + + Assert.Equal(expected, actual); + } + + /// + /// Compares inverted normalized four-plane interleaving with the original scalar loop. + /// + /// The number of samples in each component plane. + private static void ValidatePackedInvertNormalizeInterleave4(int length) + { + const float maximumValue = 255F; + const float scale = 1F / maximumValue; + float[] x = CreateSamples(length, 1); + float[] y = CreateSamples(length, 2); + float[] z = CreateSamples(length, 3); + float[] w = CreateSamples(length, 4); + float[] expected = new float[length * 4]; + float[] actual = new float[length * 4]; + + for (int i = 0; i < length; i++) + { + int packedOffset = i * 4; + expected[packedOffset] = (maximumValue - x[i]) * scale; + expected[packedOffset + 1] = (maximumValue - y[i]) * scale; + expected[packedOffset + 2] = (maximumValue - z[i]) * scale; + expected[packedOffset + 3] = (maximumValue - w[i]) * scale; + } + + JpegColorConverterBase.PackedInvertNormalizeInterleave4(x, y, z, w, actual, maximumValue); + + Assert.Equal(expected, actual); + } + + /// + /// Creates deterministic, non-integral sample values that expose lane-order and arithmetic mistakes. + /// + /// The number of samples to create. + /// The one-based component number used to distinguish each plane. + /// The generated sample values. + private static float[] CreateSamples(int length, int component) + { + float[] samples = new float[length]; + + for (int i = 0; i < samples.Length; i++) + { + // The relatively prime multipliers produce a distinct sequence for each plane + // while keeping every value inside the eight-bit JPEG sample domain. + samples[i] = (((i * 37) + (component * 53)) % 251) + (component * 0.125F); + } + + return samples; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs index f147e4132..e7e6d2033 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs @@ -30,7 +30,7 @@ public partial class JpegDecoderTests } using Image image = provider.GetImage(JpegDecoder.Instance); - image.DebugSave(provider, testOutputDetails: nonContiguousBuffersStr); + image.DebugSave(provider, testOutputDetails: nonContiguousBuffersStr, appendPixelTypeToFileName: false); provider.Utility.TestName = DecodeBaselineJpegOutputName; image.CompareToReferenceOutput( diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs index a5472e1ae..fff1e085b 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs @@ -21,7 +21,7 @@ public partial class JpegDecoderTests where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(JpegDecoder.Instance); - image.DebugSave(provider); + image.DebugSave(provider, appendPixelTypeToFileName: false); provider.Utility.TestName = DecodeProgressiveJpegOutputName; image.CompareToReferenceOutput( diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 3fd55eb91..2f84a1a7b 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -418,6 +418,21 @@ public partial class JpegDecoderTests image.CompareToReferenceOutput(provider); } + [Theory] + [WithFile(TestImages.Jpeg.ICC.Issue3064, PixelTypes.Rgba32)] + public void Decode_RGB_ICC_Jpeg_Issue3064(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + JpegDecoderOptions options = new() + { + GeneralOptions = new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert } + }; + + using Image image = provider.GetImage(JpegDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + // https://github.com/SixLabors/ImageSharp/issues/2948 [Theory] [WithFile(TestImages.Jpeg.Issues.Issue2948, PixelTypes.Rgb24)] @@ -433,4 +448,80 @@ public partial class JpegDecoderTests [InlineData(TestImages.Jpeg.Issues.Issue2948)] public void Issue2948_No_SOS_Identify_Throws_InvalidImageContentException(string imagePath) => Assert.Throws(() => _ = Image.Identify(TestFile.Create(imagePath).Bytes)); + + [Fact] + public void Issue_3071_Decode_TruncatedJpeg_Throws_InvalidImageContentException() + => Assert.Throws(() => + { + // SOI marker (FF D8) + garbage bytes — only 11 bytes + byte[] data = [0xFF, 0xD8, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]; + using Image image = Image.Load(data); + }); + + // https://github.com/SixLabors/ImageSharp/issues/3118 + [Theory] + [WithFile(TestImages.Jpeg.Issues.Issue3118, PixelTypes.Rgb24)] + public void Issue3118_Multiple_SOF_WithSOS_DoesNotThrow(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(JpegDecoder.Instance); + image.DebugSave(provider); + } + + [Fact] + public void Identify_MalformedApp13Segment_IgnoresNonCriticalErrorsByDefault() + { + ImageInfo info = Image.Identify(CreateJpegWithMalformedApp13Segment()); + Assert.True(info.Width > 0); + Assert.True(info.Height > 0); + } + + [Fact] + public void Decode_MalformedApp13Segment_IgnoresNonCriticalErrorsByDefault() + { + using Image image = Image.Load(CreateJpegWithMalformedApp13Segment()); + Assert.True(image.Width > 0); + Assert.True(image.Height > 0); + } + + [Fact] + public void Identify_MalformedApp13Segment_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => Image.Identify(options, CreateJpegWithMalformedApp13Segment())); + } + + [Fact] + public void Decode_MalformedApp13Segment_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => + { + using Image image = Image.Load(options, CreateJpegWithMalformedApp13Segment()); + }); + } + + private static byte[] CreateJpegWithMalformedApp13Segment() + { + byte[] source = TestFile.Create(TestImages.Jpeg.Baseline.Calliphora).Bytes; + + // This APP13 segment starts with the valid "Photoshop 3.0\0" identifier, but the remaining + // payload does not begin with the required "8BIM" image resource block signature. + byte[] malformedApp13 = + [ + 0xFF, 0xED, + 0x00, 0x1D, + (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', 0x00, + (byte)'B', (byte)'a', (byte)'d', (byte)'R', (byte)'e', (byte)'s', (byte)'o', (byte)'u', (byte)'r', (byte)'c', (byte)'e', (byte)'!', (byte)'!' + ]; + + byte[] payload = new byte[source.Length + malformedApp13.Length]; + payload[0] = source[0]; + payload[1] = source[1]; + + Buffer.BlockCopy(malformedApp13, 0, payload, 2, malformedApp13.Length); + Buffer.BlockCopy(source, 2, payload, 2 + malformedApp13.Length, source.Length - 2); + + return payload; + } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index ede147dbe..5fae85cc8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -49,7 +49,7 @@ public partial class JpegEncoderTests public static readonly TheoryData CmykEncodingSetups = new() { - { JpegColorType.Cmyk, 100, 0.0159f / 100 }, + { JpegColorType.Cmyk, 100, 0.0164f / 100 }, { JpegColorType.Cmyk, 80, 0.3922f / 100 }, { JpegColorType.Cmyk, 40, 0.6488f / 100 }, }; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 903a6b076..1610df6eb 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -119,7 +119,7 @@ public class SpectralJpegTests this.Output.WriteLine($"Component{i}: [total: {total} | average: {average}]"); averageDifference += average; totalDifference += total; - Size s = libJpegComponent.SpectralBlocks.Size(); + Size s = libJpegComponent.SpectralBlocks.Size; tolerance += s.Width * s.Height; } diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs index 476538ccd..5c52c9785 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs @@ -123,6 +123,19 @@ public class PbmDecoderTests appendPixelTypeToFileName: false); } + [Theory] + [InlineData("P2")] + [InlineData("P3")] + [InlineData("P5")] + [InlineData("P6")] + public void Decode_MaxPixelValueZero_ThrowsInvalidImageContentException(string magic) + { + byte[] bytes = Encoding.ASCII.GetBytes($"{magic} 1 1 0\n0"); + using MemoryStream stream = new(bytes); + + Assert.Throws(() => Image.Load(stream)); + } + [Fact] public void PlainText_PrematureEof() { diff --git a/tests/ImageSharp.Tests/Formats/Png/PngCgbiProcessorTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngCgbiProcessorTests.cs new file mode 100644 index 000000000..863c7fc4f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngCgbiProcessorTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Png; + +[Trait("Format", "Png")] +public class PngCgbiProcessorTests +{ + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(3)] + [InlineData(4)] + [InlineData(7)] + [InlineData(8)] + [InlineData(15)] + [InlineData(16)] + [InlineData(17)] + [InlineData(31)] + [InlineData(32)] + [InlineData(33)] + [InlineData(64)] + public void ApplyTransform_RgbWithAlpha_MatchesScalar(int pixelCount) => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + serialized => AssertApplyTransformMatchesScalar(FeatureTestRunner.Deserialize(serialized)), + pixelCount, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertApplyTransformMatchesScalar(int pixelCount) + { + byte[] input = CreateBgraScanline(pixelCount); + byte[] processorOutput = (byte[])input.Clone(); + byte[] scalarOutput = (byte[])input.Clone(); + + PngCgbiProcessor.ApplyTransform(Configuration.Default, processorOutput, PngColorType.RgbWithAlpha); + ApplyCgbiTransformScalarReference(scalarOutput); + + Assert.Equal(scalarOutput, processorOutput); + } + + /// + /// Builds synthetic input for tests. Produces inputs that exercise all three alpha cases. + /// + /// Channel values laid out as a defiltered CgBI scanline in premultiplied BGRA order + private static byte[] CreateBgraScanline(int pixelCount) + { + // The distinct strides keep the channels from being equal to each other or constant across pixels + // So a bug that e.g. swaps two channels or reuses one channel's value doesn't accidentally pass + const int alphaCaseCount = 7; + const int redStride = 13; + const int greenStride = 29; + const int blueStride = 53; + + byte[] bytes = new byte[pixelCount * 4]; + for (int p = 0; p < pixelCount; p++) + { + // Cycling alpha through [0..255], and an odd partial value every pixel rotation + // ensures all three branches get covered within any scanline of 3 or more pixels + byte a = (p % alphaCaseCount) switch + { + 0 => byte.MinValue, + 1 => byte.MaxValue, + _ => (byte)(((p * 37) + 23) | 1) // Produce a spread of alpha values and make sure to never get 0 + }; + + int offset = p * 4; + bytes[offset + 0] = Premultiply((byte)(p * blueStride), a); + bytes[offset + 1] = Premultiply((byte)(p * greenStride), a); + bytes[offset + 2] = Premultiply((byte)(p * redStride), a); + bytes[offset + 3] = a; + } + + return bytes; + + // CgBI stores channels premultiplied by alpha + static byte Premultiply(byte channel, byte alpha) => (byte)(channel * alpha / byte.MaxValue); + } + + private static void ApplyCgbiTransformScalarReference(Span scanline) + { + Span pixels = MemoryMarshal.Cast(scanline); + for (int i = 0; i < pixels.Length; i++) + { + ref Rgba32 pixel = ref pixels[i]; + pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A); + + byte a = pixel.A; + if (a is 0 or byte.MaxValue) + { + continue; + } + + int half = a >> 1; + byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a); + byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a); + byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a); + pixel = new Rgba32(r, g, b, a); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index 8dfa98748..ed33f7163 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -20,40 +20,40 @@ public partial class PngDecoderTests private static readonly byte[] Raw1X1PngIhdrAndpHYs = [ // PNG Identifier - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, - // IHDR - 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, - 0x00, 0x00, 0x00, + // IHDR + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, + 0x00, 0x00, 0x00, - // IHDR CRC - 0x90, 0x77, 0x53, 0xDE, + // IHDR CRC + 0x90, 0x77, 0x53, 0xDE, - // pHYS - 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, - 0x00, 0x0E, 0xC3, 0x00, 0x00, 0x0E, 0xC3, 0x01, + // pHYS + 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, + 0x00, 0x0E, 0xC3, 0x00, 0x00, 0x0E, 0xC3, 0x01, - // pHYS CRC - 0xC7, 0x6F, 0xA8, 0x64 + // pHYS CRC + 0xC7, 0x6F, 0xA8, 0x64 ]; // Contains the png marker, IDAT and IEND chunks of a 1x1 pixel 32bit png 1 a single black pixel. private static readonly byte[] Raw1X1PngIdatAndIend = [ // IDAT - 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x18, - 0x57, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04, - 0x00, 0x01, + 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x18, + 0x57, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x01, - // IDAT CRC - 0x5C, 0xCD, 0xFF, 0x69, + // IDAT CRC + 0x5C, 0xCD, 0xFF, 0x69, - // IEND - 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, + // IEND + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, - // IEND CRC - 0xAE, 0x42, 0x60, 0x82 + // IEND CRC + 0xAE, 0x42, 0x60, 0x82 ]; [Theory] @@ -70,13 +70,136 @@ public partial class PngDecoderTests WriteChunk(memStream, chunkName); WriteDataChunk(memStream); - ImageFormatException exception = + InvalidImageContentException exception = Assert.Throws(() => PngDecoder.Instance.Decode(DecoderOptions.Default, memStream)); Assert.Equal($"CRC Error. PNG {chunkName} chunk is corrupt!", exception.Message); } } + // https://github.com/SixLabors/ImageSharp/issues/3078 + [Fact] + public void Decode_TruncatedPhysChunk_ExceptionIsThrown() + { + // 24 bytes — PNG signature + truncated pHYs chunk + byte[] payload = Convert.FromHexString( + "89504e470d0a1a0a3030303070485973" + + "3030303030303030"); + + using MemoryStream stream = new(payload); + InvalidImageContentException exception = Assert.Throws(() => Image.Load(stream)); + + Assert.Equal("pHYs chunk is too short", exception.Message); + } + + // https://github.com/SixLabors/ImageSharp/issues/3093 + [Fact] + public void Decode_TruncatedFrameControlChunk_ExceptionIsThrown() + { + // PNG signature + truncated frame control chunk + byte[] payload = Convert.FromHexString( + "89504e470d0a1a0a424d3a00000000007f000000000028030405060000000100" + + "000101002000000000000000000000000000ff00006663544cff190000000000" + + "010000424d000100000101002000000000"); + + using MemoryStream stream = new(payload); + InvalidImageContentException exception = Assert.Throws(() => Image.Load(stream)); + + Assert.Equal("The frame control chunk does not contain enough data!", exception.Message); + } + + // https://github.com/SixLabors/ImageSharp/issues/3079 + [Fact] + public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow() + { + byte[] payload = [137, 80, 78, 71, 13, 10, 26, 10, // PNG signature + 0, 0, 0, 13, // chunk length 13 bytes + 73, 72, 68, 82, // chunk type IHDR + 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, // data + 55, 110, 249, 36, // checksum + 0, 0, 0, 2, // chunk length + 122, 84, 88, 116, // chunk type zTXt + 1, 0, // truncated data + 100, 138, 166, 20, // crc + 0, 0, 0, 10, // chunk length 10 bytes + 73, 68, 65, 84, // chunk type IDAT + 120, 1, 99, 96, 0, 0, 0, 2, 0, 1, // data + 115, 117, 1, 24, // checksum + 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; // end chunk + + using MemoryStream stream = new(payload); + using Image image = Image.Load(stream); + } + + // https://github.com/SixLabors/ImageSharp/issues/3079 + [Fact] + public void Decode_InternationalText_WithTruncatedData_DoesNotThrow() + { + byte[] payload = [137, 80, 78, 71, 13, 10, 26, 10, // PNG signature + 0, 0, 0, 13, // chunk length 13 bytes + 73, 72, 68, 82, // chunk type IHDR + 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, // data + 55, 110, 249, 36, // checksum + 0, 0, 0, 2, // chunk length + 105, 84, 88, 116, // chunk type iTXt + 1, 0, // truncated data + 225, 200, 214, 33, // crc + 0, 0, 0, 10, // chunk length 10 bytes + 73, 68, 65, 84, // chunk type IDAT + 120, 1, 99, 96, 0, 0, 0, 2, 0, 1, // data + 115, 117, 1, 24, // checksum + 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; // end chunk + + using MemoryStream stream = new(payload); + using Image image = Image.Load(stream); + } + + // https://github.com/SixLabors/ImageSharp/issues/3079 + [Fact] + public void Decode_InternationalText_WithTruncatedDataAfterLanguageTag_DoesNotThrow() + { + byte[] payload = [137, 80, 78, 71, 13, 10, 26, 10, // PNG signature + 0, 0, 0, 13, // chunk length 13 bytes + 73, 72, 68, 82, // chunk type IHDR + 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, // data + 55, 110, 249, 36, // checksum + 0, 0, 0, 21, // chunk length + 105, 84, 88, 116, // chunk type iTXt + 73, 110, 116, 101, 114, 110, 97, 116, 105, 111, 110, 97, 108, 50, 0, 0, 0, 114, 117, 115, 0, // truncated data after language tag + 225, 200, 214, 33, // crc + 0, 0, 0, 10, // chunk length 10 bytes + 73, 68, 65, 84, // chunk type IDAT + 120, 1, 99, 96, 0, 0, 0, 2, 0, 1, // data + 115, 117, 1, 24, // checksum + 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; // end chunk + + using MemoryStream stream = new(payload); + using Image image = Image.Load(stream); + } + + [Fact] + public void Decode_tRnsChunk_WithAlphaLengthGreaterColorTableLength_ShouldNotThrowException() + { + byte[] payload = [137, 80, 78, 71, 13, 10, 26, 10, // PNG signature + 0, 0, 0, 13, // chunk length 13 bytes + 73, 72, 68, 82, // chunk type IHDR + 0, 0, 0, 1, 0, 0, 0, 1, 8, 3, 0, 0, 0, // data + 40, 203, 52, 187, // crc + 0, 0, 0, 6, // chunk length 6 bytes + 80, 76, 84, 69, // chunk type palettte + 255, 0, 0, 0, 255, 0, // data + 210, 135, 239, 113, // crc + 0, 0, 0, 18, // chunk length + 116, 82, 78, 83, // chunk type tRns + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, // data + 0, 0, 0, 10, // chunk length + 73, 68, 65, 84, // chunk type data + 120, 156, 99, 96, 0, 0, 0, 2, 0, 1, 72, 175, 164, 113]; // alpha.Length > colorTable.Length + + using MemoryStream stream = new(payload); + using Image image = Image.Load(stream); + } + private static string GetChunkTypeName(uint value) { byte[] data = new byte[4]; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index a58101a6b..88ed512d1 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; using System.Runtime.Intrinsics.X86; using Microsoft.DotNet.RemoteExecutor; using SixLabors.ImageSharp.Formats; @@ -405,12 +406,97 @@ public partial class PngDecoderTests TestFile testFile = TestFile.Create(imagePath); using MemoryStream stream = new(testFile.Bytes, false); - ImageInfo imageInfo = Image.Identify(new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData }, stream); + ImageInfo imageInfo = Image.Identify(new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }, stream); Assert.NotNull(imageInfo); Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel); } + [Fact] + public void Identify_AnimatedPng_ReadsFrameCountCorrectly() + { + TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount); + + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + Assert.Equal(48, imageInfo.FrameMetadataCollection.Count); + } + + [Fact] + public void Identify_AnimatedPngWithMaxFrames_ReadsFrameCountCorrectly() + { + TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount); + + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(new DecoderOptions { MaxFrames = 40 }, stream); + + Assert.NotNull(imageInfo); + Assert.Equal(40, imageInfo.FrameMetadataCollection.Count); + } + + [Fact] + public void Load_AnimatedPngWithMaxFrames_ReadsFrameCountCorrectly() + { + TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount); + + using MemoryStream stream = new(testFile.Bytes, false); + using Image image = Image.Load(new DecoderOptions { MaxFrames = 40 }, stream); + + Assert.NotNull(image); + Assert.Equal(40, image.Frames.Count); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + [InlineData(10)] + [InlineData(100)] + public void Identify_AnimatedPng_FrameCount_MatchesDecode(int frameCount) + { + using Image image = new(10, 10, Color.Red.ToPixel()); + for (int i = 1; i < frameCount; i++) + { + using ImageFrame frame = new(Configuration.Default, 10, 10); + image.Frames.AddFrame(frame); + } + + using MemoryStream stream = new(); + image.Save(stream, new PngEncoder()); + stream.Position = 0; + + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + Assert.Equal(frameCount, imageInfo.FrameMetadataCollection.Count); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + [InlineData(10)] + [InlineData(100)] + public void Decode_AnimatedPng_FrameCount(int frameCount) + { + using Image image = new(10, 10, Color.Red.ToPixel()); + for (int i = 1; i < frameCount; i++) + { + using ImageFrame frame = new(Configuration.Default, 10, 10); + image.Frames.AddFrame(frame); + } + + using MemoryStream stream = new(); + image.Save(stream, new PngEncoder()); + stream.Position = 0; + + using Image decoded = Image.Load(stream); + + Assert.Equal(frameCount, decoded.Frames.Count); + } + [Theory] [WithFile(TestImages.Png.Bad.MissingDataChunk, PixelTypes.Rgba32)] public void Decode_MissingDataChunk_ThrowsException(TestImageProvider provider) @@ -509,7 +595,7 @@ public partial class PngDecoderTests public void Decode_InvalidDataChunkCrc_IgnoreCrcErrors(TestImageProvider provider, bool compare) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData }); + using Image image = provider.GetImage(PngDecoder.Instance, new DecoderOptions() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }); image.DebugSave(provider); if (compare) @@ -629,26 +715,133 @@ public partial class PngDecoderTests Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToPixel().A < 255); } - // https://github.com/SixLabors/ImageSharp/issues/410 [Theory] - [WithFile(TestImages.Png.Bad.Issue410_MalformedApplePng, PixelTypes.Rgba32)] - public void Issue410_MalformedApplePng(TestImageProvider provider) + [WithFile(TestImages.Png.Cgbi.Issue410, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Cgbi.Colors, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Cgbi.Clocks, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Cgbi.Screen, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Cgbi.Flecks, PixelTypes.Rgb24)] + public void Decode_AppleCgBI(TestImageProvider provider) where TPixel : unmanaged, IPixel + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunDecodeAppleCgbi, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic, + provider, + provider.PixelType.ToString()); + + private static void RunDecodeAppleCgbi(string providerDump, string pixelType) { - Exception ex = Record.Exception( - () => - { - using Image image = provider.GetImage(PngDecoder.Instance); - image.DebugSave(provider); + if (Enum.Parse(pixelType) == PixelTypes.Rgb24) + { + TestImageProvider provider = + FeatureTestRunner.DeserializeForXunit>(providerDump); - // We don't have another x-plat reference decoder that can be compared for this image. - if (TestEnvironment.IsWindows) - { - image.CompareToOriginal(provider, ImageComparer.Exact, SystemDrawingReferenceDecoder.Png); - } - }); - Assert.NotNull(ex); - Assert.Contains("Proprietary Apple PNG detected!", ex.Message); + using Image image = provider.GetImage(PngDecoder.Instance); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider, ImageComparer.Exact); + + return; + } + + TestImageProvider rgbaProvider = + FeatureTestRunner.DeserializeForXunit>(providerDump); + + using Image rgbaImage = rgbaProvider.GetImage(PngDecoder.Instance); + rgbaImage.DebugSave(rgbaProvider); + rgbaImage.CompareToReferenceOutput(rgbaProvider, ImageComparer.Exact); + } + + [Theory] + [InlineData(TestImages.Png.Cgbi.Colors, 120, 120)] + [InlineData(TestImages.Png.Cgbi.Issue410, 42, 26)] + [InlineData(TestImages.Png.Cgbi.Flecks, 510, 512)] + public void Identify_AppleCgBI(string imagePath, int expectedWidth, int expectedHeight) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + Assert.Equal(PngFormat.Instance, imageInfo.Metadata.DecodedImageFormat); + Assert.Equal(expectedWidth, imageInfo.Width); + Assert.Equal(expectedHeight, imageInfo.Height); + } + + [Theory] + [InlineData(TestImages.Png.Cgbi.BitDepth16)] + [InlineData(TestImages.Png.Cgbi.Palette)] + public void Identify_CgBI_IncompatibleHeader_ThrowsInvalidImageContentException(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + InvalidImageContentException ex = Assert.Throws(() => Image.Identify(stream)); + Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message); + } + + [Theory] + [WithFile(TestImages.Png.Cgbi.BitDepth16, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Cgbi.Palette, PixelTypes.Rgba32)] + public void Decode_CgBI_IncompatibleHeader_ThrowsInvalidImageContentException(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + InvalidImageContentException ex = Assert.Throws( + () => { using Image image = provider.GetImage(PngDecoder.Instance); }); + Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message); + } + + [Theory] + [InlineData(TestImages.Png.Cgbi.BitDepth16)] + [InlineData(TestImages.Png.Cgbi.Palette)] + public void Identify_CgBI_AfterHeader_IncompatibleHeader_ThrowsInvalidImageContentException(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + byte[] reordered = MoveFirstPngChunkAfterSecond(testFile.Bytes); + using MemoryStream stream = new(reordered, false); + + InvalidImageContentException ex = Assert.Throws(() => Image.Identify(stream)); + Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message); + } + + [Theory] + [InlineData(TestImages.Png.Cgbi.BitDepth16)] + [InlineData(TestImages.Png.Cgbi.Palette)] + public void Decode_CgBI_AfterHeader_IncompatibleHeader_ThrowsInvalidImageContentException(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + byte[] reordered = MoveFirstPngChunkAfterSecond(testFile.Bytes); + using MemoryStream stream = new(reordered, false); + + InvalidImageContentException ex = Assert.Throws( + () => { using Image image = PngDecoder.Instance.Decode(DecoderOptions.Default, stream); }); + Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message); + } + + /// + /// Moves the first PNG chunk after the second while preserving each chunk's data and CRC. + /// + /// A PNG whose first two chunks should be exchanged. + /// A copy of the PNG with its first two chunks exchanged. + private static byte[] MoveFirstPngChunkAfterSecond(byte[] source) + { + const int signatureLength = 8; + const int chunkOverheadLength = 12; + + int firstChunkLength = BinaryPrimitives.ReadInt32BigEndian(source.AsSpan(signatureLength, 4)) + chunkOverheadLength; + int secondChunkOffset = signatureLength + firstChunkLength; + int secondChunkLength = BinaryPrimitives.ReadInt32BigEndian(source.AsSpan(secondChunkOffset, 4)) + chunkOverheadLength; + byte[] reordered = new byte[source.Length]; + + source.AsSpan(0, signatureLength).CopyTo(reordered); + source.AsSpan(secondChunkOffset, secondChunkLength).CopyTo(reordered.AsSpan(signatureLength)); + source.AsSpan(signatureLength, firstChunkLength).CopyTo(reordered.AsSpan(signatureLength + secondChunkLength)); + + // Chunk CRCs cover only each chunk's type and data, so moving complete chunks + // leaves both checksums valid and isolates ordering as the tested behavior. + source.AsSpan(secondChunkOffset + secondChunkLength) + .CopyTo(reordered.AsSpan(signatureLength + secondChunkLength + firstChunkLength)); + + return reordered; } [Theory] @@ -690,7 +883,7 @@ public partial class PngDecoderTests public void Binary_PrematureEof() { PngDecoder decoder = PngDecoder.Instance; - PngDecoderOptions options = new() { GeneralOptions = new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreData } }; + PngDecoderOptions options = new() { GeneralOptions = new DecoderOptions { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData } }; using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(TestImages.Png.Bad.FlagOfGermany0000016446, decoder, options); // TODO: Try to reduce this to 1. diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs index 3a31b395f..423d54e5f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs @@ -63,9 +63,7 @@ public class PngEncoderFilterTests : MeasureFixture data.TestFilter(); } - FeatureTestRunner.RunWithHwIntrinsicsFeature( - RunTest, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); } [Fact] @@ -208,6 +206,95 @@ public class PngEncoderFilterTests : MeasureFixture HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); } + /// + /// Verifies every encoder across pixel strides, register boundaries, and hardware widths. + /// + [Fact] + public void EncodeMatchesReferencesAcrossRegisterBoundaries() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertEncodersMatchReferences, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Compares every filter with its independent scalar reference across SIMD boundaries and pixel strides. + /// + private static void AssertEncodersMatchReferences() + { + int[] bytesPerPixels = [1, 2, 3, 4, 6, 8]; + int[] lengths = [8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 131, 132, 133, 135, 136, 137, 257]; + + foreach (int bytesPerPixel in bytesPerPixels) + { + foreach (int length in lengths) + { + if (length < bytesPerPixel) + { + continue; + } + + byte[] scanline = new byte[length]; + byte[] previousScanline = new byte[length]; + Random random = new((bytesPerPixel * 397) + length); + random.NextBytes(scanline); + random.NextBytes(previousScanline); + + AssertFilterMatchesReference(PngFilterMethod.Sub, scanline, previousScanline, bytesPerPixel); + + AssertFilterMatchesReference(PngFilterMethod.Up, scanline, previousScanline, bytesPerPixel); + + AssertFilterMatchesReference(PngFilterMethod.Average, scanline, previousScanline, bytesPerPixel); + + AssertFilterMatchesReference(PngFilterMethod.Paeth, scanline, previousScanline, bytesPerPixel); + } + } + } + + /// + /// Compares one filter result and variance sum with its scalar reference. + /// + /// The filter to evaluate. + /// The current scanline. + /// The preceding scanline. + /// The component distance between adjacent pixels. + private static void AssertFilterMatchesReference(PngFilterMethod filter, byte[] scanline, byte[] previousScanline, int bytesPerPixel) + { + byte[] expected = new byte[scanline.Length + 1]; + byte[] actual = new byte[scanline.Length + 1]; + int expectedSum; + int actualSum; + + switch (filter) + { + case PngFilterMethod.Sub: + ReferenceImplementations.EncodeSubFilter(scanline, expected, bytesPerPixel, out expectedSum); + SubFilter.Encode(scanline, actual, bytesPerPixel, out actualSum); + break; + + case PngFilterMethod.Up: + ReferenceImplementations.EncodeUpFilter(scanline, previousScanline, expected, out expectedSum); + UpFilter.Encode(scanline, previousScanline, actual, out actualSum); + break; + + case PngFilterMethod.Average: + ReferenceImplementations.EncodeAverageFilter(scanline, previousScanline, expected, bytesPerPixel, out expectedSum); + + AverageFilter.Encode(scanline, previousScanline, actual, (uint)bytesPerPixel, out actualSum); + + break; + + case PngFilterMethod.Paeth: + ReferenceImplementations.EncodePaethFilter(scanline, previousScanline, expected, bytesPerPixel, out expectedSum); + + PaethFilter.Encode(scanline, previousScanline, actual, bytesPerPixel, out actualSum); + + break; + + default: + throw new InvalidOperationException(); + } + + Assert.Equal(expectedSum, actualSum); + Assert.Equal(expected, actual); + } + public class TestData { private readonly PngFilterMethod filter; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 4ebcbc13b..eef8d5ba8 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -680,7 +680,7 @@ public partial class PngEncoderTests PaletteQuantizer quantizer = new( palette.Select(Color.FromPixel).ToArray(), - new QuantizerOptions { ColorMatchingMode = ColorMatchingMode.Hybrid }); + new QuantizerOptions { ColorMatchingMode = ColorMatchingMode.Exact }); using MemoryStream ms = new(); image.Save(ms, new PngEncoder diff --git a/tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs index 31ec27da0..5ec23b146 100644 --- a/tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs +++ b/tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.Formats.Qoi; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs index d19f27807..6aebf7b9c 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs @@ -88,6 +88,21 @@ public class BigTiffMetadataTests Assert.Equal(ExifDataType.Long8, long8.DataType); } + [Fact] + public void ExifLong8Array_CanWriteValuesAsLong() + { + ExifLong8Array long8 = new(ExifTagValue.StripOffsets); + Assert.True(long8.TrySetValue(new long[] { 1, uint.MaxValue })); + Assert.Equal(ExifDataType.Long, long8.DataType); + + byte[] buffer = new byte[8]; + int written = ExifWriter.WriteValue(long8, buffer, 0); + + Assert.Equal(buffer.Length, written); + Assert.Equal(1U, BinaryPrimitives.ReadUInt32LittleEndian(buffer)); + Assert.Equal(uint.MaxValue, BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(4))); + } + [Fact] public void ExifSignedLong8Array() { diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbaTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbaTiffColorTests.cs new file mode 100644 index 000000000..6a0afcd85 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbaTiffColorTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation; + +[Trait("Format", "Tiff")] +public class RgbaTiffColorTests : PhotometricInterpretationTestBase +{ + [Fact] + public void DecodeUsesAlphaChannelBitDepth() + { + // The single pixel packs 4-bit RGB followed by 8-bit alpha. Using the blue-channel width for alpha would consume + // only the first alpha nibble and leave the decoded opacity at 8 instead of 128. + byte[] input = [0xF0, 0x88, 0x00]; + Rgba32[][] expected = [[new Rgba32(255, 0, 136, 128)]]; + + AssertDecode(expected, pixels => + { + RgbaTiffColor color = new(TiffExtraSampleType.UnassociatedAlphaData, new TiffBitsPerSample(4, 4, 4, 8)); + color.Decode(input, pixels, 0, 0, 1, 1); + }); + } + + [Fact] + public void DecodeAssociatedAlphaRestoresLogicalColor() + { + byte[] input = [64, 32, 16, 128]; + Rgba32[][] expected = [[new Rgba32(128, 64, 32, 128)]]; + + AssertDecode(expected, pixels => + { + RgbaTiffColor color = new(TiffExtraSampleType.AssociatedAlphaData, new TiffBitsPerSample(8, 8, 8, 8)); + color.Decode(input, pixels, 0, 0, 1, 1); + }); + } + + [Fact] + public void DecodeUnassociatedAlphaAssociatesDestinationStorage() + { + byte[] input = [128, 64, 32, 128]; + using Image image = new(1, 1); + + RgbaTiffColor color = new(TiffExtraSampleType.UnassociatedAlphaData, new TiffBitsPerSample(8, 8, 8, 8)); + color.Decode(input, image.Frames.RootFrame.PixelBuffer, 0, 0, 1, 1); + + // The decoder receives straight TIFF channels, while Rgba32P stores RGB multiplied by its quantized alpha byte. + Assert.Equal(new Rgba32P(64, 32, 16, 128), image[0, 0]); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index e432a7251..ec6113be3 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -4,9 +4,12 @@ // ReSharper disable InconsistentNaming using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; @@ -367,6 +370,22 @@ public class TiffDecoderTests : TiffDecoderBaseTester Assert.Null(image.Metadata.IccProfile); } + [Theory] + [WithFile(Icc.PerceptualRgb8, PixelTypes.Rgba32)] + [WithFile(Icc.PerceptualRgb16, PixelTypes.Rgba32)] + public void Decode_WhenColorProfileHandlingIsPreserve_PreservesIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + using Image image = provider.GetImage(TiffDecoder.Instance, options); + + Assert.NotNull(image.Metadata.IccProfile); + Assert.NotSame(image.Frames.RootFrame.Metadata.IccProfile, image.Metadata.IccProfile); + Assert.Equal( + image.Frames.RootFrame.Metadata.IccProfile.ToByteArray(), + image.Metadata.IccProfile.ToByteArray()); + } + [Theory] [WithFile(Issues2454_A, PixelTypes.Rgba32)] [WithFile(Issues2454_B, PixelTypes.Rgba32)] @@ -388,7 +407,15 @@ public class TiffDecoderTests : TiffDecoderBaseTester where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(TiffDecoder.Instance); - image.DebugSave(provider); + PngEncoder encoder = new() + { + BitDepth = PngBitDepth.Bit16, + ColorType = PngColorType.RgbWithAlpha + }; + + // This exact Rgba64 comparison requires a 16-bit reference. The Windows reference encoder uses a 32-bit + // System.Drawing bitmap and would otherwise quantize each channel to 8 bits while producing the PNG. + image.DebugSave(provider, testOutputDetails: null, extension: "png", encoder: encoder); image.CompareToReferenceOutput(ImageComparer.Exact, provider); } @@ -707,17 +734,17 @@ public class TiffDecoderTests : TiffDecoderBaseTester [WithFile(YCbCrJpegCompressed2, PixelTypes.Rgba32)] [WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)] [WithFile(GrayscaleJpegCompressed, PixelTypes.Rgba32)] - [WithFile(Issues2123, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false); + [WithFile(Issues2123, PixelTypes.Rgba32, 0.110f)] + public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider, float tolerance = 0.001f) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false, compareTolerance: tolerance); [Theory] [WithFile(RgbOldJpegCompressed, PixelTypes.Rgba32)] - [WithFile(RgbOldJpegCompressed2, PixelTypes.Rgba32)] + [WithFile(RgbOldJpegCompressed2, PixelTypes.Rgba32, 0.1003f)] [WithFile(RgbOldJpegCompressed3, PixelTypes.Rgba32)] [WithFile(RgbOldJpegCompressedGray, PixelTypes.Rgba32)] [WithFile(YCbCrOldJpegCompressed, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_OldJpegCompressed(TestImageProvider provider) + public void TiffDecoder_CanDecode_OldJpegCompressed(TestImageProvider provider, float tolerance = 0.001f) where TPixel : unmanaged, IPixel { DecoderOptions decoderOptions = new() @@ -728,7 +755,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester image.DebugSave(provider); image.CompareToOriginal( provider, - ImageComparer.Tolerant(0.001f), + ImageComparer.Tolerant(tolerance), ReferenceDecoder, decoderOptions); } @@ -784,7 +811,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester // The image is handcrafted to simulate issue 2679. ImageMagick will throw an expection here and wont decode, // so we compare to rererence output instead. - image.DebugSave(provider); + image.DebugSave(provider, appendPixelTypeToFileName: false); image.CompareToReferenceOutput( ImageComparer.Exact, provider, @@ -868,4 +895,40 @@ public class TiffDecoderTests : TiffDecoderBaseTester [WithFile(Issue2983, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_Issue2983(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Fact] + public void Identify_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault() + { + ImageInfo info = Image.Identify(CreateTiffWithMalformedIccProfile()); + Assert.Equal(1, info.Width); + Assert.Equal(1, info.Height); + } + + [Fact] + public void Decode_MalformedIccProfile_IgnoresNonCriticalErrorsByDefault() + { + using Image image = Image.Load(CreateTiffWithMalformedIccProfile()); + Assert.Equal(1, image.Width); + Assert.Equal(1, image.Height); + } + + [Fact] + public void Identify_MalformedIccProfile_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => Image.Identify(options, CreateTiffWithMalformedIccProfile())); + } + + [Fact] + public void Decode_MalformedIccProfile_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => + { + using Image image = Image.Load(options, CreateTiffWithMalformedIccProfile()); + }); + } + + private static byte[] CreateTiffWithMalformedIccProfile() + => CorruptedMetadataImageFactory.CreateImageWithMalformedIccProfile(new TiffEncoder()); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 8d75cd9bc..9f0f0e991 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestDataIcc; using SixLabors.ImageSharp.Tests.TestUtilities; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; @@ -149,13 +150,22 @@ public class TiffMetadataTests Assert.NotNull(meta); if (ignoreMetadata) { + Assert.Null(image.Metadata.XmpProfile); + Assert.Null(image.Metadata.ExifProfile); Assert.Null(rootFrameMetaData.XmpProfile); Assert.Null(rootFrameMetaData.ExifProfile); } else { + Assert.NotNull(image.Metadata.XmpProfile); + Assert.NotNull(image.Metadata.ExifProfile); Assert.NotNull(rootFrameMetaData.XmpProfile); Assert.NotNull(rootFrameMetaData.ExifProfile); + + Assert.NotSame(rootFrameMetaData.XmpProfile, image.Metadata.XmpProfile); + Assert.NotSame(rootFrameMetaData.ExifProfile, image.Metadata.ExifProfile); + Assert.Equal(rootFrameMetaData.XmpProfile.Data, image.Metadata.XmpProfile.Data); + Assert.Equal(rootFrameMetaData.ExifProfile.ToByteArray(), image.Metadata.ExifProfile.ToByteArray()); Assert.Equal(2596, rootFrameMetaData.XmpProfile.Data.Length); // padding bytes are trimmed Assert.Equal(25, rootFrameMetaData.ExifProfile.Values.Count); } @@ -170,6 +180,10 @@ public class TiffMetadataTests IptcProfile iptcProfile = image.Frames.RootFrame.Metadata.IptcProfile; Assert.NotNull(iptcProfile); + Assert.NotNull(image.Metadata.IptcProfile); + Assert.NotSame(iptcProfile, image.Metadata.IptcProfile); + Assert.Equal(iptcProfile.Data, image.Metadata.IptcProfile.Data); + IptcValue byline = iptcProfile.Values.FirstOrDefault(data => data.Tag == IptcTag.Byline); Assert.NotNull(byline); Assert.Equal("Studio Mantyniemi", byline.Value); @@ -335,8 +349,7 @@ public class TiffMetadataTests iptcProfile.SetValue(IptcTag.Name, "Test name"); rootFrameInput.Metadata.IptcProfile = iptcProfile; - IccProfileHeader iccProfileHeader = new() { Class = IccProfileClass.ColorSpace }; - IccProfile iccProfile = new(); + IccProfile iccProfile = new(IccTestDataProfiles.ProfileRandomArray); rootFrameInput.Metadata.IccProfile = iccProfile; TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata(); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffUtilitiesTest.cs b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffUtilitiesTest.cs index 6483b63c5..104ab76a7 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffUtilitiesTest.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffUtilitiesTest.cs @@ -20,6 +20,26 @@ public class TiffUtilitiesTest Assert.Equal(default, actual); } + [Fact] + public void ColorScaleTo24BitPremultiplied_WithZeroAlpha_ReturnsDefaultPixel() + { + Rgba64 unassociated = TiffUtilities.ColorScaleTo24BitPremultiplied(0x123456, 0x654321, 0xABCDEF, 0); + Rgba32P associated = TiffUtilities.ColorScaleTo24BitPremultiplied(0x123456, 0x654321, 0xABCDEF, 0); + + Assert.Equal(default, unassociated); + Assert.Equal(default, associated); + } + + [Fact] + public void ColorScaleTo32BitPremultiplied_WithZeroAlpha_ReturnsDefaultPixel() + { + Rgba64 unassociated = TiffUtilities.ColorScaleTo32BitPremultiplied(0x12345678, 0x654321AB, 0xABCDEF12, 0); + Rgba32P associated = TiffUtilities.ColorScaleTo32BitPremultiplied(0x12345678, 0x654321AB, 0xABCDEF12, 0); + + Assert.Equal(default, unassociated); + Assert.Equal(default, associated); + } + [Theory] [InlineData(65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535)] [InlineData(32767, 0, 0, 65535, 32767, 0, 0, 65535)] @@ -36,9 +56,11 @@ public class TiffUtilitiesTest [InlineData(32766, 0, 0, 32766, 65535, 0, 0, 32766)] // Red, 50% Alpha [InlineData(0, 32766, 0, 32766, 0, 65535, 0, 32766)] // Green, 50% Alpha [InlineData(0, 0, 32766, 32766, 0, 0, 65535, 32766)] // Blue, 50% Alpha - [InlineData(8191, 0, 0, 16383, 32765, 0, 0, 16383)] // Red, 25% Alpha - [InlineData(0, 8191, 0, 16383, 0, 32765, 0, 16383)] // Green, 25% Alpha - [InlineData(0, 0, 8191, 16383, 0, 0, 32765, 16383)] // Blue, 25% Alpha + /* The exact 25% result lies below the 16-bit midpoint by less than one float ULP. The Vector4 conversion therefore + reaches the midpoint and applies the established round-to-even contract without requiring a comparison tolerance. */ + [InlineData(8191, 0, 0, 16383, 32766, 0, 0, 16383)] // Red, 25% Alpha + [InlineData(0, 8191, 0, 16383, 0, 32766, 0, 16383)] // Green, 25% Alpha + [InlineData(0, 0, 8191, 16383, 0, 0, 32766, 16383)] // Blue, 25% Alpha [InlineData(8191, 0, 0, 0, 0, 0, 0, 0)] // Red, 0% Alpha [InlineData(0, 8191, 0, 0, 0, 0, 0, 0)] // Green, 0% Alpha [InlineData(0, 0, 8191, 0, 0, 0, 0, 0)] // Blue, 0% Alpha diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index f9836ffb1..ee8268716 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -135,9 +135,9 @@ public class WebpEncoderTests // Alpha thresholding is 64/255F. GifEncoder gifEncoder = new() { - Quantizer = new OctreeQuantizer(options) + Quantizer = new HexadecatreeQuantizer(options) }; - provider.Utility.SaveTestOutputFile(image, "gif", gifEncoder, "octree"); + provider.Utility.SaveTestOutputFile(image, "gif", gifEncoder, "hexadecatree"); gifEncoder = new GifEncoder { @@ -152,8 +152,8 @@ public class WebpEncoderTests }; using Image cloned1 = image.Clone(); - cloned1.Mutate(c => c.Quantize(new OctreeQuantizer(options))); - provider.Utility.SaveTestOutputFile(cloned1, "webp", encoder, "octree"); + cloned1.Mutate(c => c.Quantize(new HexadecatreeQuantizer(options))); + provider.Utility.SaveTestOutputFile(cloned1, "webp", encoder, "hexadecatree"); using Image cloned2 = image.Clone(); cloned2.Mutate(c => c.Quantize(new WuQuantizer(options))); @@ -162,7 +162,7 @@ public class WebpEncoderTests // Now blend the images with a blue background and save as webp. using Image background1 = new(image.Width, image.Height, Color.White.ToPixel()); background1.Mutate(c => c.DrawImage(cloned1, 1)); - provider.Utility.SaveTestOutputFile(background1, "webp", encoder, "octree-blended"); + provider.Utility.SaveTestOutputFile(background1, "webp", encoder, "hexadecatree-blended"); using Image background2 = new(image.Width, image.Height, Color.White.ToPixel()); background2.Mutate(c => c.DrawImage(cloned2, 1)); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 020b42f37..394479f89 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -200,4 +200,32 @@ public class WebpMetaDataTests }); Assert.Null(ex); } + + [Fact] + public void Identify_InvalidExifChunk_IgnoresNonCriticalErrorsByDefault() + { + using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false); + ImageInfo info = Image.Identify(stream); + Assert.True(info.Width > 0); + Assert.True(info.Height > 0); + } + + [Fact] + public void Identify_InvalidExifChunk_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false); + Assert.Throws(() => Image.Identify(options, stream)); + } + + [Fact] + public void Decode_InvalidExifChunk_ThrowsWithStrict() + { + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false); + Assert.Throws(() => + { + using Image image = Image.Load(options, stream); + }); + } } diff --git a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs index 0ccb80d3f..1b87819b6 100644 --- a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs @@ -13,7 +13,7 @@ public class GraphicsOptionsTests private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone(); [Fact] - public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null); + public void CloneGraphicsOptionsIsNotNull() => Assert.NotNull(this.cloneGraphicsOptions); [Fact] public void DefaultGraphicsOptionsAntialias() @@ -23,35 +23,35 @@ public class GraphicsOptionsTests } [Fact] - public void DefaultGraphicsOptionsAntialiasSuppixelDepth() + public void DefaultGraphicsOptionsAntialiasThreshold() { - const int Expected = 16; - Assert.Equal(Expected, this.newGraphicsOptions.AntialiasSubpixelDepth); - Assert.Equal(Expected, this.cloneGraphicsOptions.AntialiasSubpixelDepth); + const float expected = .5F; + Assert.Equal(expected, this.newGraphicsOptions.AntialiasThreshold); + Assert.Equal(expected, this.cloneGraphicsOptions.AntialiasThreshold); } [Fact] public void DefaultGraphicsOptionsBlendPercentage() { - const float Expected = 1F; - Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage); - Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage); + const float expected = 1F; + Assert.Equal(expected, this.newGraphicsOptions.BlendPercentage); + Assert.Equal(expected, this.cloneGraphicsOptions.BlendPercentage); } [Fact] public void DefaultGraphicsOptionsColorBlendingMode() { - const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; - Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode); - Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode); + const PixelColorBlendingMode expected = PixelColorBlendingMode.Normal; + Assert.Equal(expected, this.newGraphicsOptions.ColorBlendingMode); + Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode); } [Fact] public void DefaultGraphicsOptionsAlphaCompositionMode() { - const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; - Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode); - Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode); + const PixelAlphaCompositionMode expected = PixelAlphaCompositionMode.SrcOver; + Assert.Equal(expected, this.newGraphicsOptions.AlphaCompositionMode); + Assert.Equal(expected, this.cloneGraphicsOptions.AlphaCompositionMode); } [Fact] @@ -61,7 +61,7 @@ public class GraphicsOptionsTests { AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, Antialias = false, - AntialiasSubpixelDepth = 23, + AntialiasThreshold = .33F, BlendPercentage = .25F, ColorBlendingMode = PixelColorBlendingMode.HardLight, }; @@ -79,7 +79,7 @@ public class GraphicsOptionsTests actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; actual.Antialias = false; - actual.AntialiasSubpixelDepth = 23; + actual.AntialiasThreshold = .67F; actual.BlendPercentage = .25F; actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; diff --git a/tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs b/tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs index 1bf137f34..903e997aa 100644 --- a/tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ColorNumericsTests.cs @@ -18,11 +18,65 @@ public class ColorNumericsTests Vector4 vector = new(x, y, z, 0.0f); // act - int actual = ColorNumerics.GetBT709Luminance(ref vector, luminanceLevels); + int actual = ColorNumerics.GetBT709Luminance(vector, luminanceLevels); // assert Assert.Equal(expected, actual); } - // TODO: We need to test all ColorNumerics methods! + [Theory] + [InlineData((ushort)0, (byte)0)] + [InlineData((ushort)128, (byte)0)] + [InlineData((ushort)129, (byte)1)] + [InlineData((ushort)257, (byte)1)] + [InlineData((ushort)32896, (byte)128)] + [InlineData(ushort.MaxValue, byte.MaxValue)] + public void From16BitTo8Bit_ReturnsExpectedValue(ushort component, byte expected) + { + byte actual = ColorNumerics.From16BitTo8Bit(component); + + Assert.Equal(expected, actual); + } + + [Fact] + public void From16BitTo8Bit_RoundTripsAllExpanded8BitValues() + { + for (int i = 0; i <= byte.MaxValue; i++) + { + byte expected = (byte)i; + ushort component = ColorNumerics.From8BitTo16Bit(expected); + + byte actual = ColorNumerics.From16BitTo8Bit(component); + + Assert.Equal(expected, actual); + } + } + + [Theory] + [InlineData(0U, (byte)0)] + [InlineData(8421504U, (byte)0)] + [InlineData(8421505U, (byte)1)] + [InlineData(16843009U, (byte)1)] + [InlineData(2155905152U, (byte)128)] + [InlineData(uint.MaxValue, byte.MaxValue)] + public void From32BitTo8Bit_ReturnsExpectedValue(uint component, byte expected) + { + byte actual = ColorNumerics.From32BitTo8Bit(component); + + Assert.Equal(expected, actual); + } + + [Fact] + public void From32BitTo8Bit_RoundTripsAllExpanded8BitValues() + { + for (int i = 0; i <= byte.MaxValue; i++) + { + byte expected = (byte)i; + uint component = ColorNumerics.From8BitTo32Bit(expected); + + byte actual = ColorNumerics.From32BitTo8Bit(component); + + Assert.Equal(expected, actual); + } + } } diff --git a/tests/ImageSharp.Tests/Helpers/NumericsTests.cs b/tests/ImageSharp.Tests/Helpers/NumericsTests.cs index 1106144c4..35109d352 100644 --- a/tests/ImageSharp.Tests/Helpers/NumericsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/NumericsTests.cs @@ -11,6 +11,11 @@ public class NumericsTests private readonly ApproximateFloatComparer approximateFloatComparer = new(1e-6f); + /// + /// Gets lengths that straddle the Vector4 block boundaries used by the 128-, 256-, and 512-bit kernels. + /// + public static TheoryData AssociationSpanLengths => new() { 0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63 }; + [Theory] [InlineData(0)] [InlineData(1)] @@ -156,14 +161,23 @@ public class NumericsTests } [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(30)] - [InlineData(63)] + [MemberData(nameof(AssociationSpanLengths))] public void PremultiplyVectorSpan(int length) { Random rnd = new(42); Vector4[] source = rnd.GenerateRandomVectorArray(length, 0, 1); + + for (int i = 0; i < source.Length; i++) + { + // Exact zero and one exercise the special alpha boundaries alongside the random fractional values. + source[i].W = i % 5 switch + { + 0 => 0F, + 1 => 1F, + _ => source[i].W + }; + } + Vector4[] expected = source.Select(v => { Numerics.Premultiply(ref v); @@ -173,17 +187,32 @@ public class NumericsTests Numerics.Premultiply(source); Assert.Equal(expected, source, this.approximateFloatComparer); + + for (int i = 0; i < source.Length; i++) + { + // Alpha is storage metadata here and must survive each SIMD width without any floating-point transformation. + Assert.Equal(BitConverter.SingleToInt32Bits(expected[i].W), BitConverter.SingleToInt32Bits(source[i].W)); + } } [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(30)] - [InlineData(63)] + [MemberData(nameof(AssociationSpanLengths))] public void UnPremultiplyVectorSpan(int length) { Random rnd = new(42); Vector4[] source = rnd.GenerateRandomVectorArray(length, 0, 1); + + for (int i = 0; i < source.Length; i++) + { + // A zero alpha preserves the complete source vector by contract; one also verifies the identity case. + source[i].W = i % 5 switch + { + 0 => 0F, + 1 => 1F, + _ => source[i].W + }; + } + Vector4[] expected = source.Select(v => { Numerics.UnPremultiply(ref v); @@ -193,6 +222,12 @@ public class NumericsTests Numerics.UnPremultiply(source); Assert.Equal(expected, source, this.approximateFloatComparer); + + for (int i = 0; i < source.Length; i++) + { + // Alpha is the divisor and must still survive each SIMD width without any floating-point transformation. + Assert.Equal(BitConverter.SingleToInt32Bits(expected[i].W), BitConverter.SingleToInt32Bits(source[i].W)); + } } [Theory] diff --git a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs index 4b06f877f..017926fc5 100644 --- a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs @@ -13,6 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers; public class ParallelRowIteratorTests { + public delegate void BufferedRowAction(int y, Span span); public delegate void RowIntervalAction(RowInterval rows, Span span); private readonly ITestOutputHelper output; @@ -200,6 +201,29 @@ public class ParallelRowIteratorTests Assert.Equal(expectedData, actualData); } + [Fact] + public void IterateRows_MaxDegreeOfParallelismMinusOne_ShouldVisitAllRows() + { + ParallelExecutionSettings parallelSettings = new( + -1, + 10, + Configuration.Default.MemoryAllocator); + + Rectangle rectangle = new(0, 0, 10, 10); + int[] actualData = new int[rectangle.Height]; + + void RowAction(int y) => actualData[y]++; + + TestRowActionOperation operation = new(RowAction); + + ParallelRowIterator.IterateRows( + rectangle, + in parallelSettings, + in operation); + + Assert.Equal(Enumerable.Repeat(1, rectangle.Height), actualData); + } + public static TheoryData IterateRows_WithEffectiveMinimumPixelsLimit_Data = new() { @@ -296,6 +320,35 @@ public class ParallelRowIteratorTests Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); } + [Fact] + public void IterateRowIntervalsWithTempBuffer_MaxDegreeOfParallelismMinusOne_ShouldVisitAllRows() + { + ParallelExecutionSettings parallelSettings = new( + -1, + 10, + Configuration.Default.MemoryAllocator); + + Rectangle rectangle = new(0, 0, 10, 10); + int[] actualData = new int[rectangle.Height]; + + void RowAction(RowInterval rows, Span buffer) + { + for (int y = rows.Min; y < rows.Max; y++) + { + actualData[y]++; + } + } + + TestRowIntervalOperation operation = new(RowAction); + + ParallelRowIterator.IterateRowIntervals, Vector4>( + rectangle, + in parallelSettings, + in operation); + + Assert.Equal(Enumerable.Repeat(1, rectangle.Height), actualData); + } + public static readonly TheoryData IterateRectangularBuffer_Data = new() { @@ -445,6 +498,32 @@ public class ParallelRowIteratorTests } } + private readonly struct TestRowActionOperation : IRowOperation + { + private readonly Action action; + + public TestRowActionOperation(Action action) + => this.action = action; + + public void Invoke(int y) + => this.action(y); + } + + private readonly struct TestRowOperation : IRowOperation + where TBuffer : unmanaged + { + private readonly BufferedRowAction action; + + public TestRowOperation(BufferedRowAction action) + => this.action = action; + + public int GetRequiredBufferLength(Rectangle bounds) + => bounds.Width; + + public void Invoke(int y, Span span) + => this.action(y, span); + } + private readonly struct TestRowIntervalOperation : IRowIntervalOperation { private readonly Action action; diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs index d7f2c7d28..417ee18fb 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs @@ -64,7 +64,7 @@ public abstract partial class ImageFrameCollectionTests using ImageFrame addedFrame = this.Collection.AddFrame(Array.Empty()); }); - Assert.StartsWith($"Parameter \"data\" ({typeof(int)}) must be greater than or equal to {100}, was {0}", ex.Message); + Assert.StartsWith($"Parameter \"data\" ({typeof(long)}) must be greater than or equal to {100}, was {0}", ex.Message); } [Fact] diff --git a/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs b/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs index 47ae0daf7..171aa6d4c 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs @@ -52,5 +52,79 @@ public partial class ImageTests Assert.Equal(Color.White, Color.FromPixel(img[1, 0])); Assert.Equal(Color.Black, Color.FromPixel(img[1, 1])); } + + [Fact] + public void FromPixels_WithRowStride() + { + Rgba32[] data = + [ + Color.Black.ToPixel(), + Color.White.ToPixel(), + Color.Red.ToPixel(), // padding + Color.White.ToPixel(), + Color.Black.ToPixel(), + Color.Red.ToPixel() // padding + ]; + + using Image img = Image.LoadPixelData(data.AsSpan(), width: 2, height: 2, rowStride: 3); + Assert.NotNull(img); + Assert.Equal(Color.Black, Color.FromPixel(img[0, 0])); + Assert.Equal(Color.White, Color.FromPixel(img[0, 1])); + Assert.Equal(Color.White, Color.FromPixel(img[1, 0])); + Assert.Equal(Color.Black, Color.FromPixel(img[1, 1])); + } + + [Fact] + public void FromBytes_WithRowStrideInBytes() + { + byte[] data = + [ + 0, 0, 0, 255, // 0,0 + 255, 255, 255, 255, // 0,1 + 255, 0, 0, 255, // padding + 255, 255, 255, 255, // 1,0 + 0, 0, 0, 255, // 1,1 + 255, 0, 0, 255 // padding + ]; + + using Image img = Image.LoadPixelData(data.AsSpan(), width: 2, height: 2, rowStrideInBytes: 12); + Assert.NotNull(img); + Assert.Equal(Color.Black, Color.FromPixel(img[0, 0])); + Assert.Equal(Color.White, Color.FromPixel(img[0, 1])); + Assert.Equal(Color.White, Color.FromPixel(img[1, 0])); + Assert.Equal(Color.Black, Color.FromPixel(img[1, 1])); + } + + [Fact] + public void FromPixels_WithRowStride_InvalidStride_Throws() + { + Rgba32[] data = new Rgba32[6]; + Assert.ThrowsAny( + () => Image.LoadPixelData(data.AsSpan(), width: 2, height: 2, rowStride: 1)); + } + + [Fact] + public void FromPixels_WithRowStride_InvalidLength_Throws() + { + Rgba32[] data = new Rgba32[4]; + Assert.ThrowsAny( + () => Image.LoadPixelData(data.AsSpan(), width: 2, height: 2, rowStride: 3)); + } + + [Fact] + public void FromBytes_WithRowStrideInBytes_InvalidStride_Throws() + { + byte[] data = new byte[24]; + Assert.ThrowsAny( + () => Image.LoadPixelData(data.AsSpan(), width: 2, height: 2, rowStrideInBytes: 10)); + } + + [Fact] + public void FromBytes_WithRowStrideInBytes_InvalidLength_Throws() + { + byte[] data = new byte[19]; + Assert.ThrowsAny( + () => Image.LoadPixelData(data.AsSpan(), width: 2, height: 2, rowStrideInBytes: 12)); + } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 6322e65aa..65eeb124f 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -141,6 +141,103 @@ public partial class ImageTests } } + [Fact] + public void WrapMemory_MemoryOfT_Strided_CreatedImageIsCorrect() + { + Rgba32[] source = + [ + new Rgba32(1, 1, 1, 255), + new Rgba32(2, 2, 2, 255), + new Rgba32(3, 3, 3, 255), + new Rgba32(90, 90, 90, 255), + new Rgba32(4, 4, 4, 255), + new Rgba32(5, 5, 5, 255), + new Rgba32(6, 6, 6, 255), + new Rgba32(91, 91, 91, 255) + ]; + + using Image image = Image.WrapMemory(source.AsMemory(), width: 3, height: 2, rowStride: 4); + + Assert.Equal(4, image.Frames.RootFrame.PixelBuffer.RowStride); + Assert.False(image.DangerousTryGetSinglePixelMemory(out Memory _)); + Assert.Equal(source[0], image[0, 0]); + Assert.Equal(source[2], image[2, 0]); + Assert.Equal(source[4], image[0, 1]); + Assert.Equal(source[6], image[2, 1]); + } + + [Fact] + public void WrapMemory_MemoryOfT_Strided_CopyPixelDataTo_UsesRowStrideLayout() + { + Rgba32[] source = + [ + new Rgba32(1, 1, 1, 255), + new Rgba32(2, 2, 2, 255), + new Rgba32(3, 3, 3, 255), + new Rgba32(90, 90, 90, 255), + new Rgba32(4, 4, 4, 255), + new Rgba32(5, 5, 5, 255), + new Rgba32(6, 6, 6, 255), + new Rgba32(91, 91, 91, 255) + ]; + + using Image image = Image.WrapMemory(source.AsMemory(), width: 3, height: 2, rowStride: 4); + + Rgba32 sentinel = new(250, 1, 1, 255); + Rgba32[] destination = [sentinel, sentinel, sentinel, sentinel, sentinel, sentinel, sentinel]; + image.CopyPixelDataTo(destination); + + Assert.Equal(source[0], destination[0]); + Assert.Equal(source[1], destination[1]); + Assert.Equal(source[2], destination[2]); + Assert.Equal(sentinel, destination[3]); + Assert.Equal(source[4], destination[4]); + Assert.Equal(source[5], destination[5]); + Assert.Equal(source[6], destination[6]); + Assert.ThrowsAny(() => image.CopyPixelDataTo(new Rgba32[6])); + } + + [Fact] + public void WrapMemory_MemoryOfByte_Strided_CreatedImageIsCorrect() + { + int pixelSize = Unsafe.SizeOf(); + byte[] sourceBytes = new byte[8 * pixelSize]; + Span source = MemoryMarshal.Cast(sourceBytes.AsSpan()); + + source[0] = new Rgba32(1, 1, 1, 255); + source[1] = new Rgba32(2, 2, 2, 255); + source[2] = new Rgba32(3, 3, 3, 255); + source[4] = new Rgba32(4, 4, 4, 255); + source[5] = new Rgba32(5, 5, 5, 255); + source[6] = new Rgba32(6, 6, 6, 255); + + using Image image = Image.WrapMemory( + sourceBytes.AsMemory(), + width: 3, + height: 2, + rowStrideInBytes: 4 * pixelSize); + + Assert.Equal(4, image.Frames.RootFrame.PixelBuffer.RowStride); + Assert.False(image.DangerousTryGetSinglePixelMemory(out Memory _)); + Assert.Equal(source[0], image[0, 0]); + Assert.Equal(source[2], image[2, 0]); + Assert.Equal(source[4], image[0, 1]); + Assert.Equal(source[6], image[2, 1]); + } + + [Fact] + public void WrapMemory_Strided_InvalidStride_Throws() + { + Rgba32[] pixelSource = new Rgba32[8]; + byte[] byteSource = new byte[8 * Unsafe.SizeOf()]; + + Assert.ThrowsAny(() => Image.WrapMemory(pixelSource.AsMemory(), width: 3, height: 2, rowStride: 2)); + Assert.ThrowsAny(() => Image.WrapMemory(pixelSource.AsMemory(0, 6), width: 3, height: 2, rowStride: 4)); + + Assert.ThrowsAny(() => Image.WrapMemory(byteSource.AsMemory(), width: 3, height: 2, rowStrideInBytes: (4 * Unsafe.SizeOf()) - 1)); + Assert.ThrowsAny(() => Image.WrapMemory(byteSource.AsMemory(0, 6 * Unsafe.SizeOf()), width: 3, height: 2, rowStrideInBytes: 4 * Unsafe.SizeOf())); + } + [Fact] public void WrapSystemDrawingBitmap_WhenObserved() { diff --git a/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs b/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs index 27e42f84e..a5dfb0d23 100644 --- a/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs @@ -313,7 +313,15 @@ public abstract class ProcessPixelRowsTestBase protected internal override int GetBufferCapacityInBytes() => int.MaxValue; - public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) => - this.buffers.Pop() as IMemoryOwner; + protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) + { + object buffer = this.buffers.Pop(); + if (buffer is AllocationTrackedMemoryManager trackedBuffer) + { + return trackedBuffer; + } + + throw new InvalidMemoryOperationException("The requested buffer type does not match the mock allocator buffer type."); + } } } diff --git a/tests/ImageSharp.Tests/ImageInfoTests.cs b/tests/ImageSharp.Tests/ImageInfoTests.cs index 322b0af19..748c8a4f6 100644 --- a/tests/ImageSharp.Tests/ImageInfoTests.cs +++ b/tests/ImageSharp.Tests/ImageInfoTests.cs @@ -54,4 +54,34 @@ public class ImageInfoTests Assert.Equal(meta, info.Metadata); Assert.Equal(frameMetadata.Count, info.FrameMetadataCollection.Count); } + + [Fact] + public void GetPixelMemorySize_UsesSingleFrameWhenFrameMetadataIsEmpty() + { + const int width = 10; + const int height = 20; + + ImageMetadata meta = new() { DecodedImageFormat = PngFormat.Instance }; + meta.GetPngMetadata(); + + ImageInfo info = new(new Size(width, height), meta); + + Assert.Equal(width * height * 4, info.GetPixelMemorySize()); + } + + [Fact] + public void GetPixelMemorySize_UsesFrameMetadataCountWhenAvailable() + { + const int width = 10; + const int height = 20; + IReadOnlyList frameMetadata = [new(), new(), new()]; + + ImageMetadata meta = new() { DecodedImageFormat = PngFormat.Instance }; + meta.GetPngMetadata(); + + ImageInfo info = new(new Size(width, height), meta, frameMetadata); + + Assert.Equal(width * height * 4 * frameMetadata.Count, info.GetPixelMemorySize()); + } + } diff --git a/tests/ImageSharp.Tests/Issues/Issue_396.cs b/tests/ImageSharp.Tests/Issues/Issue_396.cs new file mode 100644 index 000000000..0d76f0c80 --- /dev/null +++ b/tests/ImageSharp.Tests/Issues/Issue_396.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Tests.Issues; + +// https://github.com/SixLabors/ImageSharp.Drawing/discussions/396 +public class Issue_396 +{ + [Theory] + [WithFile(TestImages.Png.Issue396Dragon, PixelTypes.Rgba32)] + public void GeneratesGifForInspection(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Image[] handImages = + [ + Image.Load(TestFile.Create(TestImages.Png.Issue396Hand1).Bytes), + Image.Load(TestFile.Create(TestImages.Png.Issue396Hand2).Bytes) + ]; + + try + { + using Image inputImage = provider.GetImage(); + using Image outputImage = new(handImages[0].Width, handImages[0].Height); + + const float scaleFactor = 0.6F; + const float squashStepFactor = 0.22F; + + for (int i = 0; i < handImages.Length; i++) + { + using Image frameImage = new(handImages[i].Width, handImages[i].Height); + float squashFactorY = 1 - (i * squashStepFactor); + + frameImage.ProcessPixelRows(accessor => + { + Rgba32 gray = Color.Gray.ToPixel(); + + for (int y = 0; y < accessor.Height; y++) + { + accessor.GetRowSpan(y).Fill(gray); + } + }); + + Rectangle targetRect = new( + (int)((frameImage.Width - (frameImage.Width * scaleFactor)) / 2), + (int)(frameImage.Height * (((1 - scaleFactor) / 2) + (scaleFactor * (1 - squashFactorY)))), + (int)(frameImage.Width * scaleFactor), + (int)(frameImage.Height * scaleFactor * squashFactorY)); + + using Image dragonFrame = inputImage.CloneAs(); + dragonFrame.Mutate(context => context.Resize(targetRect.Size)); + + frameImage.Mutate(context => + { + context.DrawImage(dragonFrame, targetRect.Location, 1F); + context.DrawImage(handImages[i], Point.Empty, 1F); + }); + + ImageFrame outputFrame = outputImage.Frames.AddFrame(frameImage.Frames.RootFrame); + GifFrameMetadata gifFrameMetadata = outputFrame.Metadata.GetGifMetadata(); + gifFrameMetadata.FrameDelay = 40; + gifFrameMetadata.DisposalMode = FrameDisposalMode.RestoreToBackground; + } + + for (int i = handImages.Length - 1; i > 0; i--) + { + outputImage.Frames.AddFrame(outputImage.Frames[i]); + } + + outputImage.Frames.RemoveFrame(0); + GifMetadata gifMetadata = outputImage.Metadata.GetGifMetadata(); + gifMetadata.RepeatCount = 0; + + Assert.Equal((handImages.Length * 2) - 1, outputImage.Frames.Count); + foreach (ImageFrame frame in outputImage.Frames) + { + Assert.Equal(FrameDisposalMode.RestoreToBackground, frame.Metadata.GetGifMetadata().DisposalMode); + } + + outputImage.DebugSaveMultiFrame(provider, "Issue396-source-frames", appendPixelTypeToFileName: false); + provider.Utility.SaveTestOutputFile( + outputImage, + "gif", + new GifEncoder(), + "Issue396-encoded", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + // Save and decode the GIF so the encoder's optimized frame diffs can be inspected separately. + using MemoryStream gifStream = new(); + outputImage.SaveAsGif(gifStream); + gifStream.Position = 0; + + using Image decodedImage = Image.Load(gifStream); + decodedImage.DebugSaveMultiFrame(provider, "Issue396-decoded-frames", appendPixelTypeToFileName: false); + + Assert.Equal(outputImage.Frames.Count, decodedImage.Frames.Count); + } + finally + { + foreach (Image handImage in handImages) + { + handImage.Dispose(); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs index 0e791c5d9..c33e6d107 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs @@ -48,6 +48,50 @@ public class SimpleGcMemoryAllocatorTests } } + [Fact] + public void Allocate_AccumulativeLimit_ReleasesOnOwnerDispose() + { + SimpleGcMemoryAllocator allocator = new(new MemoryAllocatorOptions + { + AccumulativeAllocationLimitMegabytes = 1 + }); + const int oneMb = 1 << 20; + + // Reserve the full limit with a single owner. + IMemoryOwner b0 = allocator.Allocate(oneMb); + + // Additional allocation should exceed the limit while the owner is live. + Assert.Throws(() => allocator.Allocate(1)); + + // Disposing the owner releases the reservation. + b0.Dispose(); + + // Allocation should succeed after the reservation is released. + allocator.Allocate(oneMb).Dispose(); + } + + [Fact] + public void AllocateGroup_AccumulativeLimit_ReleasesOnGroupDispose() + { + SimpleGcMemoryAllocator allocator = new(new MemoryAllocatorOptions + { + AccumulativeAllocationLimitMegabytes = 1 + }); + const int oneMb = 1 << 20; + + // Reserve the full limit with a single group. + MemoryGroup g0 = allocator.AllocateGroup(oneMb, 1024); + + // Additional allocation should exceed the limit while the group is live. + Assert.Throws(() => allocator.AllocateGroup(1, 1024)); + + // Disposing the group releases the reservation. + g0.Dispose(); + + // Allocation should succeed after the reservation is released. + allocator.AllocateGroup(oneMb, 1024).Dispose(); + } + [StructLayout(LayoutKind.Explicit, Size = 512)] private struct BigStruct { diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs index c1f5b44bf..b4593d4d0 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs @@ -16,8 +16,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests { public class BufferTests1 : BufferTestSuite { - private static MemoryAllocator CreateMemoryAllocator() => - new UniformUnmanagedMemoryPoolMemoryAllocator( + private static UniformUnmanagedMemoryPoolMemoryAllocator CreateMemoryAllocator() => + new( sharedArrayPoolThresholdInBytes: 1024, poolBufferSizeInBytes: 2048, maxPoolSizeInBytes: 2048 * 4, @@ -31,8 +31,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests public class BufferTests2 : BufferTestSuite { - private static MemoryAllocator CreateMemoryAllocator() => - new UniformUnmanagedMemoryPoolMemoryAllocator( + private static UniformUnmanagedMemoryPoolMemoryAllocator CreateMemoryAllocator() => + new( sharedArrayPoolThresholdInBytes: 512, poolBufferSizeInBytes: 1024, maxPoolSizeInBytes: 1024 * 4, @@ -179,8 +179,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests g1.Dispose(); // Do some unmanaged allocations to make sure new non-pooled unmanaged allocations will grab different memory: - IntPtr dummy1 = Marshal.AllocHGlobal((IntPtr)B(8)); - IntPtr dummy2 = Marshal.AllocHGlobal((IntPtr)B(8)); + IntPtr dummy1 = Marshal.AllocHGlobal(checked((IntPtr)B(8))); + IntPtr dummy2 = Marshal.AllocHGlobal(checked((IntPtr)B(8))); using MemoryGroup g2 = allocator.AllocateGroup(B(8), 1024); using MemoryGroup g3 = allocator.AllocateGroup(B(8), 1024); @@ -323,7 +323,7 @@ public class UniformUnmanagedPoolMemoryAllocatorTests } [MethodImpl(MethodImplOptions.NoInlining)] - private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length) + private static void AllocateGroupAndForget(MemoryAllocator allocator, int length) { // Allocate a group and drop the reference without disposing. // The test relies on the group's finalizer to return the rented memory to the pool. @@ -387,8 +387,34 @@ public class UniformUnmanagedPoolMemoryAllocatorTests } } + [Theory] + [InlineData(1)] // SharedArrayPoolBuffer + [InlineData(2)] // UniformUnmanagedMemoryPool buffer + public void Allocate_AccumulativeLimit_Finalization_ReleasesOwnerReservation(int megabytes) + { + RemoteExecutor.Invoke(RunTest, megabytes.ToString(CultureInfo.InvariantCulture)).Dispose(); + + static void RunTest(string megabytesStr) + { + int megabytesInner = int.Parse(megabytesStr, CultureInfo.InvariantCulture); + int length = megabytesInner * (1 << 20); + MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions + { + AccumulativeAllocationLimitMegabytes = megabytesInner + }); + + AllocateSingleAndForget(allocator, length); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + allocator.Allocate(length).Dispose(); + } + } + [MethodImpl(MethodImplOptions.NoInlining)] - private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length) + private static void AllocateSingleAndForget(MemoryAllocator allocator, int length) { // Allocate and intentionally do not dispose. IMemoryOwner g = allocator.Allocate(length); @@ -410,15 +436,26 @@ public class UniformUnmanagedPoolMemoryAllocatorTests } [Fact] - public void Issue2001_NegativeMemoryReportedByGc() + public void AllocateGroup_AccumulativeLimit_Finalization_ReleasesGroupReservation() { RemoteExecutor.Invoke(RunTest).Dispose(); static void RunTest() { - // Emulate GC.GetGCMemoryInfo() issue https://github.com/dotnet/runtime/issues/65466 - UniformUnmanagedMemoryPoolMemoryAllocator.GetTotalAvailableMemoryBytes = () => -402354176; - _ = MemoryAllocator.Create(); + const int megabytes = 5; + int length = megabytes * (1 << 20); + MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions + { + AccumulativeAllocationLimitMegabytes = megabytes + }); + + AllocateGroupAndForget(allocator, length); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + allocator.AllocateGroup(length, 1024).Dispose(); } } @@ -446,6 +483,107 @@ public class UniformUnmanagedPoolMemoryAllocatorTests Assert.Throws(() => allocator.AllocateGroup(5 * oneMb, 1024)); } + [Fact] + public void Allocate_AccumulativeLimit_ReleasesOnOwnerDispose() + { + MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions + { + AccumulativeAllocationLimitMegabytes = 1 + }); + const int oneMb = 1 << 20; + + // Reserve the full limit with a single owner. + IMemoryOwner b0 = allocator.Allocate(oneMb); + + // Additional allocation should exceed the limit while the owner is live. + Assert.Throws(() => allocator.Allocate(1)); + + // Disposing the owner releases the reservation. + b0.Dispose(); + + // Allocation should succeed after the reservation is released. + allocator.Allocate(oneMb).Dispose(); + } + + [Fact] + public void AllocateGroup_AccumulativeLimit_ReleasesOnGroupDispose() + { + MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions + { + AccumulativeAllocationLimitMegabytes = 1 + }); + const int oneMb = 1 << 20; + + // Reserve the full limit with a single group. + MemoryGroup g0 = allocator.AllocateGroup(oneMb, 1024); + + // Additional allocation should exceed the limit while the group is live. + Assert.Throws(() => allocator.AllocateGroup(1, 1024)); + + // Disposing the group releases the reservation. + g0.Dispose(); + + // Allocation should succeed after the reservation is released. + allocator.AllocateGroup(oneMb, 1024).Dispose(); + } + + [Fact] + public void AllocateGroup_AccumulativeLimit_NonPoolFallback_TracksOncePerGroup() + { + // Configure the pool with zero capacity so multi-segment requests bypass both the + // single-buffer-from-pool path and MemoryGroup.TryAllocate(pool, ...) and fall + // through to MemoryGroup.Allocate(nonPoolAllocator, ...). The unmanaged segment + // size is small enough that the request must span multiple segments, which is the + // path where per-segment double-counting could regress. + UniformUnmanagedMemoryPoolMemoryAllocator allocator = new( + sharedArrayPoolThresholdInBytes: 64 * 1024, + poolBufferSizeInBytes: 128 * 1024, + maxPoolSizeInBytes: 0, + unmanagedBufferSizeInBytes: 256 * 1024, + new MemoryAllocatorOptions { AccumulativeAllocationLimitMegabytes = 1 }); + + // 768 KB exceeds the pool buffer size, so the request takes the multi-segment + // non-pool fallback (three 256 KB segments). If tracking double-counted (group + // plus each segment), reservation would be 768 KB + 768 KB = 1.5 MB and exceed + // the 1 MB limit on allocation itself. + MemoryGroup g = allocator.AllocateGroup(768 * 1024, 1024); + Assert.True(g.Count > 1, "Test setup must exercise the multi-segment fallback path."); + + // Reservation should be exactly 768 KB; another 512 KB would push to 1.25 MB and throw. + Assert.Throws(() => allocator.Allocate(512 * 1024)); + + g.Dispose(); + + // After disposal the reservation is fully released; a second equivalent group succeeds. + allocator.AllocateGroup(768 * 1024, 1024).Dispose(); + } + + [Fact] + public void AllocateGroup_AccumulativeLimit_NonPoolFallback_Finalization_ReleasesGroupReservation() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + UniformUnmanagedMemoryPoolMemoryAllocator allocator = new( + sharedArrayPoolThresholdInBytes: 64 * 1024, + poolBufferSizeInBytes: 128 * 1024, + maxPoolSizeInBytes: 0, + unmanagedBufferSizeInBytes: 256 * 1024, + new MemoryAllocatorOptions { AccumulativeAllocationLimitMegabytes = 1 }); + + // This exercises the non-pool multi-segment fallback, where reservation ownership has + // to follow the finalizable segment guards because the MemoryGroup itself has no finalizer. + AllocateGroupAndForget(allocator, 768 * 1024); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + allocator.AllocateGroup(768 * 1024, 1024).Dispose(); + } + } + [ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))] public void MemoryAllocator_Create_SetHighLimit() { diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.CopyTo.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.CopyTo.cs new file mode 100644 index 000000000..b19095be9 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.CopyTo.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Memory; + +public partial class Buffer2DTests +{ + [Fact] + public void CopyToSpan_StridedSource_WritesDestinationUsingSourceStride() + { + int[] source = [1, 2, 3, 777, 4, 5, 6, 888]; + int[] destination = [-1, -1, -1, -1, -1, -1, -1]; + + using Buffer2D buffer = Buffer2D.WrapMemory(source.AsMemory(), width: 3, height: 2, stride: 4); + buffer.CopyTo(destination); + + Assert.Equal(new[] { 1, 2, 3, -1, 4, 5, 6 }, destination); + } + + [Fact] + public void CopyToSpan_PackedSource_WritesPackedDestination() + { + int[] source = [1, 2, 3, 4, 5, 6]; + int[] destination = new int[6]; + + using Buffer2D buffer = Buffer2D.WrapMemory(source.AsMemory(), width: 3, height: 2); + buffer.CopyTo(destination); + + Assert.Equal(new[] { 1, 2, 3, 4, 5, 6 }, destination); + } + + [Fact] + public void CopyToSpan_StridedSource_ThrowsWhenDestinationTooShort() + { + int[] source = [1, 2, 3, 777, 4, 5, 6, 888]; + int[] destination = new int[6]; + + using Buffer2D buffer = Buffer2D.WrapMemory(source.AsMemory(), width: 3, height: 2, stride: 4); + Assert.Throws(() => buffer.CopyTo(destination)); + } +} diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs index e17c8c46f..6e9069a45 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Memory; @@ -15,26 +15,24 @@ public partial class Buffer2DTests [Fact] public void SwapOrCopyContent_WhenBothAllocated() { - using (Buffer2D a = this.memoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean)) - using (Buffer2D b = this.memoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean)) - { - a[1, 3] = 666; - b[1, 3] = 444; + using Buffer2D a = this.memoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean); + using Buffer2D b = this.memoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean); + a[1, 3] = 666; + b[1, 3] = 444; - Memory aa = a.FastMemoryGroup.Single(); - Memory bb = b.FastMemoryGroup.Single(); + Memory aa = a.FastMemoryGroup.Single(); + Memory bb = b.FastMemoryGroup.Single(); - Buffer2D.SwapOrCopyContent(a, b); + Buffer2D.SwapOrCopyContent(a, b); - Assert.Equal(bb, a.FastMemoryGroup.Single()); - Assert.Equal(aa, b.FastMemoryGroup.Single()); + Assert.Equal(bb, a.FastMemoryGroup.Single()); + Assert.Equal(aa, b.FastMemoryGroup.Single()); - Assert.Equal(new Size(3, 7), a.Size()); - Assert.Equal(new Size(10, 5), b.Size()); + Assert.Equal(new Size(3, 7), a.Size); + Assert.Equal(new Size(10, 5), b.Size); - Assert.Equal(666, b[1, 3]); - Assert.Equal(444, a[1, 3]); - } + Assert.Equal(666, b[1, 3]); + Assert.Equal(444, a[1, 3]); } [Fact] @@ -152,5 +150,74 @@ public partial class Buffer2DTests Assert.Equal(color, source.MemoryGroup[0].Span[10]); Assert.NotEqual(color, dest.MemoryGroup[0].Span[10]); } + + [Fact] + public void WhenDestIsNotMemoryOwner_DifferentSizeSameTotal_PackedLayout_ShouldCopy() + { + int[] data = new int[6]; + using TestMemoryManager destOwner = new(data); + using Buffer2D dest = new(MemoryGroup.Wrap(destOwner.Memory), 2, 3); + using Buffer2D source = this.memoryAllocator.Allocate2D(3, 2, AllocationOptions.Clean); + + source[0, 0] = 1; + source[1, 0] = 2; + source[2, 0] = 3; + source[0, 1] = 4; + source[1, 1] = 5; + source[2, 1] = 6; + + bool swap = Buffer2D.SwapOrCopyContent(dest, source); + + Assert.False(swap); + Assert.Equal(new Size(3, 2), dest.Size); + Assert.Equal(6, dest[2, 1]); + } + + [Fact] + public void WhenDestIsNotMemoryOwner_DifferentSizeSameTotal_StridedLayout_ShouldCopy() + { + int[] data = new int[5]; + using Buffer2D dest = Buffer2D.WrapMemory(data.AsMemory(), width: 2, height: 2, stride: 3); + using Buffer2D source = this.memoryAllocator.Allocate2D(1, 5, AllocationOptions.Clean); + + source[0, 0] = 1; + source[0, 1] = 2; + source[0, 2] = 3; + source[0, 3] = 4; + source[0, 4] = 5; + + bool swap = Buffer2D.SwapOrCopyContent(dest, source); + + Assert.False(swap); + Assert.Equal(new Size(1, 5), dest.Size); + Assert.Equal(1, dest[0, 0]); + Assert.Equal(2, dest[0, 1]); + Assert.Equal(3, dest[0, 2]); + Assert.Equal(4, dest[0, 3]); + Assert.Equal(5, dest[0, 4]); + } + + [Fact] + public void WhenDestIsNotMemoryOwner_DifferentSizeDifferentTotal_ButBothLayoutsFit_ShouldCopy() + { + int[] data = new int[5]; + using TestMemoryManager destOwner = new(data); + using Buffer2D dest = new(MemoryGroup.Wrap(destOwner.Memory), 1, 3); + using Buffer2D source = this.memoryAllocator.Allocate2D(2, 2, AllocationOptions.Clean); + + source[0, 0] = 1; + source[1, 0] = 2; + source[0, 1] = 3; + source[1, 1] = 4; + + bool swap = Buffer2D.SwapOrCopyContent(dest, source); + + Assert.False(swap); + Assert.Equal(new Size(2, 2), dest.Size); + Assert.Equal(1, dest[0, 0]); + Assert.Equal(2, dest[1, 0]); + Assert.Equal(3, dest[0, 1]); + Assert.Equal(4, dest[1, 1]); + } } } diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.WrapMemory.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.WrapMemory.cs new file mode 100644 index 000000000..f65f193ea --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.WrapMemory.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Memory; + +public partial class Buffer2DTests +{ + [Fact] + public void WrapMemory_Packed_DangerousTryGetSinglePixelMemory_ReturnsTrue() + { + int[] data = [1, 2, 3, 4, 5, 6]; + + using Buffer2D buffer = Buffer2D.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 3); + + Assert.True(buffer.DangerousTryGetSingleMemory(out Memory memory)); + Assert.Equal(3, buffer.RowStride); + Assert.Equal(6, memory.Length); + Assert.True(Unsafe.AreSame(ref data[0], ref memory.Span[0])); + Assert.SpanPointsTo(buffer.DangerousGetRowSpan(1), data.AsMemory(), bufferOffset: 3); + } + + [Fact] + public void WrapMemory_Strided_DangerousTryGetSinglePixelMemory_ReturnsFalse() + { + int[] data = [1, 2, 3, 777, 4, 5, 6, 888]; + + using Buffer2D buffer = Buffer2D.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 4); + + Assert.False(buffer.DangerousTryGetSingleMemory(out Memory _)); + Assert.Equal(4, buffer.RowStride); + Assert.Equal(4, new Buffer2DRegion(buffer).Stride); + + Span row0 = buffer.DangerousGetRowSpan(0); + Span row1 = buffer.DangerousGetRowSpan(1); + + Assert.Equal(3, row0.Length); + Assert.Equal(3, row1.Length); + Assert.Equal(1, row0[0]); + Assert.Equal(3, row0[2]); + Assert.Equal(4, row1[0]); + Assert.Equal(6, row1[2]); + + Assert.SpanPointsTo(row0, data.AsMemory(), bufferOffset: 0); + Assert.SpanPointsTo(row1, data.AsMemory(), bufferOffset: 4); + } + + [Fact] + public void WrapMemory_Packed_WithTrailingData_DangerousTryGetSinglePixelMemory_IsLogicalSize() + { + int[] data = [1, 2, 3, 4, 5, 6, 777, 888]; + + using Buffer2D buffer = Buffer2D.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 3); + + Assert.True(buffer.DangerousTryGetSingleMemory(out Memory memory)); + Assert.Equal(6, memory.Length); + Assert.True(Unsafe.AreSame(ref data[0], ref memory.Span[0])); + } + + [Fact] + public void WrapMemory_Strided_ThrowsWhenStrideIsLessThanWidth() + { + int[] data = new int[10]; + + Assert.Throws(() => Buffer2D.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 2)); + } + + [Fact] + public void WrapMemory_Strided_ThrowsWhenInputMemoryTooSmall() + { + int[] data = new int[6]; + + Assert.Throws(() => Buffer2D.WrapMemory(data.AsMemory(), width: 3, height: 2, stride: 4)); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(1, 0)] + [InlineData(0, 0)] + [InlineData(-1, 1)] + [InlineData(1, -1)] + public void WrapMemory_ThrowsWhenWidthOrHeightIsNotPositive(int width, int height) + { + int[] data = new int[16]; + + Assert.Throws(() => Buffer2D.WrapMemory(data.AsMemory(), width, height)); + Assert.Throws(() => Buffer2D.WrapMemory(data.AsMemory(), width, height, stride: 4)); + } +} diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs index 6dfdd294c..8e3f3d286 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs @@ -50,17 +50,11 @@ public partial class Buffer2DTests [InlineData(Big, 1, 0)] [InlineData(60, 42, 0)] [InlineData(3, 0, 0)] - public unsafe void Construct_Empty(int bufferCapacity, int width, int height) + public unsafe void Construct_Empty_Throws(int bufferCapacity, int width, int height) { this.MemoryAllocator.BufferCapacityInBytes = sizeof(TestStructs.Foo) * bufferCapacity; - using (Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height)) - { - Assert.Equal(width, buffer.Width); - Assert.Equal(height, buffer.Height); - Assert.Equal(0, buffer.FastMemoryGroup.TotalLength); - Assert.Equal(0, buffer.DangerousGetSingleSpan().Length); - } + Assert.Throws(() => this.MemoryAllocator.Allocate2D(width, height)); } [Theory] @@ -339,25 +333,47 @@ public partial class Buffer2DTests Assert.NotSame(mgBefore, buffer1.MemoryGroup); } - public static TheoryData InvalidLengths { get; set; } = new() + public static TheoryData InvalidDimensions { get; set; } = new() { { new Size(-1, -1) }, + { new Size(0, 1) }, + { new Size(1, 0) }, + { new Size(0, 0) } + }; + + public static TheoryData OverflowDimensions { get; set; } = new() + { { new Size(32768, 32769) }, { new Size(32769, 32768) } }; [Theory] - [MemberData(nameof(InvalidLengths))] - public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(Size size) + [MemberData(nameof(InvalidDimensions))] + public void Allocate_InvalidDimensions_ThrowsArgumentOutOfRangeException(Size size) + => Assert.Throws(() => this.MemoryAllocator.Allocate2D(size.Width, size.Height)); + + [Theory] + [MemberData(nameof(InvalidDimensions))] + public void Allocate_InvalidDimensions_SizeOverload_ThrowsArgumentOutOfRangeException(Size size) + => Assert.Throws(() => this.MemoryAllocator.Allocate2D(new Size(size))); + + [Theory] + [MemberData(nameof(InvalidDimensions))] + public void Allocate_InvalidDimensions_OverAligned_ThrowsArgumentOutOfRangeException(Size size) + => Assert.Throws(() => this.MemoryAllocator.Allocate2DOveraligned(size.Width, size.Height, 1)); + + [Theory] + [MemberData(nameof(OverflowDimensions))] + public void Allocate_OverflowDimensions_ThrowsInvalidMemoryOperationException(Size size) => Assert.Throws(() => this.MemoryAllocator.Allocate2D(size.Width, size.Height)); [Theory] - [MemberData(nameof(InvalidLengths))] - public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException_Size(Size size) + [MemberData(nameof(OverflowDimensions))] + public void Allocate_OverflowDimensions_SizeOverload_ThrowsInvalidMemoryOperationException(Size size) => Assert.Throws(() => this.MemoryAllocator.Allocate2D(new Size(size))); [Theory] - [MemberData(nameof(InvalidLengths))] - public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException_OverAligned(Size size) + [MemberData(nameof(OverflowDimensions))] + public void Allocate_OverflowDimensions_OverAligned_ThrowsInvalidMemoryOperationException(Size size) => Assert.Throws(() => this.MemoryAllocator.Allocate2DOveraligned(size.Width, size.Height, 1)); } diff --git a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs index be7edbacc..a44d6ce1a 100644 --- a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs +++ b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs @@ -17,7 +17,7 @@ public class BufferAreaTests Buffer2DRegion area = new(buffer, rectangle); Assert.Equal(buffer, area.Buffer); - Assert.Equal(rectangle, area.Rectangle); + Assert.Equal(rectangle, area.Bounds); } private Buffer2D CreateTestBuffer(int w, int h) @@ -90,7 +90,7 @@ public class BufferAreaTests Rectangle expectedRect = new(10, 12, 5, 5); Assert.Equal(buffer, area1.Buffer); - Assert.Equal(expectedRect, area1.Rectangle); + Assert.Equal(expectedRect, area1.Bounds); int value00 = (12 * 100) + 10; Assert.Equal(value00, area1[0, 0]); @@ -147,9 +147,9 @@ public class BufferAreaTests Assert.Equal(0, buffer[5, 5]); Assert.Equal(0, buffer[14, 14]); - for (int y = region.Rectangle.Y; y < region.Rectangle.Bottom; y++) + for (int y = region.Bounds.Y; y < region.Bounds.Bottom; y++) { - Span span = buffer.DangerousGetRowSpan(y).Slice(region.Rectangle.X, region.Width); + Span span = buffer.DangerousGetRowSpan(y).Slice(region.Bounds.X, region.Width); Assert.True(span.SequenceEqual(new int[region.Width])); } } diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs index 678a089a8..cca2230e6 100644 --- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs @@ -98,7 +98,15 @@ public partial class MemoryGroupTests [InlineData(AllocationOptions.Clean)] public unsafe void Allocate_FromPool_AllocationOptionsAreApplied(AllocationOptions options) { - UniformUnmanagedMemoryPool pool = new(10, 5); + // Disable trimming to avoid buffers being freed between Return and TryAllocate by the + // trim timer or the Gen2 GC callback. + UniformUnmanagedMemoryPool pool = new( + 10, + 5, + new UniformUnmanagedMemoryPool.TrimSettings + { + Rate = 0 + }); UnmanagedMemoryHandle[] buffers = pool.Rent(5); foreach (UnmanagedMemoryHandle b in buffers) { diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs index c140571fc..ee5cd9cd0 100644 --- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs @@ -9,100 +9,74 @@ public partial class MemoryGroupTests { public class CopyTo : MemoryGroupTestsBase { - public static readonly TheoryData WhenSourceBufferIsShorterOrEqual_Data = - CopyAndTransformData; - - [Theory] - [MemberData(nameof(WhenSourceBufferIsShorterOrEqual_Data))] - public void WhenSourceBufferIsShorterOrEqual(int srcTotal, int srcBufLen, int trgTotal, int trgBufLen) + [Fact] + public void GroupToSpan_StridedSource_DoesNotRequireTrailingPadding() { - using MemoryGroup src = this.CreateTestGroup(srcTotal, srcBufLen, true); - using MemoryGroup trg = this.CreateTestGroup(trgTotal, trgBufLen, false); - - src.CopyTo(trg); + using MemoryGroup src = this.CreateTestGroup(totalLength: 7, bufferLength: 4, fillSequence: true); + int[] trg = new int[6]; - int pos = 0; - MemoryGroupIndex i = src.MinIndex(); - MemoryGroupIndex j = trg.MinIndex(); - for (; i < src.MaxIndex(); i += 1, j += 1, pos++) - { - int a = src.GetElementAt(i); - int b = trg.GetElementAt(j); + src.CopyTo( + sourceStride: 4, + trg, + targetStride: 3, + width: 3, + height: 2); - Assert.True(a == b, $"Mismatch @ {pos} Expected: {a} Actual: {b}"); - } + Assert.Equal(new[] { 1, 2, 3, 5, 6, 7 }, trg); } [Fact] - public void WhenTargetBufferTooShort_Throws() + public void GroupToGroup_StridedCopy_DoesNotRequireTrailingPadding() { - using MemoryGroup src = this.CreateTestGroup(10, 20, true); - using MemoryGroup trg = this.CreateTestGroup(5, 20, false); - - Assert.Throws(() => src.CopyTo(trg)); + using MemoryGroup src = this.CreateTestGroup(totalLength: 11, bufferLength: 5, fillSequence: true); + using MemoryGroup trg = this.CreateTestGroup(totalLength: 13, bufferLength: 6, fillSequence: false); + + src.CopyTo( + sourceStride: 4, + trg, + targetStride: 5, + width: 3, + height: 3); + + Assert.Equal(1, GetElementAtLinearIndex(trg, 0)); + Assert.Equal(2, GetElementAtLinearIndex(trg, 1)); + Assert.Equal(3, GetElementAtLinearIndex(trg, 2)); + Assert.Equal(5, GetElementAtLinearIndex(trg, 5)); + Assert.Equal(6, GetElementAtLinearIndex(trg, 6)); + Assert.Equal(7, GetElementAtLinearIndex(trg, 7)); + Assert.Equal(9, GetElementAtLinearIndex(trg, 10)); + Assert.Equal(10, GetElementAtLinearIndex(trg, 11)); + Assert.Equal(11, GetElementAtLinearIndex(trg, 12)); } - [Theory] - [InlineData(30, 10, 40)] - [InlineData(42, 23, 42)] - [InlineData(1, 3, 10)] - [InlineData(0, 4, 0)] - public void GroupToSpan_Success(long totalLength, int bufferLength, int spanLength) + [Fact] + public void GroupToSpan_StridedSource_HeightOne() { - using MemoryGroup src = this.CreateTestGroup(totalLength, bufferLength, true); - int[] trg = new int[spanLength]; - src.CopyTo(trg); + using MemoryGroup src = this.CreateTestGroup(totalLength: 3, bufferLength: 2, fillSequence: true); + int[] trg = new int[3]; - int expected = 1; - foreach (int val in trg.AsSpan().Slice(0, (int)totalLength)) - { - Assert.Equal(expected, val); - expected++; - } - } + src.CopyTo( + sourceStride: 8, + trg, + targetStride: 3, + width: 3, + height: 1); - [Theory] - [InlineData(20, 7, 19)] - [InlineData(2, 1, 1)] - public void GroupToSpan_OutOfRange(long totalLength, int bufferLength, int spanLength) - { - using MemoryGroup src = this.CreateTestGroup(totalLength, bufferLength, true); - int[] trg = new int[spanLength]; - Assert.ThrowsAny(() => src.CopyTo(trg)); + Assert.Equal(new[] { 1, 2, 3 }, trg); } - [Theory] - [InlineData(30, 35, 10)] - [InlineData(42, 23, 42)] - [InlineData(10, 3, 1)] - [InlineData(0, 3, 0)] - public void SpanToGroup_Success(long totalLength, int bufferLength, int spanLength) + private static int GetElementAtLinearIndex(MemoryGroup group, int index) { - int[] src = new int[spanLength]; - for (int i = 0; i < src.Length; i++) - { - src[i] = i + 1; - } - - using MemoryGroup trg = this.CreateTestGroup(totalLength, bufferLength); - src.AsSpan().CopyTo(trg); - - int position = 0; - for (MemoryGroupIndex i = trg.MinIndex(); position < spanLength; i += 1, position++) + int pos = 0; + for (MemoryGroupIndex i = group.MinIndex(); i < group.MaxIndex(); i += 1, pos++) { - int expected = position + 1; - Assert.Equal(expected, trg.GetElementAt(i)); + if (pos == index) + { + return group.GetElementAt(i); + } } - } - [Theory] - [InlineData(10, 3, 11)] - [InlineData(0, 3, 1)] - public void SpanToGroup_OutOfRange(long totalLength, int bufferLength, int spanLength) - { - int[] src = new int[spanLength]; - using MemoryGroup trg = this.CreateTestGroup(totalLength, bufferLength, true); - Assert.ThrowsAny(() => src.AsSpan().CopyTo(trg)); + throw new ArgumentOutOfRangeException(nameof(index)); } } } diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs index fe1867f20..b82b20120 100644 --- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs @@ -26,76 +26,6 @@ public partial class MemoryGroupTests : MemoryGroupTestsBase Assert.False(g.IsValid); } -#pragma warning disable SA1509 - private static readonly TheoryData CopyAndTransformData = - new() - { - { 20, 10, 20, 10 }, - { 20, 5, 20, 4 }, - { 20, 4, 20, 5 }, - { 18, 6, 20, 5 }, - { 19, 10, 20, 10 }, - { 21, 10, 22, 2 }, - { 1, 5, 5, 4 }, - - { 30, 12, 40, 5 }, - { 30, 5, 40, 12 }, - }; - - public class TransformTo : MemoryGroupTestsBase - { - public static readonly TheoryData WhenSourceBufferIsShorterOrEqual_Data = - CopyAndTransformData; - - [Theory] - [MemberData(nameof(WhenSourceBufferIsShorterOrEqual_Data))] - public void WhenSourceBufferIsShorterOrEqual(int srcTotal, int srcBufLen, int trgTotal, int trgBufLen) - { - using MemoryGroup src = this.CreateTestGroup(srcTotal, srcBufLen, true); - using MemoryGroup trg = this.CreateTestGroup(trgTotal, trgBufLen, false); - - src.TransformTo(trg, MultiplyAllBy2); - - int pos = 0; - MemoryGroupIndex i = src.MinIndex(); - MemoryGroupIndex j = trg.MinIndex(); - for (; i < src.MaxIndex(); i += 1, j += 1, pos++) - { - int a = src.GetElementAt(i); - int b = trg.GetElementAt(j); - - Assert.True(b == 2 * a, $"Mismatch @ {pos} Expected: {a} Actual: {b}"); - } - } - - [Fact] - public void WhenTargetBufferTooShort_Throws() - { - using MemoryGroup src = this.CreateTestGroup(10, 20, true); - using MemoryGroup trg = this.CreateTestGroup(5, 20, false); - - Assert.Throws(() => src.TransformTo(trg, MultiplyAllBy2)); - } - } - - [Theory] - [InlineData(100, 5)] - [InlineData(100, 101)] - public void TransformInplace(int totalLength, int bufferLength) - { - using MemoryGroup src = this.CreateTestGroup(10, 20, true); - - src.TransformInplace(s => MultiplyAllBy2(s, s)); - - int cnt = 1; - for (MemoryGroupIndex i = src.MinIndex(); i < src.MaxIndex(); i += 1) - { - int val = src.GetElementAt(i); - Assert.Equal(expected: cnt * 2, val); - cnt++; - } - } - [Fact] public void Wrap() { @@ -224,12 +154,4 @@ public partial class MemoryGroupTests : MemoryGroupTestsBase } } - private static void MultiplyAllBy2(ReadOnlySpan source, Span target) - { - Assert.Equal(source.Length, target.Length); - for (int k = 0; k < source.Length; k++) - { - target[k] = source[k] * 2; - } - } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/Various/IccLutTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/Various/IccLutTests.cs new file mode 100644 index 000000000..f6705a1d0 --- /dev/null +++ b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/Various/IccLutTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.ICC.Various; + +[Trait("Profile", "Icc")] +public class IccLutTests +{ + /// + /// Gets lengths that exercise scalar execution, every SIMD width, mixed-width remainders, and multiple vectors. + /// + public static TheoryData LutLengths => new() + { + 0, + 1, + 7, + 8, + 9, + 15, + 16, + 17, + 31, + 32, + 33, + 63, + 64, + 65, + 255, + 256, + 257 + }; + + /// + /// Verifies that byte LUT construction preserves the scalar conversion result for every traversal shape. + /// + /// The number of LUT entries. + [Theory] + [MemberData(nameof(LutLengths))] + public void ByteConstructorMatchesScalarFormula(int length) + { + byte[] values = new byte[length]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = (byte)((i * 73) + 19); + } + + IccLut actual = new(values); + + Assert.Equal(values.Length, actual.Values.Length); + + for (int i = 0; i < values.Length; i++) + { + float expected = values[i] / (float)byte.MaxValue; + Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(actual.Values[i])); + } + } + + /// + /// Verifies that unsigned-short LUT construction preserves the scalar conversion result for every traversal shape. + /// + /// The number of LUT entries. + [Theory] + [MemberData(nameof(LutLengths))] + public void UInt16ConstructorMatchesScalarFormula(int length) + { + ushort[] values = new ushort[length]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = (ushort)((i * 12_347) + 1_019); + } + + IccLut actual = new(values); + + Assert.Equal(values.Length, actual.Values.Length); + + for (int i = 0; i < values.Length; i++) + { + float expected = values[i] / (float)ushort.MaxValue; + Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(actual.Values[i])); + } + } + + /// + /// Verifies bit-exact normalization for every possible unsigned-short input. + /// + [Fact] + public void UInt16ConstructorMatchesScalarFormulaForEveryValue() + { + ushort[] values = new ushort[ushort.MaxValue + 1]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = (ushort)i; + } + + IccLut actual = new(values); + + for (int i = 0; i < values.Length; i++) + { + float expected = values[i] / (float)ushort.MaxValue; + Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(actual.Values[i])); + } + } +} diff --git a/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs b/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs new file mode 100644 index 000000000..c115850fc --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs @@ -0,0 +1,1364 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +/// +/// Verifies the shared contract for pixel formats that store associated alpha. +/// +/// The associated-alpha pixel format. +[Trait("Category", "PixelFormats")] +public abstract class AssociatedAlphaPixelTests + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the color channels described by the pixel format. + /// + protected virtual PixelColorType ExpectedColorType => PixelColorType.RGB | PixelColorType.Alpha; + + [Fact] + public void PixelInformationDescribesAssociatedAlpha() + { + PixelTypeInfo info = TPixel.GetPixelTypeInfo(); + PixelComponentInfo componentInfo = info.ComponentInfo.Value; + int expectedComponentPrecision = (Unsafe.SizeOf() * 8) / 4; + + Assert.Equal(Unsafe.SizeOf() * 8, info.BitsPerPixel); + Assert.Equal(PixelAlphaRepresentation.Associated, info.AlphaRepresentation); + Assert.Equal(this.ExpectedColorType, info.ColorType); + Assert.Equal(4, componentInfo.ComponentCount); + Assert.Equal(0, componentInfo.Padding); + + for (int i = 0; i < componentInfo.ComponentCount; i++) + { + Assert.Equal(expectedComponentPrecision, componentInfo.GetComponentPrecision(i)); + } + } +} + +/// +/// Tests the pixel format. +/// +public class Rgba32PTests : AssociatedAlphaPixelTests +{ + [Fact] + public void ByteLayoutAndPackedValue() + { + Rgba32P[] pixels = [new(1, 2, 3, 4)]; + + Assert.Equal(new byte[] { 1, 2, 3, 4 }, MemoryMarshal.AsBytes(pixels.AsSpan()).ToArray()); + Assert.Equal(0x04030201U, pixels[0].PackedValue); + } +} + +/// +/// Tests the pixel format. +/// +public class Bgra32PTests : AssociatedAlphaPixelTests +{ + /// + protected override PixelColorType ExpectedColorType => PixelColorType.BGR | PixelColorType.Alpha; + + [Fact] + public void ByteLayoutAndPackedValue() + { + Bgra32P[] pixels = [new(1, 2, 3, 4)]; + + Assert.Equal(new byte[] { 3, 2, 1, 4 }, MemoryMarshal.AsBytes(pixels.AsSpan()).ToArray()); + Assert.Equal(0x04010203U, pixels[0].PackedValue); + } +} + +/// +/// Tests the pixel format. +/// +public class Argb32PTests : AssociatedAlphaPixelTests +{ + [Fact] + public void ByteLayoutAndPackedValue() + { + Argb32P[] pixels = [new(1, 2, 3, 4)]; + + Assert.Equal(new byte[] { 4, 1, 2, 3 }, MemoryMarshal.AsBytes(pixels.AsSpan()).ToArray()); + Assert.Equal(0x03020104U, pixels[0].PackedValue); + } +} + +/// +/// Tests the pixel format. +/// +public class Abgr32PTests : AssociatedAlphaPixelTests +{ + /// + protected override PixelColorType ExpectedColorType => PixelColorType.BGR | PixelColorType.Alpha; + + [Fact] + public void ByteLayoutAndPackedValue() + { + Abgr32P[] pixels = [new(1, 2, 3, 4)]; + + Assert.Equal(new byte[] { 4, 3, 2, 1 }, MemoryMarshal.AsBytes(pixels.AsSpan()).ToArray()); + Assert.Equal(0x01020304U, pixels[0].PackedValue); + } +} + +/// +/// Tests the pixel format. +/// +public class NormalizedByte4PTests : AssociatedAlphaPixelTests +{ + [Fact] + public void PackedValueMatchesNormalizedByte4ForAssociatedVector() + { + Vector4 associated = new(.1F, -.3F, .5F, -.7F); + + Assert.Equal(new NormalizedByte4(associated).PackedValue, new NormalizedByte4P(associated).PackedValue); + } + + [Fact] + public void MinimumStorageCodeDecodesAsNegativeOne() + { + NormalizedByte4P pixel = new() { PackedValue = 0x80808080 }; + + Assert.Equal(-Vector4.One, pixel.ToVector4()); + Assert.Equal(Vector4.Zero, pixel.ToScaledVector4()); + Assert.Equal(-Vector4.One, pixel.ToUnassociatedVector4()); + Assert.Equal(Vector4.Zero, pixel.ToUnassociatedScaledVector4()); + } + + [Fact] + public void AssociatedScaledVectorsMatchUnassociatedVectorsWithinTwoUlpsForEveryValidComponentAndAlpha() + { + for (int alpha = 0; alpha < byte.MaxValue; alpha++) + { + for (int associated = 0; associated <= alpha; associated++) + { + uint component = (byte)(associated - 127); + uint alphaComponent = (byte)(alpha - 127); + NormalizedByte4P pixel = new() { PackedValue = component | (component << 8) | (component << 16) | (alphaComponent << 24) }; + Vector4 expected = pixel.ToUnassociatedScaledVector4(); + expected.X *= expected.W; + expected.Y *= expected.W; + expected.Z *= expected.W; + Vector4 actual = pixel.ToAssociatedScaledVector4(); + + // All 32,640 valid storage pairs are covered. Two ULP is the measured maximum from the independently ordered division and multiplication. + AlphaRepresentationTestAssertions.EqualWithinTwoUlps(expected, actual); + Assert.Equal(BitConverter.SingleToInt32Bits(expected.W), BitConverter.SingleToInt32Bits(actual.W)); + } + } + } + + [Fact] + public void BulkConversionsMatchScalarAcrossHardwareWidths() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertBulkConversionsMatchScalar, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertBulkConversionsMatchScalar() + { + // Values immediately below, at, and above each vector width force every hardware regime through its vector body and scalar tail. + int[] lengths = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 259]; + AssociatedAlphaPixelOperations operations = (AssociatedAlphaPixelOperations)PixelOperations.Instance; + + foreach (int length in lengths) + { + NormalizedByte4P[] pixels = new NormalizedByte4P[length]; + Vector4[] expectedNative = new Vector4[length]; + Vector4[] expectedAssociatedNative = new Vector4[length]; + Vector4[] expectedAssociatedScaled = new Vector4[length]; + Vector4[] expectedUnassociatedNative = new Vector4[length]; + Vector4[] expectedUnassociatedScaled = new Vector4[length]; + Vector4[] actualNative = new Vector4[length]; + Vector4[] actualAssociatedNative = new Vector4[length]; + Vector4[] actualAssociatedScaled = new Vector4[length]; + Vector4[] actualUnassociatedNative = new Vector4[length]; + Vector4[] actualUnassociatedScaled = new Vector4[length]; + + for (int i = 0; i < length; i++) + { + pixels[i].PackedValue = (uint)((byte)(i * 37 + 128) | ((byte)(i * 73 + 64) << 8) | ((byte)(i * 109 + 192) << 16) | ((byte)(i * 151 + 128) << 24)); + expectedNative[i] = pixels[i].ToVector4(); + expectedAssociatedNative[i] = pixels[i].ToAssociatedVector4(); + expectedAssociatedScaled[i] = pixels[i].ToAssociatedScaledVector4(); + expectedUnassociatedNative[i] = pixels[i].ToUnassociatedVector4(); + expectedUnassociatedScaled[i] = pixels[i].ToUnassociatedScaledVector4(); + } + + if (length > 0) + { + // PackedValue permits every signed byte pattern. Explicit -128 components ensure bulk sign extension matches scalar decoding even though the packer emits no -128 values. + pixels[0].PackedValue = 0x80808080; + expectedNative[0] = pixels[0].ToVector4(); + expectedAssociatedNative[0] = pixels[0].ToAssociatedVector4(); + expectedAssociatedScaled[0] = pixels[0].ToAssociatedScaledVector4(); + expectedUnassociatedNative[0] = pixels[0].ToUnassociatedVector4(); + expectedUnassociatedScaled[0] = pixels[0].ToUnassociatedScaledVector4(); + } + + operations.ToVector4(Configuration.Default, pixels, actualNative, PixelConversionModifiers.None); + operations.ToVector4(Configuration.Default, pixels, actualAssociatedNative, PixelConversionModifiers.Premultiply); + operations.ToVector4(Configuration.Default, pixels, actualAssociatedScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + operations.ToVector4(Configuration.Default, pixels, actualUnassociatedNative, PixelConversionModifiers.UnPremultiply); + operations.ToVector4(Configuration.Default, pixels, actualUnassociatedScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + Assert.Equal(expectedNative, actualNative); + Assert.Equal(expectedAssociatedNative, actualAssociatedNative); + Assert.Equal(expectedAssociatedScaled, actualAssociatedScaled); + Assert.Equal(expectedUnassociatedNative, actualUnassociatedNative); + Assert.Equal(expectedUnassociatedScaled, actualUnassociatedScaled); + + Vector4[] unassociatedScaled = new Vector4[length]; + Vector4[] associatedScaled = new Vector4[length]; + Vector4[] unassociatedNative = new Vector4[length]; + Vector4[] associatedNative = new Vector4[length]; + NormalizedByte4P[] expectedFromUnassociatedNative = new NormalizedByte4P[length]; + NormalizedByte4P[] expectedFromAssociatedNative = new NormalizedByte4P[length]; + NormalizedByte4P[] expectedFromUnassociatedScaled = new NormalizedByte4P[length]; + NormalizedByte4P[] expectedFromAssociatedScaled = new NormalizedByte4P[length]; + NormalizedByte4P[] actualFromUnassociatedNative = new NormalizedByte4P[length]; + NormalizedByte4P[] actualFromAssociatedNative = new NormalizedByte4P[length]; + NormalizedByte4P[] actualFromUnassociatedScaled = new NormalizedByte4P[length]; + NormalizedByte4P[] actualFromAssociatedScaled = new NormalizedByte4P[length]; + + for (int i = 0; i < length; i++) + { + float alpha = ((i * 151) % 1021) / 1020F; + unassociatedScaled[i] = new Vector4(((i * 37) % 1021) / 1020F, ((i * 73) % 1021) / 1020F, ((i * 109) % 1021) / 1020F, alpha); + associatedScaled[i] = new Vector4(unassociatedScaled[i].X * alpha, unassociatedScaled[i].Y * alpha, unassociatedScaled[i].Z * alpha, alpha); + + // Signed-native formats encode the common scaled domain over [-1, 1], so native inputs must be derived before invoking the native entry points. + unassociatedNative[i] = (unassociatedScaled[i] * 2F) - Vector4.One; + associatedNative[i] = (associatedScaled[i] * 2F) - Vector4.One; + expectedFromUnassociatedNative[i] = NormalizedByte4P.FromUnassociatedVector4(unassociatedNative[i]); + expectedFromAssociatedNative[i] = NormalizedByte4P.FromAssociatedVector4(associatedNative[i]); + expectedFromUnassociatedScaled[i] = NormalizedByte4P.FromUnassociatedScaledVector4(unassociatedScaled[i]); + expectedFromAssociatedScaled[i] = NormalizedByte4P.FromAssociatedScaledVector4(associatedScaled[i]); + } + + Vector4[] unassociatedNativeSource = [.. unassociatedNative]; + Vector4[] associatedNativeSource = [.. associatedNative]; + Vector4[] unassociatedScaledSource = [.. unassociatedScaled]; + Vector4[] associatedScaledSource = [.. associatedScaled]; + + operations.FromVector4Destructive(Configuration.Default, unassociatedNativeSource, actualFromUnassociatedNative, PixelConversionModifiers.UnPremultiply); + operations.FromVector4Destructive(Configuration.Default, associatedNativeSource, actualFromAssociatedNative, PixelConversionModifiers.Premultiply); + operations.FromVector4Destructive(Configuration.Default, unassociatedScaledSource, actualFromUnassociatedScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + operations.FromVector4Destructive(Configuration.Default, associatedScaledSource, actualFromAssociatedScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + + Assert.Equal(expectedFromUnassociatedNative, actualFromUnassociatedNative); + Assert.Equal(expectedFromAssociatedNative, actualFromAssociatedNative); + Assert.Equal(expectedFromUnassociatedScaled, actualFromUnassociatedScaled); + Assert.Equal(expectedFromAssociatedScaled, actualFromAssociatedScaled); + + NormalizedByte4P[] expectedFromNative = new NormalizedByte4P[length]; + NormalizedByte4P[] expectedFromScaled = new NormalizedByte4P[length]; + NormalizedByte4P[] actualFromNative = new NormalizedByte4P[length]; + NormalizedByte4P[] actualFromScaled = new NormalizedByte4P[length]; + + for (int i = 0; i < length; i++) + { + expectedFromNative[i] = NormalizedByte4P.FromVector4(expectedNative[i]); + expectedFromScaled[i] = NormalizedByte4P.FromScaledVector4(expectedAssociatedScaled[i]); + } + + Vector4[] actualNativeSource = [.. expectedNative]; + Vector4[] actualScaledSource = [.. expectedAssociatedScaled]; + + operations.FromVector4Destructive(Configuration.Default, actualNativeSource, actualFromNative, PixelConversionModifiers.None); + operations.FromVector4Destructive(Configuration.Default, actualScaledSource, actualFromScaled, PixelConversionModifiers.Scale); + + Assert.Equal(expectedFromNative, actualFromNative); + Assert.Equal(expectedFromScaled, actualFromScaled); + } + } +} + +/// +/// Tests the pixel format. +/// +public class HalfVector4PTests : AssociatedAlphaPixelTests +{ + [Fact] + public void PackedValueMatchesHalfVector4ForAssociatedVector() + { + Vector4 associated = new(.1F, -.3F, .5F, -.7F); + + Assert.Equal(new HalfVector4(associated).PackedValue, new HalfVector4P(associated).PackedValue); + } + + [Fact] + public void ColorRoundTripDoesNotIntroduceAdditionalAssociationLoss() + { + HalfVector4P source = HalfVector4P.FromAssociatedScaledVector4(new Vector4(.125F, .25F, .375F, .5F)); + HalfVector4P expected = HalfVector4P.FromScaledVector4(source.ToScaledVector4()); + Color color = Color.FromPixel(source); + Color[] bulkColors = new Color[1]; + + Color.FromPixel(new[] { source }, bulkColors); + + Assert.Equal(source.ToScaledVector4(), color.ToScaledVector4()); + Assert.Equal(expected, color.ToPixel()); + Assert.Equal(source.ToScaledVector4(), bulkColors[0].ToScaledVector4()); + Assert.Equal(expected, bulkColors[0].ToPixel()); + } + + [Fact] + public void BulkConversionsMatchScalarAcrossHardwareWidths() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertBulkConversionsMatchScalar, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertBulkConversionsMatchScalar() + { + AssertEveryHalfBitPatternUnpacksExactly(); + AssertHalfPackingMatchesScalar(); + AssertAlphaConversionsMatchScalar(); + } + + private static void AssertEveryHalfBitPatternUnpacksExactly() + { + HalfVector4P[] pixels = new HalfVector4P[(ushort.MaxValue + 1) / 4]; + Vector4[] expectedNative = new Vector4[pixels.Length]; + Vector4[] expectedScaled = new Vector4[pixels.Length]; + Vector4[] actualNative = new Vector4[pixels.Length]; + Vector4[] actualScaled = new Vector4[pixels.Length]; + + for (int i = 0; i < pixels.Length; i++) + { + ulong x = (ushort)(i * 4); + ulong y = (ushort)((i * 4) + 1); + ulong z = (ushort)((i * 4) + 2); + ulong w = (ushort)((i * 4) + 3); + pixels[i].PackedValue = x | (y << 16) | (z << 32) | (w << 48); + expectedNative[i] = pixels[i].ToVector4(); + expectedScaled[i] = pixels[i].ToScaledVector4(); + } + + PixelOperations.Instance.ToVector4(Configuration.Default, pixels, actualNative, PixelConversionModifiers.None); + PixelOperations.Instance.ToVector4(Configuration.Default, pixels, actualScaled, PixelConversionModifiers.Scale); + + AssertVectorBitsEqual(expectedNative, actualNative); + AssertVectorBitsEqual(expectedScaled, actualScaled); + } + + private static void AssertHalfPackingMatchesScalar() + { + const int finitePositiveHalfCount = 0x7BFF; + const int distributedFloatCount = ushort.MaxValue + 1; + const int componentCount = (ushort.MaxValue + 1) + (finitePositiveHalfCount * 2) + distributedFloatCount + 2; + Vector4[] source = new Vector4[componentCount / 4]; + Span components = MemoryMarshal.Cast(source.AsSpan()); + int index = 0; + + for (int bits = 0; bits <= ushort.MaxValue; bits++) + { + components[index++] = (float)BitConverter.UInt16BitsToHalf((ushort)bits); + } + + for (int bits = 0; bits < finitePositiveHalfCount; bits++) + { + float lower = (float)BitConverter.UInt16BitsToHalf((ushort)bits); + float upper = (float)BitConverter.UInt16BitsToHalf((ushort)(bits + 1)); + + // Every midpoint exercises binary16 round-to-nearest-even; negating it covers the symmetric sign path. + float midpoint = (lower + upper) * .5F; + components[index++] = midpoint; + components[index++] = -midpoint; + } + + // Equal high and low words distribute samples across the complete binary32 sign, exponent, and fraction space. + for (uint bits = 0; bits <= ushort.MaxValue; bits++) + { + components[index++] = BitConverter.UInt32BitsToSingle(bits * 0x0001_0001U); + } + + // The two padding components also verify finite overflow in both directions. + components[index++] = float.MaxValue; + components[index] = float.MinValue; + + HalfVector4P[] expected = new HalfVector4P[source.Length]; + HalfVector4P[] actual = new HalfVector4P[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + expected[i] = HalfVector4P.FromVector4(source[i]); + } + + Vector4[] destructiveSource = [.. source]; + + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actual, PixelConversionModifiers.None); + + for (int i = 0; i < expected.Length; i++) + { + for (int component = 0; component < 4; component++) + { + int shift = component * 16; + ushort expectedBits = (ushort)(expected[i].PackedValue >> shift); + ushort actualBits = (ushort)(actual[i].PackedValue >> shift); + + // Association performs floating-point arithmetic before packing. IEEE 754 requires a NaN result but does not specify which operand's payload survives that arithmetic. + if (Half.IsNaN(BitConverter.UInt16BitsToHalf(expectedBits))) + { + Assert.True(Half.IsNaN(BitConverter.UInt16BitsToHalf(actualBits)), $"Pixel {i}, component {component} should be NaN."); + } + else + { + Assert.Equal(expectedBits, actualBits); + } + } + } + } + + private static void AssertAlphaConversionsMatchScalar() + { + // Values immediately below, at, and above each vector width force every hardware regime through its vector body and scalar tail. + int[] lengths = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 259]; + AssociatedAlphaPixelOperations operations = (AssociatedAlphaPixelOperations)PixelOperations.Instance; + + foreach (int length in lengths) + { + Vector4[] unassociatedScaled = new Vector4[length]; + Vector4[] associatedScaled = new Vector4[length]; + Vector4[] unassociatedNative = new Vector4[length]; + Vector4[] associatedNative = new Vector4[length]; + HalfVector4P[] expectedFromUnassociatedNative = new HalfVector4P[length]; + HalfVector4P[] expectedFromAssociatedNative = new HalfVector4P[length]; + HalfVector4P[] expectedFromUnassociatedScaled = new HalfVector4P[length]; + HalfVector4P[] expectedFromAssociatedScaled = new HalfVector4P[length]; + HalfVector4P[] actualFromUnassociatedNative = new HalfVector4P[length]; + HalfVector4P[] actualFromAssociatedNative = new HalfVector4P[length]; + HalfVector4P[] actualFromUnassociatedScaled = new HalfVector4P[length]; + HalfVector4P[] actualFromAssociatedScaled = new HalfVector4P[length]; + + for (int i = 0; i < length; i++) + { + float alpha = ((i * 151) % 4093) / 4092F; + unassociatedScaled[i] = new Vector4(((i * 37) % 4093) / 4092F, ((i * 73) % 4093) / 4092F, ((i * 109) % 4093) / 4092F, alpha); + associatedScaled[i] = new Vector4(unassociatedScaled[i].X * alpha, unassociatedScaled[i].Y * alpha, unassociatedScaled[i].Z * alpha, alpha); + + unassociatedNative[i] = (unassociatedScaled[i] * 131008F) - new Vector4(65504F); + associatedNative[i] = (associatedScaled[i] * 131008F) - new Vector4(65504F); + expectedFromUnassociatedNative[i] = HalfVector4P.FromUnassociatedVector4(unassociatedNative[i]); + expectedFromAssociatedNative[i] = HalfVector4P.FromAssociatedVector4(associatedNative[i]); + expectedFromUnassociatedScaled[i] = HalfVector4P.FromUnassociatedScaledVector4(unassociatedScaled[i]); + expectedFromAssociatedScaled[i] = HalfVector4P.FromAssociatedScaledVector4(associatedScaled[i]); + } + + Vector4[] unassociatedNativeSource = [.. unassociatedNative]; + Vector4[] associatedNativeSource = [.. associatedNative]; + Vector4[] unassociatedScaledSource = [.. unassociatedScaled]; + Vector4[] associatedScaledSource = [.. associatedScaled]; + + operations.FromVector4Destructive(Configuration.Default, unassociatedNativeSource, actualFromUnassociatedNative, PixelConversionModifiers.UnPremultiply); + operations.FromVector4Destructive(Configuration.Default, associatedNativeSource, actualFromAssociatedNative, PixelConversionModifiers.Premultiply); + operations.FromVector4Destructive(Configuration.Default, unassociatedScaledSource, actualFromUnassociatedScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + operations.FromVector4Destructive(Configuration.Default, associatedScaledSource, actualFromAssociatedScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + + Assert.Equal(expectedFromUnassociatedNative, actualFromUnassociatedNative); + Assert.Equal(expectedFromAssociatedNative, actualFromAssociatedNative); + Assert.Equal(expectedFromUnassociatedScaled, actualFromUnassociatedScaled); + Assert.Equal(expectedFromAssociatedScaled, actualFromAssociatedScaled); + + Vector4[] expectedAssociatedNative = new Vector4[length]; + Vector4[] expectedAssociatedScaled = new Vector4[length]; + Vector4[] expectedUnassociatedNative = new Vector4[length]; + Vector4[] expectedUnassociatedScaled = new Vector4[length]; + Vector4[] actualAssociatedNative = new Vector4[length]; + Vector4[] actualAssociatedScaled = new Vector4[length]; + Vector4[] actualUnassociatedNative = new Vector4[length]; + Vector4[] actualUnassociatedScaled = new Vector4[length]; + + for (int i = 0; i < length; i++) + { + expectedAssociatedNative[i] = expectedFromAssociatedScaled[i].ToAssociatedVector4(); + expectedAssociatedScaled[i] = expectedFromAssociatedScaled[i].ToAssociatedScaledVector4(); + expectedUnassociatedNative[i] = expectedFromAssociatedScaled[i].ToUnassociatedVector4(); + expectedUnassociatedScaled[i] = expectedFromAssociatedScaled[i].ToUnassociatedScaledVector4(); + } + + operations.ToVector4(Configuration.Default, expectedFromAssociatedScaled, actualAssociatedNative, PixelConversionModifiers.Premultiply); + operations.ToVector4(Configuration.Default, expectedFromAssociatedScaled, actualAssociatedScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + operations.ToVector4(Configuration.Default, expectedFromAssociatedScaled, actualUnassociatedNative, PixelConversionModifiers.UnPremultiply); + operations.ToVector4(Configuration.Default, expectedFromAssociatedScaled, actualUnassociatedScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + AssertVectorBitsEqual(expectedAssociatedNative, actualAssociatedNative); + AssertVectorBitsEqual(expectedAssociatedScaled, actualAssociatedScaled); + AssertVectorBitsEqual(expectedUnassociatedNative, actualUnassociatedNative); + AssertVectorBitsEqual(expectedUnassociatedScaled, actualUnassociatedScaled); + } + } + + private static void AssertVectorBitsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + ReadOnlySpan expectedBits = MemoryMarshal.Cast(expected); + ReadOnlySpan actualBits = MemoryMarshal.Cast(actual); + + Assert.Equal(expectedBits.ToArray(), actualBits.ToArray()); + } +} + +/// +/// Tests conversion between associated-alpha packed byte layouts. +/// +public class AssociatedAlphaPackedPixelConversionTests +{ + [Fact] + public void Rgba32PToBgra32PRoundTripIsLossless() => AssertLosslessRoundTrip(); + + [Fact] + public void Rgba32PToArgb32PRoundTripIsLossless() => AssertLosslessRoundTrip(); + + [Fact] + public void Rgba32PToAbgr32PRoundTripIsLossless() => AssertLosslessRoundTrip(); + + [Fact] + public void Rgba32PScalarAndBulkAssociatedVectorsAreEqual() => AssertScalarAndBulkAssociatedVectorsAreEqual((r, g, b, a) => new Rgba32P(r, g, b, a)); + + [Fact] + public void Bgra32PScalarAndBulkAssociatedVectorsAreEqual() => AssertScalarAndBulkAssociatedVectorsAreEqual((r, g, b, a) => new Bgra32P(r, g, b, a)); + + [Fact] + public void Argb32PScalarAndBulkAssociatedVectorsAreEqual() => AssertScalarAndBulkAssociatedVectorsAreEqual((r, g, b, a) => new Argb32P(r, g, b, a)); + + [Fact] + public void Abgr32PScalarAndBulkAssociatedVectorsAreEqual() => AssertScalarAndBulkAssociatedVectorsAreEqual((r, g, b, a) => new Abgr32P(r, g, b, a)); + + [Fact] + public void Rgba32PScalarAndBulkFromAssociatedVectorsAreEqual() => AssertScalarAndBulkFromAssociatedVectorsAreEqual(); + + [Fact] + public void Bgra32PScalarAndBulkFromAssociatedVectorsAreEqual() => AssertScalarAndBulkFromAssociatedVectorsAreEqual(); + + [Fact] + public void Argb32PScalarAndBulkFromAssociatedVectorsAreEqual() => AssertScalarAndBulkFromAssociatedVectorsAreEqual(); + + [Fact] + public void Abgr32PScalarAndBulkFromAssociatedVectorsAreEqual() => AssertScalarAndBulkFromAssociatedVectorsAreEqual(); + + [Fact] + public void Rgba32PScalarAndBulkUnassociatedVectorsAreEqual() => AssertScalarAndBulkUnassociatedVectorsAreEqual((r, g, b, a) => new Rgba32P(r, g, b, a)); + + [Fact] + public void Bgra32PScalarAndBulkUnassociatedVectorsAreEqual() => AssertScalarAndBulkUnassociatedVectorsAreEqual((r, g, b, a) => new Bgra32P(r, g, b, a)); + + [Fact] + public void Argb32PScalarAndBulkUnassociatedVectorsAreEqual() => AssertScalarAndBulkUnassociatedVectorsAreEqual((r, g, b, a) => new Argb32P(r, g, b, a)); + + [Fact] + public void Abgr32PScalarAndBulkUnassociatedVectorsAreEqual() => AssertScalarAndBulkUnassociatedVectorsAreEqual((r, g, b, a) => new Abgr32P(r, g, b, a)); + + [Fact] + public void Rgba32PScalarAndBulkFromUnassociatedVectorsAreEqual() => AssertScalarAndBulkFromUnassociatedVectorsAreEqual(); + + [Fact] + public void Bgra32PScalarAndBulkFromUnassociatedVectorsAreEqual() => AssertScalarAndBulkFromUnassociatedVectorsAreEqual(); + + [Fact] + public void Argb32PScalarAndBulkFromUnassociatedVectorsAreEqual() => AssertScalarAndBulkFromUnassociatedVectorsAreEqual(); + + [Fact] + public void Abgr32PScalarAndBulkFromUnassociatedVectorsAreEqual() => AssertScalarAndBulkFromUnassociatedVectorsAreEqual(); + + [Fact] + public void BulkConversionsMatchScalarAcrossHardwareWidths() + { + // RemoteExecutor resolves the delegate by method name alone, so its entry point must not share the generic worker's name. + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertPackedBulkConversionsMatchScalar, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + } + + private static void AssertPackedBulkConversionsMatchScalar() + { + AssertBulkConversionsMatchScalar((r, g, b, a) => new Rgba32P(r, g, b, a)); + AssertBulkConversionsMatchScalar((r, g, b, a) => new Bgra32P(r, g, b, a)); + AssertBulkConversionsMatchScalar((r, g, b, a) => new Argb32P(r, g, b, a)); + AssertBulkConversionsMatchScalar((r, g, b, a) => new Abgr32P(r, g, b, a)); + } + + private static void AssertBulkConversionsMatchScalar(Func createPixel) + where TPixel : unmanaged, IPixel + { + AssertScalarAndBulkAssociatedVectorsAreEqual(createPixel); + AssertScalarAndBulkFromAssociatedVectorsAreEqual(); + AssertScalarAndBulkUnassociatedVectorsAreEqual(createPixel); + AssertScalarAndBulkFromUnassociatedVectorsAreEqual(); + } + + private static void AssertLosslessRoundTrip() + where TIntermediate : unmanaged, IPixel + { + const int componentAlphaPairCount = ((byte.MaxValue + 1) * (byte.MaxValue + 2)) / 2; + const int colorChannelCount = 3; + Rgba32P[] expected = new Rgba32P[componentAlphaPairCount * colorChannelCount]; + TIntermediate[] intermediate = new TIntermediate[expected.Length]; + Rgba32P[] actual = new Rgba32P[expected.Length]; + int index = 0; + + // Every component/alpha pair in the valid associated-byte domain must survive a layout + // conversion exactly. Exercising each color lane also catches an incorrect channel map. + for (int alpha = 0; alpha <= byte.MaxValue; alpha++) + { + for (int component = 0; component <= alpha; component++) + { + expected[index++] = new Rgba32P((byte)component, 0, 0, (byte)alpha); + expected[index++] = new Rgba32P(0, (byte)component, 0, (byte)alpha); + expected[index++] = new Rgba32P(0, 0, (byte)component, (byte)alpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, expected, intermediate); + PixelOperations.Instance.From(Configuration.Default, intermediate, actual); + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].ToScaledVector4(), intermediate[i].ToScaledVector4()); + } + + Assert.Equal(expected, actual); + } + + private static void AssertScalarAndBulkAssociatedVectorsAreEqual(Func createPixel) + where TPixel : unmanaged, IPixel + { + // Prefixes straddle every vector width; the final length also preserves exhaustive byte-component coverage and adds a scalar tail. + int[] lengths = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 259, byte.MaxValue * 4 + 5]; + TPixel[] source = new TPixel[lengths[^1]]; + AssociatedAlphaPixelOperations operations = (AssociatedAlphaPixelOperations)PixelOperations.Instance; + + for (int component = 0; component <= byte.MaxValue; component++) + { + int index = component * 4; + source[index] = createPixel((byte)component, 0, 0, byte.MaxValue); + source[index + 1] = createPixel(0, (byte)component, 0, byte.MaxValue); + source[index + 2] = createPixel(0, 0, (byte)component, byte.MaxValue); + source[index + 3] = createPixel(0, 0, 0, (byte)component); + } + + source[^1] = createPixel(37, 73, 109, 151); + + foreach (int length in lengths) + { + ReadOnlySpan pixels = source.AsSpan(0, length); + Vector4[] expectedNative = new Vector4[length]; + Vector4[] expectedScaled = new Vector4[length]; + Vector4[] actualNative = new Vector4[length]; + Vector4[] actualScaled = new Vector4[length]; + + for (int i = 0; i < pixels.Length; i++) + { + expectedNative[i] = pixels[i].ToAssociatedVector4(); + expectedScaled[i] = pixels[i].ToAssociatedScaledVector4(); + } + + operations.ToVector4(Configuration.Default, pixels, actualNative, PixelConversionModifiers.Premultiply); + operations.ToVector4(Configuration.Default, pixels, actualScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + + Assert.Equal(expectedNative, actualNative); + Assert.Equal(expectedScaled, actualScaled); + } + } + + private static void AssertScalarAndBulkFromAssociatedVectorsAreEqual() + where TPixel : unmanaged, IPixel + { + // Values immediately below, at, and above each vector width force every hardware regime through its vector body and scalar tail. + int[] lengths = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 259]; + Vector4[] source = new Vector4[lengths[^1]]; + AssociatedAlphaPixelOperations operations = (AssociatedAlphaPixelOperations)PixelOperations.Instance; + + for (int i = 0; i < source.Length; i++) + { + // Alternating exact byte alpha values with fractional values covers both reassociation branches. + float alpha = (i & 1) == 0 ? (i % 256) / 255F : ((i % 255) + .375F) / 255F; + source[i] = new Vector4(((i * 37) % 256) / 255F, ((i * 73) % 256) / 255F, ((i * 109) % 256) / 255F, 1F) * alpha; + source[i].W = alpha; + } + + foreach (int length in lengths) + { + Vector4[] nativeSource = source.AsSpan(0, length).ToArray(); + Vector4[] scaledSource = source.AsSpan(0, length).ToArray(); + TPixel[] expectedNative = new TPixel[length]; + TPixel[] expectedScaled = new TPixel[length]; + TPixel[] actualNative = new TPixel[length]; + TPixel[] actualScaled = new TPixel[length]; + + for (int i = 0; i < length; i++) + { + expectedNative[i] = TPixel.FromAssociatedVector4(nativeSource[i]); + expectedScaled[i] = TPixel.FromAssociatedScaledVector4(scaledSource[i]); + } + + operations.FromVector4Destructive(Configuration.Default, nativeSource, actualNative, PixelConversionModifiers.Premultiply); + operations.FromVector4Destructive(Configuration.Default, scaledSource, actualScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + + Assert.Equal(expectedNative, actualNative); + Assert.Equal(expectedScaled, actualScaled); + } + } + + private static void AssertScalarAndBulkUnassociatedVectorsAreEqual(Func createPixel) + where TPixel : unmanaged, IPixel + { + int[] lengths = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 259]; + AssociatedAlphaPixelOperations operations = (AssociatedAlphaPixelOperations)PixelOperations.Instance; + + foreach (int length in lengths) + { + TPixel[] pixels = new TPixel[length]; + Vector4[] expectedNative = new Vector4[length]; + Vector4[] expectedScaled = new Vector4[length]; + Vector4[] actualNative = new Vector4[length]; + Vector4[] actualScaled = new Vector4[length]; + + for (int i = 0; i < pixels.Length; i++) + { + // Independent component sequences exercise every packed layout across each SIMD width and remainder. + pixels[i] = createPixel((byte)((i * 37) % 256), (byte)((i * 73) % 256), (byte)((i * 109) % 256), (byte)((i * 151) % 256)); + expectedNative[i] = pixels[i].ToUnassociatedVector4(); + expectedScaled[i] = pixels[i].ToUnassociatedScaledVector4(); + } + + if (length > 0) + { + // A zero-alpha pixel with stored RGB verifies the converter's defined zero-divisor behavior outside the lossless straight-color round-trip domain. + pixels[0] = createPixel(37, 73, 109, 0); + expectedNative[0] = pixels[0].ToUnassociatedVector4(); + expectedScaled[0] = pixels[0].ToUnassociatedScaledVector4(); + } + + operations.ToVector4(Configuration.Default, pixels, actualNative, PixelConversionModifiers.UnPremultiply); + operations.ToVector4(Configuration.Default, pixels, actualScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + Assert.Equal(expectedNative, actualNative); + Assert.Equal(expectedScaled, actualScaled); + } + } + + private static void AssertScalarAndBulkFromUnassociatedVectorsAreEqual() + where TPixel : unmanaged, IPixel + { + int[] lengths = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 259]; + AssociatedAlphaPixelOperations operations = (AssociatedAlphaPixelOperations)PixelOperations.Instance; + + foreach (int length in lengths) + { + Vector4[] vectors = new Vector4[length]; + TPixel[] expectedNative = new TPixel[length]; + TPixel[] expectedScaled = new TPixel[length]; + TPixel[] actualNative = new TPixel[length]; + TPixel[] actualScaled = new TPixel[length]; + + for (int i = 0; i < vectors.Length; i++) + { + // Fractional alpha values verify destination-alpha quantization across each SIMD width and remainder. + vectors[i] = new Vector4(((i * 37) % 1021) / 1020F, ((i * 73) % 1021) / 1020F, ((i * 109) % 1021) / 1020F, ((i * 151) % 1021) / 1020F); + expectedNative[i] = TPixel.FromUnassociatedVector4(vectors[i]); + expectedScaled[i] = TPixel.FromUnassociatedScaledVector4(vectors[i]); + } + + Vector4[] nativeSource = [.. vectors]; + Vector4[] scaledSource = [.. vectors]; + + operations.FromVector4Destructive(Configuration.Default, nativeSource, actualNative, PixelConversionModifiers.UnPremultiply); + operations.FromVector4Destructive(Configuration.Default, scaledSource, actualScaled, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + Assert.Equal(expectedNative, actualNative); + Assert.Equal(expectedScaled, actualScaled); + } + } +} + +/// +/// Tests conversion between associated and unassociated packed byte formats. +/// +public class AssociatedToUnassociatedPackedPixelConversionTests +{ + [Fact] + public void Rgba32ToRgba32PMatchesExactAssociationForEveryComponentAndAlpha() + => AssertUnsignedByteAssociation((red, green, blue, alpha) => new Rgba32P(red, green, blue, alpha)); + + [Fact] + public void Rgba32ToBgra32PMatchesExactAssociationForEveryComponentAndAlpha() + => AssertUnsignedByteAssociation((red, green, blue, alpha) => new Bgra32P(red, green, blue, alpha)); + + [Fact] + public void Rgba32ToArgb32PMatchesExactAssociationForEveryComponentAndAlpha() + => AssertUnsignedByteAssociation((red, green, blue, alpha) => new Argb32P(red, green, blue, alpha)); + + [Fact] + public void Rgba32ToAbgr32PMatchesExactAssociationForEveryComponentAndAlpha() + => AssertUnsignedByteAssociation((red, green, blue, alpha) => new Abgr32P(red, green, blue, alpha)); + + [Fact] + public void Rgba32ToNormalizedByte4PMatchesExactAssociationForEveryComponentAndAlpha() + { + const int pairCount = 65536; + const int channelCount = 3; + Rgba32[] source = new Rgba32[pairCount * channelCount]; + NormalizedByte4P[] expected = new NormalizedByte4P[source.Length]; + NormalizedByte4P[] actualBulk = new NormalizedByte4P[source.Length]; + int index = 0; + + for (int alpha = 0; alpha <= byte.MaxValue; alpha++) + { + // NormalizedByte4 has 254 intervals from -1 through 1, so alpha is first rounded to that destination grid. + int destinationAlpha = ((alpha * 254) + 127) / byte.MaxValue; + + for (int unassociated = 0; unassociated <= byte.MaxValue; unassociated++) + { + // Association uses the alpha representable by the destination, then rounds the result to the same 254-interval grid. + int associated = ((unassociated * destinationAlpha) + 127) / byte.MaxValue; + + source[index] = new Rgba32((byte)unassociated, 0, 0, (byte)alpha); + expected[index++] = CreateNormalizedByte4P(associated, 0, 0, destinationAlpha); + source[index] = new Rgba32(0, (byte)unassociated, 0, (byte)alpha); + expected[index++] = CreateNormalizedByte4P(0, associated, 0, destinationAlpha); + source[index] = new Rgba32(0, 0, (byte)unassociated, (byte)alpha); + expected[index++] = CreateNormalizedByte4P(0, 0, associated, destinationAlpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualBulk); + + Assert.Equal(expected, actualBulk); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(expected[i], NormalizedByte4P.FromRgba32(source[i])); + Assert.Equal(expected[i], Color.FromPixel(source[i]).ToPixel()); + } + } + + [Fact] + public void Rgba32ToHalfVector4PMatchesExactAssociationForEveryComponentAndAlpha() + { + const int pairCount = 65536; + const int channelCount = 3; + + const float finiteMinimum = -65504F; + const float finiteRange = 131008F; + const float inverseFiniteRange = (float)(1D / finiteRange); + const ulong zeroComponentBits = 0xFBFF; + Rgba32[] source = new Rgba32[pairCount * channelCount]; + HalfVector4P[] expected = new HalfVector4P[source.Length]; + HalfVector4P[] actualBulk = new HalfVector4P[source.Length]; + int index = 0; + + for (int alpha = 0; alpha <= byte.MaxValue; alpha++) + { + // Association must use the alpha value recovered from the destination half, rather than the higher-precision source alpha. + float normalizedAlpha = (float)(alpha / (double)byte.MaxValue); + ushort alphaBits = BitConverter.HalfToUInt16Bits((Half)((normalizedAlpha * finiteRange) + finiteMinimum)); + float destinationAlpha = ((float)BitConverter.UInt16BitsToHalf(alphaBits) * inverseFiniteRange) + .5F; + + for (int unassociated = 0; unassociated <= byte.MaxValue; unassociated++) + { + float normalizedComponent = (float)(unassociated / (double)byte.MaxValue); + float associated = normalizedComponent * destinationAlpha; + ushort associatedBits = BitConverter.HalfToUInt16Bits((Half)((associated * finiteRange) + finiteMinimum)); + ulong alphaPacked = (ulong)alphaBits << 48; + + source[index] = new Rgba32((byte)unassociated, 0, 0, (byte)alpha); + expected[index++] = new HalfVector4P { PackedValue = associatedBits | (zeroComponentBits << 16) | (zeroComponentBits << 32) | alphaPacked }; + source[index] = new Rgba32(0, (byte)unassociated, 0, (byte)alpha); + expected[index++] = new HalfVector4P { PackedValue = zeroComponentBits | ((ulong)associatedBits << 16) | (zeroComponentBits << 32) | alphaPacked }; + source[index] = new Rgba32(0, 0, (byte)unassociated, (byte)alpha); + expected[index++] = new HalfVector4P { PackedValue = zeroComponentBits | (zeroComponentBits << 16) | ((ulong)associatedBits << 32) | alphaPacked }; + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualBulk); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(expected[i], HalfVector4P.FromRgba32(source[i])); + Assert.Equal(expected[i], Color.FromPixel(source[i]).ToPixel()); + Assert.Equal(expected[i], actualBulk[i]); + } + } + + [Fact] + public void Rgba32PToRgba32ScalarRoundTripPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + { + for (int alpha = 0; alpha <= byte.MaxValue; alpha++) + { + // Components greater than alpha are valid for additive blending but cannot round-trip losslessly through an 8-bit unassociated pixel. + // The exhaustive lossless domain is therefore triangular rather than 256 squared. + for (int associated = 0; associated <= alpha; associated++) + { + // This integer expression is the exact nearest 8-bit unassociated value for associated * 255 / alpha. + byte unassociated = alpha == 0 ? (byte)0 : (byte)(((associated * byte.MaxValue) + (alpha / 2)) / alpha); + Rgba32P source = new((byte)associated, 0, 0, (byte)alpha); + Rgba32 expectedUnassociated = new(unassociated, 0, 0, (byte)alpha); + + Rgba32 actualUnassociated = source.ToRgba32(); + Rgba32P actualRoundTrip = Rgba32P.FromRgba32(actualUnassociated); + + Assert.Equal(expectedUnassociated, actualUnassociated); + Assert.Equal(source, actualRoundTrip); + } + } + } + + [Fact] + public void ColorFromRgba32PPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + { + const int pairCount = 32896; + Rgba32P[] sourcePixels = new Rgba32P[pairCount]; + Color[] bulkColors = new Color[pairCount]; + int index = 0; + + for (int alpha = 0; alpha <= byte.MaxValue; alpha++) + { + for (int associated = 0; associated <= alpha; associated++) + { + byte unassociated = alpha == 0 ? (byte)0 : (byte)(((associated * byte.MaxValue) + (alpha / 2)) / alpha); + Rgba32P source = new((byte)associated, 0, 0, (byte)alpha); + Color color = Color.FromPixel(source); + sourcePixels[index++] = source; + + Assert.Equal(new Rgba32(unassociated, 0, 0, (byte)alpha), color.ToPixel()); + Assert.Equal(source, color.ToPixel()); + } + } + + Color.FromPixel(sourcePixels, bulkColors); + + for (int i = 0; i < sourcePixels.Length; i++) + { + Assert.Equal(sourcePixels[i], bulkColors[i].ToPixel()); + } + } + + [Fact] + public void Rgba32PToBgra32RoundTripPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + => AssertUnsignedByteBulkRoundTrip((red, green, blue, alpha) => new Rgba32P(red, green, blue, alpha)); + + [Fact] + public void Bgra32PToBgra32RoundTripPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + => AssertUnsignedByteBulkRoundTrip((red, green, blue, alpha) => new Bgra32P(red, green, blue, alpha)); + + [Fact] + public void Argb32PToBgra32RoundTripPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + => AssertUnsignedByteBulkRoundTrip((red, green, blue, alpha) => new Argb32P(red, green, blue, alpha)); + + [Fact] + public void Abgr32PToBgra32RoundTripPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + => AssertUnsignedByteBulkRoundTrip((red, green, blue, alpha) => new Abgr32P(red, green, blue, alpha)); + + [Fact] + public void NormalizedByte4PToRgba32ScalarRoundTripPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + { + for (int alpha = 0; alpha < byte.MaxValue; alpha++) + { + for (int associated = 0; associated <= alpha; associated++) + { + byte unassociated = alpha == 0 ? (byte)0 : (byte)(((associated * byte.MaxValue) + (alpha / 2)) / alpha); + byte unassociatedAlpha = (byte)(((alpha * byte.MaxValue) + 127) / 254); + NormalizedByte4P source = CreateNormalizedByte4P(associated, 0, 0, alpha); + + Rgba32 actualUnassociated = source.ToRgba32(); + NormalizedByte4P actualRoundTrip = NormalizedByte4P.FromRgba32(actualUnassociated); + + Assert.Equal(new Rgba32(unassociated, 0, 0, unassociatedAlpha), actualUnassociated); + Assert.Equal(source, actualRoundTrip); + } + } + } + + [Fact] + public void ColorFromNormalizedByte4PPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + { + const int pairCount = 32640; + NormalizedByte4P[] sourcePixels = new NormalizedByte4P[pairCount]; + Color[] bulkColors = new Color[pairCount]; + int index = 0; + + for (int alpha = 0; alpha < byte.MaxValue; alpha++) + { + for (int associated = 0; associated <= alpha; associated++) + { + byte unassociated = alpha == 0 ? (byte)0 : (byte)(((associated * byte.MaxValue) + (alpha / 2)) / alpha); + byte unassociatedAlpha = (byte)(((alpha * byte.MaxValue) + 127) / 254); + NormalizedByte4P source = CreateNormalizedByte4P(associated, 0, 0, alpha); + Color color = Color.FromPixel(source); + sourcePixels[index++] = source; + + Assert.Equal(new Rgba32(unassociated, 0, 0, unassociatedAlpha), color.ToPixel()); + Assert.Equal(source, color.ToPixel()); + } + } + + Color.FromPixel(sourcePixels, bulkColors); + + for (int i = 0; i < sourcePixels.Length; i++) + { + Assert.Equal(sourcePixels[i], bulkColors[i].ToPixel()); + } + } + + [Fact] + public void NormalizedByte4PToBgra32RoundTripPreservesEveryLosslesslyRoundTrippableAssociatedComponent() + { + const int pairCount = 32640; + const int channelCount = 3; + NormalizedByte4P[] source = new NormalizedByte4P[pairCount * channelCount]; + Bgra32[] expectedUnassociated = new Bgra32[source.Length]; + Bgra32[] actualUnassociated = new Bgra32[source.Length]; + NormalizedByte4P[] actualRoundTrip = new NormalizedByte4P[source.Length]; + int index = 0; + + for (int alpha = 0; alpha < byte.MaxValue; alpha++) + { + for (int associated = 0; associated <= alpha; associated++) + { + byte unassociated = alpha == 0 ? (byte)0 : (byte)(((associated * byte.MaxValue) + (alpha / 2)) / alpha); + byte unassociatedAlpha = (byte)(((alpha * byte.MaxValue) + 127) / 254); + + source[index] = CreateNormalizedByte4P(associated, 0, 0, alpha); + expectedUnassociated[index++] = new Bgra32(unassociated, 0, 0, unassociatedAlpha); + source[index] = CreateNormalizedByte4P(0, associated, 0, alpha); + expectedUnassociated[index++] = new Bgra32(0, unassociated, 0, unassociatedAlpha); + source[index] = CreateNormalizedByte4P(0, 0, associated, alpha); + expectedUnassociated[index++] = new Bgra32(0, 0, unassociated, unassociatedAlpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualUnassociated); + PixelOperations.Instance.From(Configuration.Default, actualUnassociated, actualRoundTrip); + + Assert.Equal(expectedUnassociated, actualUnassociated); + Assert.Equal(source, actualRoundTrip); + + for (int i = 0; i < source.Length; i++) + { + Bgra32 expected = expectedUnassociated[i]; + + // Scalar conversion and Color must preserve the same exact canonical value proven for the bulk path. + Assert.Equal(new Rgba32(expected.R, expected.G, expected.B, expected.A), source[i].ToRgba32()); + Assert.Equal(expected, Color.FromPixel(source[i]).ToPixel()); + } + } + + private static void AssertUnsignedByteBulkRoundTrip(Func createPixel) + where TPixel : unmanaged, IPixel + { + const int pairCount = 32896; + const int channelCount = 3; + TPixel[] source = new TPixel[pairCount * channelCount]; + Bgra32[] expectedUnassociated = new Bgra32[source.Length]; + Bgra32[] actualUnassociated = new Bgra32[source.Length]; + TPixel[] actualRoundTrip = new TPixel[source.Length]; + int index = 0; + + for (int alpha = 0; alpha <= byte.MaxValue; alpha++) + { + // Components greater than alpha are valid for additive blending but cannot round-trip losslessly through an 8-bit unassociated pixel. + // The exhaustive lossless domain is therefore triangular rather than 256 squared. + for (int associated = 0; associated <= alpha; associated++) + { + // This integer expression is the exact nearest 8-bit unassociated value for associated * 255 / alpha. + byte unassociated = alpha == 0 ? (byte)0 : (byte)(((associated * byte.MaxValue) + (alpha / 2)) / alpha); + + source[index] = createPixel((byte)associated, 0, 0, (byte)alpha); + expectedUnassociated[index++] = new Bgra32(unassociated, 0, 0, (byte)alpha); + source[index] = createPixel(0, (byte)associated, 0, (byte)alpha); + expectedUnassociated[index++] = new Bgra32(0, unassociated, 0, (byte)alpha); + source[index] = createPixel(0, 0, (byte)associated, (byte)alpha); + expectedUnassociated[index++] = new Bgra32(0, 0, unassociated, (byte)alpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualUnassociated); + PixelOperations.Instance.From(Configuration.Default, actualUnassociated, actualRoundTrip); + + Assert.Equal(expectedUnassociated, actualUnassociated); + Assert.Equal(source, actualRoundTrip); + + for (int i = 0; i < source.Length; i++) + { + Bgra32 expected = expectedUnassociated[i]; + + // Scalar conversion and Color must use the same exact canonical byte as the independently calculated bulk oracle. + Assert.Equal(new Rgba32(expected.R, expected.G, expected.B, expected.A), source[i].ToRgba32()); + Assert.Equal(expected, Color.FromPixel(source[i]).ToPixel()); + } + } + + private static void AssertUnsignedByteAssociation(Func createPixel) + where TPixel : unmanaged, IPixel + { + const int pairCount = 65536; + const int channelCount = 3; + Rgba32[] source = new Rgba32[pairCount * channelCount]; + TPixel[] expected = new TPixel[source.Length]; + TPixel[] actualBulk = new TPixel[source.Length]; + int index = 0; + + for (int alpha = 0; alpha <= byte.MaxValue; alpha++) + { + for (int unassociated = 0; unassociated <= byte.MaxValue; unassociated++) + { + // The destination stores round(unassociated * alpha / 255). The integer numerator adds half + // the divisor so the expected value is independent of the floating-point implementation. + byte associated = (byte)(((unassociated * alpha) + 127) / byte.MaxValue); + + source[index] = new Rgba32((byte)unassociated, 0, 0, (byte)alpha); + expected[index++] = createPixel(associated, 0, 0, (byte)alpha); + source[index] = new Rgba32(0, (byte)unassociated, 0, (byte)alpha); + expected[index++] = createPixel(0, associated, 0, (byte)alpha); + source[index] = new Rgba32(0, 0, (byte)unassociated, (byte)alpha); + expected[index++] = createPixel(0, 0, associated, (byte)alpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualBulk); + + Assert.Equal(expected, actualBulk); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(expected[i], TPixel.FromRgba32(source[i])); + Assert.Equal(expected[i], Color.FromPixel(source[i]).ToPixel()); + } + } + + private static NormalizedByte4P CreateNormalizedByte4P(int red, int green, int blue, int alpha) + { + uint packed = (byte)(red - 127) + | ((uint)(byte)(green - 127) << 8) + | ((uint)(byte)(blue - 127) << 16) + | ((uint)(byte)(alpha - 127) << 24); + + return new NormalizedByte4P { PackedValue = packed }; + } +} + +/// +/// Tests that associated destinations use their stored alpha value when associating color components. +/// +public class AssociatedDestinationAlphaQuantizationTests +{ + [Fact] + public void Rgba32PQuantizesDestinationAlphaBeforeAssociation() => AssertUnsignedByteDestinationQuantizesAlphaBeforeAssociation(); + + [Fact] + public void Bgra32PQuantizesDestinationAlphaBeforeAssociation() => AssertUnsignedByteDestinationQuantizesAlphaBeforeAssociation(); + + [Fact] + public void Argb32PQuantizesDestinationAlphaBeforeAssociation() => AssertUnsignedByteDestinationQuantizesAlphaBeforeAssociation(); + + [Fact] + public void Abgr32PQuantizesDestinationAlphaBeforeAssociation() => AssertUnsignedByteDestinationQuantizesAlphaBeforeAssociation(); + + [Fact] + public void NormalizedByte4PQuantizesDestinationAlphaBeforeAssociation() + { + ReadOnlySpan components = [64, 127, 191]; + Rgba64[] source = new Rgba64[(ushort.MaxValue + 1) * components.Length]; + NormalizedByte4P[] actualBulk = new NormalizedByte4P[source.Length]; + int index = 0; + + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + foreach (byte component in components) + { + source[index++] = new Rgba64((ushort)(component * 257), 0, 0, (ushort)alpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualBulk); + index = 0; + + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + int expectedAlpha = ((alpha * 254) + 32767) / ushort.MaxValue; + + foreach (byte component in components) + { + int expectedRed = ((component * expectedAlpha) + 127) / byte.MaxValue; + NormalizedByte4P actualScalar = NormalizedByte4P.FromRgba64(source[index]); + int scalarRed = (sbyte)actualScalar.PackedValue + 127; + int scalarAlpha = (sbyte)(actualScalar.PackedValue >> 24) + 127; + int bulkRed = (sbyte)actualBulk[index].PackedValue + 127; + int bulkAlpha = (sbyte)(actualBulk[index].PackedValue >> 24) + 127; + + Assert.Equal(expectedRed, scalarRed); + Assert.Equal(expectedAlpha, scalarAlpha); + Assert.Equal(expectedRed, bulkRed); + Assert.Equal(expectedAlpha, bulkAlpha); + index++; + } + } + } + + [Fact] + public void HalfVector4PQuantizesDestinationAlphaBeforeAssociation() + { + const float finiteMinimum = -65504F; + const float finiteRange = 131008F; + const float inverseFiniteRange = (float)(1D / finiteRange); + ReadOnlySpan components = [64, 127, 191]; + Rgba64[] source = new Rgba64[(ushort.MaxValue + 1) * components.Length]; + HalfVector4P[] actualBulk = new HalfVector4P[source.Length]; + int index = 0; + + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + foreach (byte component in components) + { + source[index++] = new Rgba64((ushort)(component * 257), 0, 0, (ushort)alpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualBulk); + index = 0; + + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + float normalizedAlpha = alpha / (float)ushort.MaxValue; + ushort expectedAlpha = BitConverter.HalfToUInt16Bits((Half)((normalizedAlpha * finiteRange) + finiteMinimum)); + float storedAlpha = ((float)BitConverter.UInt16BitsToHalf(expectedAlpha) * inverseFiniteRange) + .5F; + + foreach (byte component in components) + { + float associatedRed = (component / (float)byte.MaxValue) * storedAlpha; + ushort expectedRed = BitConverter.HalfToUInt16Bits((Half)((associatedRed * finiteRange) + finiteMinimum)); + HalfVector4P actualScalar = HalfVector4P.FromRgba64(source[index]); + + Assert.Equal(expectedRed, (ushort)actualScalar.PackedValue); + Assert.Equal(expectedAlpha, (ushort)(actualScalar.PackedValue >> 48)); + Assert.Equal(expectedRed, (ushort)actualBulk[index].PackedValue); + Assert.Equal(expectedAlpha, (ushort)(actualBulk[index].PackedValue >> 48)); + index++; + } + } + } + + [Fact] + public void RgbaHalfPQuantizesDestinationAlphaBeforeAssociation() + { + ReadOnlySpan components = [64, 127, 191]; + Rgba64[] source = new Rgba64[(ushort.MaxValue + 1) * components.Length]; + RgbaHalfP[] actualBulk = new RgbaHalfP[source.Length]; + int index = 0; + + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + foreach (byte component in components) + { + source[index++] = new Rgba64((ushort)(component * 257), 0, 0, (ushort)alpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualBulk); + index = 0; + + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + float normalizedAlpha = alpha / (float)ushort.MaxValue; + Half expectedAlpha = (Half)normalizedAlpha; + float storedAlpha = (float)expectedAlpha; + + foreach (byte component in components) + { + Half expectedRed = (Half)((component / (float)byte.MaxValue) * storedAlpha); + RgbaHalfP actualScalar = RgbaHalfP.FromRgba64(source[index]); + + Assert.Equal(expectedRed, actualScalar.R); + Assert.Equal(expectedAlpha, actualScalar.A); + Assert.Equal(expectedRed, actualBulk[index].R); + Assert.Equal(expectedAlpha, actualBulk[index].A); + index++; + } + } + } + + [Fact] + public void ColorToRgba32PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + + [Fact] + public void ColorToBgra32PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + + [Fact] + public void ColorToArgb32PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + + [Fact] + public void ColorToAbgr32PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + + [Fact] + public void ColorToNormalizedByte4PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + + [Fact] + public void ColorToHalfVector4PUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + + [Fact] + public void ColorToRgbaHalfPUsesDestinationAlphaRepresentation() => AssertColorUsesDestinationAlphaRepresentation(); + + /// + /// Verifies the unsigned-byte destination grid through scalar and bulk conversion entry points. + /// + /// The associated unsigned-byte pixel format. + private static void AssertUnsignedByteDestinationQuantizesAlphaBeforeAssociation() + where TPixel : unmanaged, IPixel + { + ReadOnlySpan components = [64, 127, 191]; + Rgba64[] source = new Rgba64[(ushort.MaxValue + 1) * components.Length]; + TPixel[] actualBulk = new TPixel[source.Length]; + int index = 0; + + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + foreach (byte component in components) + { + source[index++] = new Rgba64((ushort)(component * 257), 0, 0, (ushort)alpha); + } + } + + PixelOperations.Instance.From(Configuration.Default, source, actualBulk); + index = 0; + + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + int expectedAlpha = ((alpha * byte.MaxValue) + 32767) / ushort.MaxValue; + + foreach (byte component in components) + { + int expectedRed = ((component * expectedAlpha) + 127) / byte.MaxValue; + Vector4 expected = new Vector4(expectedRed, 0, 0, expectedAlpha) / byte.MaxValue; + + Assert.Equal(expected, TPixel.FromRgba64(source[index]).ToScaledVector4()); + Assert.Equal(expected, actualBulk[index].ToScaledVector4()); + index++; + } + } + } + + /// + /// Verifies that delegates association to the destination pixel operations. + /// + /// The associated destination pixel format. + private static void AssertColorUsesDestinationAlphaRepresentation() + where TPixel : unmanaged, IPixel + { + for (int alpha = 0; alpha <= ushort.MaxValue; alpha++) + { + ushort component = (ushort)((byte)alpha * 257); + Rgba64 source = new(component, 0, 0, (ushort)alpha); + TPixel expected = TPixel.FromRgba64(source); + TPixel actual = Color.FromPixel(source).ToPixel(); + + Assert.Equal(expected, actual); + } + } +} diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs index 9366c51c9..3e06f0471 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -26,45 +27,62 @@ public class HalfSingleTests public void HalfSingle_ToVector4() { // arrange - HalfSingle pixel = new(0.5f); - Vector4 expected = new(0.5f, 0, 0, 1); + HalfSingle pixel = new(-2F); + Vector4 expected = new(-2F, 0, 0, 1); // act Vector4 actual = pixel.ToVector4(); // assert Assert.Equal(expected, actual); + Assert.Equal((float)Half.MinValue, new HalfSingle((float)Half.MinValue).ToSingle()); + Assert.Equal((float)Half.MaxValue, new HalfSingle((float)Half.MaxValue).ToSingle()); } [Fact] public void HalfSingle_ToScaledVector4() { - // arrange - HalfSingle pixel = new(-1F); - - // act - Vector4 actual = pixel.ToScaledVector4(); - - // assert - Assert.Equal(0, actual.X); - Assert.Equal(0, actual.Y); - Assert.Equal(0, actual.Z); - Assert.Equal(1, actual.W); + Assert.Equal(new Vector4(0F, 0F, 0F, 1F), new HalfSingle((float)Half.MinValue).ToScaledVector4()); + Assert.Equal(new Vector4(.5F, 0F, 0F, 1F), new HalfSingle(0F).ToScaledVector4()); + Assert.Equal(new Vector4(1F, 0F, 0F, 1F), new HalfSingle((float)Half.MaxValue).ToScaledVector4()); } [Fact] public void HalfSingle_FromScaledVector4() { - // arrange - Vector4 scaled = new HalfSingle(-1F).ToScaledVector4(); - const int expected = 48128; + Assert.Equal((ushort)0xFBFF, HalfSingle.FromScaledVector4(new Vector4(0F, 0F, 0F, 1F)).PackedValue); + Assert.Equal((ushort)0, HalfSingle.FromScaledVector4(new Vector4(.5F, 0F, 0F, 1F)).PackedValue); + Assert.Equal((ushort)0x7BFF, HalfSingle.FromScaledVector4(new Vector4(1F, 0F, 0F, 1F)).PackedValue); + } - // act - HalfSingle pixel = HalfSingle.FromScaledVector4(scaled); - ushort actual = pixel.PackedValue; + [Fact] + public void HalfSingle_BulkScaledConversionsCoverFiniteRange() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertHalfSingleBulkScaledConversionsCoverFiniteRange, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); - // assert - Assert.Equal(expected, actual); + private static void AssertHalfSingleBulkScaledConversionsCoverFiniteRange() + { + ushort[] packedValues = [0xFBFF, 0, 0x7BFF]; + Vector4[] scaledValues = [new(0F, 0F, 0F, 1F), new(.5F, 0F, 0F, 1F), new(1F, 0F, 0F, 1F)]; + HalfSingle[] source = new HalfSingle[17]; + Vector4[] expectedVectors = new Vector4[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + int valueIndex = i % packedValues.Length; + source[i].PackedValue = packedValues[valueIndex]; + expectedVectors[i] = scaledValues[valueIndex]; + } + + Vector4[] actualVectors = new Vector4[source.Length]; + PixelOperations.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + HalfSingle[] actualPixels = new HalfSingle[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs index c5a89df1e..1fde34a82 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -25,19 +26,21 @@ public class HalfVector2Tests Assert.Equal(Vector2.One, new HalfVector2(Vector2.One).ToVector2()); Assert.Equal(Vector2.UnitX, new HalfVector2(Vector2.UnitX).ToVector2()); Assert.Equal(Vector2.UnitY, new HalfVector2(Vector2.UnitY).ToVector2()); + Assert.Equal(new Vector2(-2F, 4F), new HalfVector2(-2F, 4F).ToVector2()); + Assert.Equal(new Vector2((float)Half.MinValue, (float)Half.MaxValue), new HalfVector2((float)Half.MinValue, (float)Half.MaxValue).ToVector2()); } [Fact] public void HalfVector2_ToScaledVector4() { // arrange - HalfVector2 halfVector = new(Vector2.One); + HalfVector2 halfVector = new((float)Half.MinValue, (float)Half.MaxValue); // act Vector4 actual = halfVector.ToScaledVector4(); // assert - Assert.Equal(1F, actual.X); + Assert.Equal(0F, actual.X); Assert.Equal(1F, actual.Y); Assert.Equal(0, actual.Z); Assert.Equal(1, actual.W); @@ -47,8 +50,8 @@ public class HalfVector2Tests public void HalfVector2_FromScaledVector4() { // arrange - Vector4 scaled = new HalfVector2(Vector2.One).ToScaledVector4(); - const uint expected = 1006648320u; + Vector4 scaled = new(0F, 1F, 0F, 1F); + const uint expected = 0x7BFF_FBFF; // act HalfVector2 halfVector = HalfVector2.FromScaledVector4(scaled); @@ -58,6 +61,30 @@ public class HalfVector2Tests Assert.Equal(expected, actual); } + [Fact] + public void HalfVector2_BulkScaledConversionsCoverFiniteRange() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertHalfVector2BulkScaledConversionsCoverFiniteRange, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertHalfVector2BulkScaledConversionsCoverFiniteRange() + { + HalfVector2 pixel = new() { PackedValue = 0x7BFF_FBFF }; + HalfVector2[] source = new HalfVector2[17]; + Vector4[] expectedVectors = new Vector4[source.Length]; + Array.Fill(source, pixel); + Array.Fill(expectedVectors, new Vector4(0F, 1F, 0F, 1F)); + + Vector4[] actualVectors = new Vector4[source.Length]; + PixelOperations.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + HalfVector2[] actualPixels = new HalfVector2[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); + } + [Fact] public void HalfVector2_ToVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs index 16c78a23d..75130c144 100644 --- a/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -33,30 +34,32 @@ public class HalfVector4Tests Assert.Equal(Vector4.UnitY, new HalfVector4(Vector4.UnitY).ToVector4()); Assert.Equal(Vector4.UnitZ, new HalfVector4(Vector4.UnitZ).ToVector4()); Assert.Equal(Vector4.UnitW, new HalfVector4(Vector4.UnitW).ToVector4()); + Assert.Equal(new Vector4(-2F, 4F, .5F, 8F), new HalfVector4(-2F, 4F, .5F, 8F).ToVector4()); + Assert.Equal( + new Vector4((float)Half.MinValue, (float)Half.MaxValue, (float)Half.MinValue, (float)Half.MaxValue), + new HalfVector4((float)Half.MinValue, (float)Half.MaxValue, (float)Half.MinValue, (float)Half.MaxValue).ToVector4()); } [Fact] public void HalfVector4_ToScaledVector4() { // arrange - HalfVector4 pixel = new(-Vector4.One); + Vector4 expected = new(0F, .5F, 1F, 0F); + HalfVector4 pixel = new((float)Half.MinValue, 0F, (float)Half.MaxValue, (float)Half.MinValue); // act Vector4 actual = pixel.ToScaledVector4(); // assert - Assert.Equal(0, actual.X); - Assert.Equal(0, actual.Y); - Assert.Equal(0, actual.Z); - Assert.Equal(0, actual.W); + Assert.Equal(expected, actual); } [Fact] public void HalfVector4_FromScaledVector4() { // arrange - Vector4 scaled = new HalfVector4(-Vector4.One).ToScaledVector4(); - const ulong expected = 13547034390470638592uL; + Vector4 scaled = new(0F, .25F, .5F, 1F); + ulong expected = new HalfVector4((float)Half.MinValue, -32752F, 0F, (float)Half.MaxValue).PackedValue; // act HalfVector4 pixel = HalfVector4.FromScaledVector4(scaled); @@ -66,6 +69,31 @@ public class HalfVector4Tests Assert.Equal(expected, actual); } + [Fact] + public void HalfVector4_BulkScaledConversionsCoverFiniteRange() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertHalfVector4BulkScaledConversionsCoverFiniteRange, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertHalfVector4BulkScaledConversionsCoverFiniteRange() + { + const ulong packedValue = 0xFBFF_7BFF_0000_FBFF; + HalfVector4 pixel = new() { PackedValue = packedValue }; + HalfVector4[] source = new HalfVector4[17]; + Vector4[] expectedVectors = new Vector4[source.Length]; + Array.Fill(source, pixel); + Array.Fill(expectedVectors, new Vector4(0F, .5F, 1F, 0F)); + + Vector4[] actualVectors = new Vector4[source.Length]; + PixelOperations.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + HalfVector4[] actualPixels = new HalfVector4[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); + } + [Fact] public void HalfVector4_FromBgra5551() { diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs index ffbddb139..e6be539c9 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -29,6 +30,42 @@ public class NormalizedByte2Tests Assert.Equal(-Vector2.One, new NormalizedByte2(Vector2.One * -1234.0f).ToVector2()); } + [Fact] + public void NormalizedByte2_MinimumStorageCodeDecodesAsNegativeOne() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertNormalizedByte2MinimumStorageCodeDecodesAsNegativeOne, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertNormalizedByte2MinimumStorageCodeDecodesAsNegativeOne() + { + NormalizedByte2 pixel = new() { PackedValue = 0x8080 }; + Vector4 expectedNative = new(-1F, -1F, 0F, 1F); + Vector4 expectedScaled = new(0F, 0F, 0F, 1F); + + Assert.Equal(expectedNative, pixel.ToVector4()); + Assert.Equal(expectedScaled, pixel.ToScaledVector4()); + + NormalizedByte2[] source = new NormalizedByte2[17]; + Vector4[] native = new Vector4[source.Length]; + Vector4[] scaled = new Vector4[source.Length]; + Array.Fill(source, pixel); + + PixelOperations.Instance.ToVector4(Configuration.Default, source, native); + PixelOperations.Instance.ToVector4(Configuration.Default, source, scaled, PixelConversionModifiers.Scale); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(expectedNative, native[i]); + Assert.Equal(expectedScaled, scaled[i]); + } + + Vector4[] destructiveSource = new Vector4[source.Length]; + Array.Fill(destructiveSource, expectedScaled); + NormalizedByte2[] actualPixels = new NormalizedByte2[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal((ushort)0x8181, actual.PackedValue)); + } + [Fact] public void NormalizedByte2_ToVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs index 5a025f6c4..c2a0047d2 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -60,6 +61,39 @@ public class NormalizedByte4Tests Assert.Equal(-Vector4.One, new NormalizedByte4(Vector4.One * -1234.0f).ToVector4()); } + [Fact] + public void NormalizedByte4_MinimumStorageCodeDecodesAsNegativeOne() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertNormalizedByte4MinimumStorageCodeDecodesAsNegativeOne, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertNormalizedByte4MinimumStorageCodeDecodesAsNegativeOne() + { + NormalizedByte4 pixel = new() { PackedValue = 0x80808080 }; + + Assert.Equal(-Vector4.One, pixel.ToVector4()); + Assert.Equal(Vector4.Zero, pixel.ToScaledVector4()); + + NormalizedByte4[] source = new NormalizedByte4[17]; + Vector4[] native = new Vector4[source.Length]; + Vector4[] scaled = new Vector4[source.Length]; + Array.Fill(source, pixel); + + PixelOperations.Instance.ToVector4(Configuration.Default, source, native); + PixelOperations.Instance.ToVector4(Configuration.Default, source, scaled, PixelConversionModifiers.Scale); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(-Vector4.One, native[i]); + Assert.Equal(Vector4.Zero, scaled[i]); + } + + Vector4[] destructiveSource = new Vector4[source.Length]; + NormalizedByte4[] actualPixels = new NormalizedByte4[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal(0x81818181U, actual.PackedValue)); + } + [Fact] public void NormalizedByte4_ToScaledVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs index be7b39052..9cb3aee50 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -33,6 +34,42 @@ public class NormalizedShort2Tests Assert.Equal(-Vector2.One, new NormalizedShort2(Vector2.One * -1234.0f).ToVector2()); } + [Fact] + public void NormalizedShort2_MinimumStorageCodeDecodesAsNegativeOne() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertNormalizedShort2MinimumStorageCodeDecodesAsNegativeOne, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertNormalizedShort2MinimumStorageCodeDecodesAsNegativeOne() + { + NormalizedShort2 pixel = new() { PackedValue = 0x80008000 }; + Vector4 expectedNative = new(-1F, -1F, 0F, 1F); + Vector4 expectedScaled = new(0F, 0F, 0F, 1F); + + Assert.Equal(expectedNative, pixel.ToVector4()); + Assert.Equal(expectedScaled, pixel.ToScaledVector4()); + + NormalizedShort2[] source = new NormalizedShort2[17]; + Vector4[] native = new Vector4[source.Length]; + Vector4[] scaled = new Vector4[source.Length]; + Array.Fill(source, pixel); + + PixelOperations.Instance.ToVector4(Configuration.Default, source, native); + PixelOperations.Instance.ToVector4(Configuration.Default, source, scaled, PixelConversionModifiers.Scale); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(expectedNative, native[i]); + Assert.Equal(expectedScaled, scaled[i]); + } + + Vector4[] destructiveSource = new Vector4[source.Length]; + Array.Fill(destructiveSource, expectedScaled); + NormalizedShort2[] actualPixels = new NormalizedShort2[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal(0x80018001U, actual.PackedValue)); + } + [Fact] public void NormalizedShort2_ToVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs index 281ae7ee5..c317586e4 100644 --- a/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -61,6 +62,39 @@ public class NormalizedShort4Tests Assert.Equal(-Vector4.One, new NormalizedShort4(Vector4.One * -1234.0f).ToVector4()); } + [Fact] + public void NormalizedShort4_MinimumStorageCodeDecodesAsNegativeOne() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertNormalizedShort4MinimumStorageCodeDecodesAsNegativeOne, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertNormalizedShort4MinimumStorageCodeDecodesAsNegativeOne() + { + NormalizedShort4 pixel = new() { PackedValue = 0x8000800080008000 }; + + Assert.Equal(-Vector4.One, pixel.ToVector4()); + Assert.Equal(Vector4.Zero, pixel.ToScaledVector4()); + + NormalizedShort4[] source = new NormalizedShort4[17]; + Vector4[] native = new Vector4[source.Length]; + Vector4[] scaled = new Vector4[source.Length]; + Array.Fill(source, pixel); + + PixelOperations.Instance.ToVector4(Configuration.Default, source, native); + PixelOperations.Instance.ToVector4(Configuration.Default, source, scaled, PixelConversionModifiers.Scale); + + for (int i = 0; i < source.Length; i++) + { + Assert.Equal(-Vector4.One, native[i]); + Assert.Equal(Vector4.Zero, scaled[i]); + } + + Vector4[] destructiveSource = new Vector4[source.Length]; + NormalizedShort4[] actualPixels = new NormalizedShort4[source.Length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.All(actualPixels, actual => Assert.Equal(0x8001800180018001UL, actual.PackedValue)); + } + [Fact] public void NormalizedShort4_ToScaledVector4() { diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs new file mode 100644 index 000000000..2cc26e76c --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs @@ -0,0 +1,548 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +/// +/// Verifies scalar and bulk alpha-representation conversions for a pixel format. +/// +/// The pixel format. +[Trait("Category", "PixelFormats")] +public abstract class PixelAlphaRepresentationTests + where TPixel : unmanaged, IPixel +{ + private static readonly Vector4[] UnassociatedScaledVectors = + [ + new(.8F, .4F, .2F, .5F), + new(.15F, .65F, .35F, .25F), + new(.9F, .1F, .7F, 1F), + Vector4.Zero + ]; + + private static readonly Vector4[] OutOfRangeUnassociatedScaledVectors = + [ + new(-.25F, 1.25F, .5F, .75F), + new(1.5F, -.5F, 2F, 1F) + ]; + + [Fact] + public void ScalarScaledConversionsDescribeTheSameLogicalColors() + { + foreach (Vector4 unassociated in UnassociatedScaledVectors) + { + Vector4 associated = Associate(unassociated); + TPixel fromUnassociated = TPixel.FromUnassociatedScaledVector4(unassociated); + TPixel fromAssociated = TPixel.FromAssociatedScaledVector4(associated); + + Assert.Equal(fromUnassociated, fromAssociated); + } + } + + [Fact] + public void ScalarAssociatedVectorsContainPremultipliedRgb() + { + foreach (Vector4 source in UnassociatedScaledVectors) + { + TPixel pixel = TPixel.FromUnassociatedScaledVector4(source); + Vector4 unassociatedScaled = pixel.ToUnassociatedScaledVector4(); + Vector4 expectedAssociatedScaled = Associate(unassociatedScaled); + Vector4 actualAssociatedScaled = pixel.ToAssociatedScaledVector4(); + + if (typeof(TPixel) == typeof(NormalizedByte4P)) + { + // Exhaustive valid-component coverage proves that signed-byte unassociation followed by reassociation can differ by at most two ULP. + AlphaRepresentationTestAssertions.EqualWithinTwoUlps(expectedAssociatedScaled, actualAssociatedScaled); + } + else + { + // Reversing association can introduce one final float rounding step, but this still rejects an unassociated no-op by several orders of magnitude. + AlphaRepresentationTestAssertions.EqualWithinOneUlp(expectedAssociatedScaled, actualAssociatedScaled); + } + + Assert.Equal(BitConverter.SingleToInt32Bits(expectedAssociatedScaled.W), BitConverter.SingleToInt32Bits(actualAssociatedScaled.W)); + + Vector4 unassociatedNative = pixel.ToUnassociatedVector4(); + + if (unassociatedNative == unassociatedScaled) + { + // Native-equals-scaled formats use the same independent oracle; affine-native formats are verified by AffineNativeAlphaRepresentationTests. + AlphaRepresentationTestAssertions.EqualWithinOneUlp(expectedAssociatedScaled, pixel.ToAssociatedVector4()); + } + } + } + + [Fact] + public void ScalarScaledConversionsClampHighAlphaConsistently() + { + Vector4 unassociated = new(.8F, .4F, .2F, 1.5F); + Vector4 associated = Associate(unassociated); + + // Both entry points describe the same logical color, so clamping opacity at the storage boundary must not change RGB differently between them. + Assert.Equal(TPixel.FromUnassociatedScaledVector4(unassociated), TPixel.FromAssociatedScaledVector4(associated)); + } + + [Fact] + public void ScalarNativeConversionsRoundTripTheirDeclaredRepresentations() + { + foreach (Vector4 source in UnassociatedScaledVectors) + { + TPixel pixel = TPixel.FromUnassociatedScaledVector4(source); + TPixel fromUnassociated = TPixel.FromUnassociatedVector4(pixel.ToUnassociatedVector4()); + TPixel fromAssociated = TPixel.FromAssociatedVector4(pixel.ToAssociatedVector4()); + + Assert.Equal(pixel, fromUnassociated); + + if (pixel.ToUnassociatedScaledVector4().W == 0F) + { + // Associated color cannot encode hidden RGB at zero alpha. The round trip must therefore produce the format's transparent black value. + Assert.Equal(TPixel.FromUnassociatedScaledVector4(Vector4.Zero), fromAssociated); + } + else + { + Assert.Equal(pixel, fromAssociated); + } + } + } + + [Fact] + public void ParameterlessVectorConversionsUseTheDeclaredAlphaRepresentation() + { + PixelAlphaRepresentation alphaRepresentation = TPixel.GetPixelTypeInfo().AlphaRepresentation; + + foreach (Vector4 source in UnassociatedScaledVectors) + { + TPixel pixel = TPixel.FromUnassociatedScaledVector4(source); + Vector4 expectedNative = alphaRepresentation == PixelAlphaRepresentation.Associated + ? pixel.ToAssociatedVector4() + : pixel.ToUnassociatedVector4(); + + Vector4 expectedScaled = alphaRepresentation == PixelAlphaRepresentation.Associated + ? pixel.ToAssociatedScaledVector4() + : pixel.ToUnassociatedScaledVector4(); + + TPixel expectedFromNative = alphaRepresentation == PixelAlphaRepresentation.Associated + ? TPixel.FromAssociatedVector4(expectedNative) + : TPixel.FromUnassociatedVector4(expectedNative); + + TPixel expectedFromScaled = alphaRepresentation == PixelAlphaRepresentation.Associated + ? TPixel.FromAssociatedScaledVector4(expectedScaled) + : TPixel.FromUnassociatedScaledVector4(expectedScaled); + + Assert.Equal(expectedNative, pixel.ToVector4()); + Assert.Equal(expectedScaled, pixel.ToScaledVector4()); + Assert.Equal(expectedFromNative, TPixel.FromVector4(expectedNative)); + Assert.Equal(expectedFromScaled, TPixel.FromScaledVector4(expectedScaled)); + } + } + + [Fact] + public void BulkConversionsMatchScalarConversions() + { + const int length = 259; + TPixel[] pixels = new TPixel[length]; + + for (int i = 0; i < pixels.Length; i++) + { + Vector4 source = UnassociatedScaledVectors[i % UnassociatedScaledVectors.Length]; + pixels[i] = TPixel.FromUnassociatedScaledVector4(source); + } + + AssertBulkToMatchesScalar(pixels, static pixel => pixel.ToUnassociatedVector4(), static (operations, source, destination) => operations.ToVector4(Configuration.Default, source, destination, PixelConversionModifiers.UnPremultiply)); + AssertBulkToMatchesScalar(pixels, static pixel => pixel.ToAssociatedVector4(), static (operations, source, destination) => operations.ToVector4(Configuration.Default, source, destination, PixelConversionModifiers.Premultiply)); + AssertBulkToMatchesScalar(pixels, static pixel => pixel.ToUnassociatedScaledVector4(), static (operations, source, destination) => operations.ToVector4(Configuration.Default, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply)); + AssertBulkToMatchesScalar(pixels, static pixel => pixel.ToAssociatedScaledVector4(), static (operations, source, destination) => operations.ToVector4(Configuration.Default, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + + AssertBulkFromMatchesScalar(pixels, static pixel => pixel.ToUnassociatedVector4(), static vector => TPixel.FromUnassociatedVector4(vector), static (operations, source, destination) => operations.FromVector4Destructive(Configuration.Default, source, destination, PixelConversionModifiers.UnPremultiply)); + AssertBulkFromMatchesScalar(pixels, static pixel => pixel.ToAssociatedVector4(), static vector => TPixel.FromAssociatedVector4(vector), static (operations, source, destination) => operations.FromVector4Destructive(Configuration.Default, source, destination, PixelConversionModifiers.Premultiply)); + AssertBulkFromMatchesScalar(pixels, static pixel => pixel.ToUnassociatedScaledVector4(), static vector => TPixel.FromUnassociatedScaledVector4(vector), static (operations, source, destination) => operations.FromVector4Destructive(Configuration.Default, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply)); + AssertBulkFromMatchesScalar(pixels, static pixel => pixel.ToAssociatedScaledVector4(), static vector => TPixel.FromAssociatedScaledVector4(vector), static (operations, source, destination) => operations.FromVector4Destructive(Configuration.Default, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + } + + [Fact] + public void BulkScaledConversionsClampLikeScalarConversions() + { + Vector4[] associated = new Vector4[OutOfRangeUnassociatedScaledVectors.Length]; + + for (int i = 0; i < associated.Length; i++) + { + associated[i] = Associate(OutOfRangeUnassociatedScaledVectors[i]); + } + + AssertBulkFromMatchesScalar(OutOfRangeUnassociatedScaledVectors, static vector => TPixel.FromUnassociatedScaledVector4(vector), static (operations, source, destination) => operations.FromVector4Destructive(Configuration.Default, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply)); + AssertBulkFromMatchesScalar(associated, static vector => TPixel.FromAssociatedScaledVector4(vector), static (operations, source, destination) => operations.FromVector4Destructive(Configuration.Default, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply)); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void BulkCompandingHonorsRequestedAlphaRepresentationAndRange(bool scaled, bool associated) + { + const int length = 259; + TPixel[] pixels = new TPixel[length]; + + for (int i = 0; i < pixels.Length; i++) + { + pixels[i] = TPixel.FromUnassociatedScaledVector4(UnassociatedScaledVectors[i % UnassociatedScaledVectors.Length]); + } + + PixelConversionModifiers modifiers = PixelConversionModifiers.SRgbCompand + | (scaled ? PixelConversionModifiers.Scale : PixelConversionModifiers.None) + | (associated ? PixelConversionModifiers.Premultiply : PixelConversionModifiers.UnPremultiply); + + Vector4[] expectedVectors = new Vector4[length]; + + for (int i = 0; i < pixels.Length; i++) + { + expectedVectors[i] = scaled ? pixels[i].ToUnassociatedScaledVector4() : pixels[i].ToUnassociatedVector4(); + } + + // Transfer functions operate on straight RGB. Association is therefore applied after expansion on the outbound path. + SRgbCompanding.Expand(expectedVectors); + + if (associated) + { + Numerics.Premultiply(expectedVectors); + } + + Vector4[] actualVectors = new Vector4[length]; + PixelOperations.Instance.ToVector4(Configuration.Default, pixels, actualVectors, modifiers); + + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] expectedPixelSource = [.. expectedVectors]; + + if (associated) + { + Numerics.UnPremultiply(expectedPixelSource); + } + + // Reverse the transfer only after restoring straight RGB, matching the observable modifier order for inbound vectors. + SRgbCompanding.Compress(expectedPixelSource); + + TPixel[] expectedPixels = new TPixel[length]; + + for (int i = 0; i < expectedPixels.Length; i++) + { + expectedPixels[i] = scaled + ? TPixel.FromUnassociatedScaledVector4(expectedPixelSource[i]) + : TPixel.FromUnassociatedVector4(expectedPixelSource[i]); + } + + Vector4[] actualPixelSource = [.. expectedVectors]; + TPixel[] actualPixels = new TPixel[length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, actualPixelSource, actualPixels, modifiers); + + Assert.Equal(expectedPixels, actualPixels); + } + + private static void AssertBulkToMatchesScalar(TPixel[] pixels, Func scalar, BulkToVector4 bulk) + { + Vector4[] expected = new Vector4[pixels.Length]; + Vector4[] actual = new Vector4[pixels.Length + 3]; + Vector4 sentinel = new(.125F, .25F, .5F, .75F); + + for (int i = 0; i < pixels.Length; i++) + { + expected[i] = scalar(pixels[i]); + } + + actual.AsSpan(pixels.Length).Fill(sentinel); + bulk(PixelOperations.Instance, pixels, actual); + + // Bulk conversion is source-length driven and must leave excess destination capacity untouched. + Assert.Equal(expected, actual[..pixels.Length]); + Assert.All(actual[pixels.Length..], vector => Assert.Equal(sentinel, vector)); + } + + private static void AssertBulkFromMatchesScalar(TPixel[] pixels, Func createSource, Func scalar, BulkFromVector4 bulk) + { + Vector4[] source = new Vector4[pixels.Length]; + + for (int i = 0; i < pixels.Length; i++) + { + source[i] = createSource(pixels[i]); + } + + AssertBulkFromMatchesScalar(source, scalar, bulk); + } + + private static void AssertBulkFromMatchesScalar(Vector4[] source, Func scalar, BulkFromVector4 bulk) + { + TPixel[] expected = new TPixel[source.Length]; + TPixel[] actual = new TPixel[source.Length + 3]; + TPixel sentinel = TPixel.FromUnassociatedScaledVector4(new Vector4(.125F, .25F, .5F, .75F)); + + for (int i = 0; i < source.Length; i++) + { + expected[i] = scalar(source[i]); + } + + // From-vector bulk operations are destructive, so preserve the scalar source for diagnostics and future assertions. + Vector4[] destructiveSource = [.. source]; + actual.AsSpan(source.Length).Fill(sentinel); + bulk(PixelOperations.Instance, destructiveSource, actual); + + // A destination may expose spare capacity; the source span still defines how many pixels are written. + Assert.Equal(expected, actual[..source.Length]); + Assert.All(actual[source.Length..], pixel => Assert.Equal(sentinel, pixel)); + } + + private static Vector4 Associate(Vector4 vector) + { + vector.X *= vector.W; + vector.Y *= vector.W; + vector.Z *= vector.W; + return vector; + } + + private delegate void BulkToVector4(PixelOperations operations, ReadOnlySpan source, Span destination); + + private delegate void BulkFromVector4(PixelOperations operations, Span source, Span destination); +} + +/// +/// Verifies the shared bulk vector conversion used by RGB byte formats. +/// +[Trait("Category", "PixelFormats")] +public class RgbaCompatiblePixelOperationsTests +{ + [Fact] + public void BulkConversionsMatchScalarAcrossHardwareWidths() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertBulkConversionsMatchScalar, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void AssertBulkConversionsMatchScalar() + { + AssertBulkConversionsMatchScalarForPixel(); + AssertBulkConversionsMatchScalarForPixel(); + } + + private static void AssertBulkConversionsMatchScalarForPixel() + where TPixel : unmanaged, IPixel + { + const int length = 259; + Vector4 sourceVector = new(.5F, .25F, .125F, 1F); + TPixel expectedPixel = TPixel.FromUnassociatedScaledVector4(sourceVector); + TPixel[] pixels = new TPixel[length]; + Array.Fill(pixels, expectedPixel); + + Vector4 expectedVector = expectedPixel.ToUnassociatedScaledVector4(); + Vector4 vectorSentinel = new(.125F, .5F, .75F, 1F); + Vector4[] vectors = new Vector4[length + 3]; + vectors.AsSpan(length).Fill(vectorSentinel); + + PixelOperations.Instance.ToVector4(Configuration.Default, pixels, vectors, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + Assert.All(vectors[..length], vector => Assert.Equal(expectedVector, vector)); + Assert.All(vectors[length..], vector => Assert.Equal(vectorSentinel, vector)); + + Vector4[] source = new Vector4[length]; + source.AsSpan().Fill(sourceVector); + + TPixel pixelSentinel = TPixel.FromUnassociatedScaledVector4(vectorSentinel); + TPixel[] destination = new TPixel[length + 3]; + destination.AsSpan(length).Fill(pixelSentinel); + + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + // A 259-pixel source crosses the 128- and 256-bit optimized thresholds while leaving spare destination capacity to detect writes beyond the source length. + Assert.All(destination[..length], pixel => Assert.Equal(expectedPixel, pixel)); + Assert.All(destination[length..], pixel => Assert.Equal(pixelSentinel, pixel)); + } +} + +/// +/// Verifies alpha conversion for formats with distinct native vector contracts. +/// +[Trait("Category", "PixelFormats")] +public class AffineNativeAlphaRepresentationTests +{ + private static readonly Vector4 UnassociatedScaled = new(.8F, .4F, .2F, .5F); + + [Fact] + public void Byte4NativeConversionsUseScaledAlpha() + => AssertNativeConversions(static vector => vector * byte.MaxValue); + + [Fact] + public void Short4NativeConversionsUseScaledAlpha() + => AssertNativeConversions(static vector => (vector * ushort.MaxValue) - new Vector4(32768F)); + + [Fact] + public void NormalizedByte4NativeConversionsUseScaledAlpha() + => AssertSignedNormalizedNativeConversions(static vector => (vector * 2F) - Vector4.One); + + [Fact] + public void NormalizedByte4PNativeConversionsUseScaledAlpha() + => AssertSignedNormalizedNativeConversions(static vector => (vector * 2F) - Vector4.One); + + [Fact] + public void NormalizedShort4NativeConversionsUseScaledAlpha() + => AssertSignedNormalizedNativeConversions(static vector => (vector * 2F) - Vector4.One); + + [Fact] + public void HalfVector4NativeConversionsUseScaledAlpha() + => AssertNativeConversions(static vector => (vector * 131008F) - new Vector4(65504F)); + + [Fact] + public void HalfVector4PNativeConversionsUseScaledAlpha() + => AssertNativeConversions(static vector => (vector * 131008F) - new Vector4(65504F)); + + [Fact] + public void HalfSingleFromAssociatedNativeVectorUsesScaledAlpha() + => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 131008F) - 65504F, 0F, 0F, vector.W)); + + [Fact] + public void HalfVector2FromAssociatedNativeVectorUsesScaledAlpha() + => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 131008F) - 65504F, (vector.Y * 131008F) - 65504F, 0F, vector.W)); + + [Fact] + public void NormalizedByte2FromAssociatedNativeVectorUsesScaledAlpha() + => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 2F) - 1F, (vector.Y * 2F) - 1F, 0F, vector.W)); + + [Fact] + public void NormalizedShort2FromAssociatedNativeVectorUsesScaledAlpha() + => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * 2F) - 1F, (vector.Y * 2F) - 1F, 0F, vector.W)); + + [Fact] + public void Short2FromAssociatedNativeVectorUsesScaledAlpha() + => AssertAlphaLessNativeFrom(static vector => new Vector4((vector.X * ushort.MaxValue) - 32768F, (vector.Y * ushort.MaxValue) - 32768F, 0F, vector.W)); + + private static void AssertNativeConversions(Func encodeNative) + where TPixel : unmanaged, IPixel + { + TPixel pixel = TPixel.FromUnassociatedScaledVector4(UnassociatedScaled); + Vector4 unassociatedScaled = pixel.ToUnassociatedScaledVector4(); + Vector4 associatedScaled = Associate(unassociatedScaled); + + // Native alpha can be signed or use an integer component range, so multiplying native RGB by native W would not represent opacity. + AlphaRepresentationTestAssertions.EqualWithinOneUlp(encodeNative(unassociatedScaled), pixel.ToUnassociatedVector4()); + AlphaRepresentationTestAssertions.EqualWithinOneUlp(encodeNative(associatedScaled), pixel.ToAssociatedVector4()); + Assert.Equal(pixel, TPixel.FromUnassociatedVector4(encodeNative(unassociatedScaled))); + Assert.Equal(pixel, TPixel.FromAssociatedVector4(encodeNative(associatedScaled))); + } + + private static void AssertSignedNormalizedNativeConversions(Func encodeNative) + where TPixel : unmanaged, IPixel + { + TPixel pixel = TPixel.FromUnassociatedScaledVector4(UnassociatedScaled); + Vector4 unassociatedScaled = pixel.ToUnassociatedScaledVector4(); + Vector4 associatedScaled = Associate(unassociatedScaled); + + // The 2*x-1 affine map is ill-conditioned around native zero, so a native-space ULP tolerance would not measure conversion accuracy. + // Verify the stored native representation exactly, then verify both inverse paths using independently encoded scaled vectors. + if (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + Assert.Equal(pixel.ToVector4(), pixel.ToAssociatedVector4()); + } + else + { + Assert.Equal(pixel.ToVector4(), pixel.ToUnassociatedVector4()); + } + + Assert.Equal(pixel, TPixel.FromUnassociatedVector4(encodeNative(unassociatedScaled))); + Assert.Equal(pixel, TPixel.FromAssociatedVector4(encodeNative(associatedScaled))); + } + + private static void AssertAlphaLessNativeFrom(Func encodeNative) + where TPixel : unmanaged, IPixel + { + TPixel expected = TPixel.FromUnassociatedScaledVector4(UnassociatedScaled); + Vector4 associatedScaled = Associate(UnassociatedScaled); + + // Formats with implicit alpha still need to unassociate an associated source before discarding its W component. + Assert.Equal(expected, TPixel.FromAssociatedVector4(encodeNative(associatedScaled))); + } + + private static Vector4 Associate(Vector4 vector) + { + vector.X *= vector.W; + vector.Y *= vector.W; + vector.Z *= vector.W; + return vector; + } +} + +/// +/// Provides exact floating-point bounds for independently ordered alpha-association calculations. +/// +internal static class AlphaRepresentationTestAssertions +{ + /// + /// Verifies that corresponding components differ by no more than one binary32 value. + /// + /// The independently calculated expected vector. + /// The vector produced by the pixel implementation. + internal static void EqualWithinOneUlp(Vector4 expected, Vector4 actual) + { + // One neighboring value is the strict bound for a single independently ordered, correctly rounded binary32 operation. + Assert.InRange(actual.X, MathF.BitDecrement(expected.X), MathF.BitIncrement(expected.X)); + Assert.InRange(actual.Y, MathF.BitDecrement(expected.Y), MathF.BitIncrement(expected.Y)); + Assert.InRange(actual.Z, MathF.BitDecrement(expected.Z), MathF.BitIncrement(expected.Z)); + Assert.InRange(actual.W, MathF.BitDecrement(expected.W), MathF.BitIncrement(expected.W)); + } + + /// + /// Verifies that corresponding components differ by no more than two binary32 values. + /// + /// The independently calculated expected vector. + /// The vector produced by the pixel implementation. + internal static void EqualWithinTwoUlps(Vector4 expected, Vector4 actual) + { + // Two neighboring values are the exhaustive bound for NormalizedByte4P unassociation followed by reassociation. + // Bit increments express that exact bound without introducing a loose decimal tolerance. + Assert.InRange(actual.X, MathF.BitDecrement(MathF.BitDecrement(expected.X)), MathF.BitIncrement(MathF.BitIncrement(expected.X))); + Assert.InRange(actual.Y, MathF.BitDecrement(MathF.BitDecrement(expected.Y)), MathF.BitIncrement(MathF.BitIncrement(expected.Y))); + Assert.InRange(actual.Z, MathF.BitDecrement(MathF.BitDecrement(expected.Z)), MathF.BitIncrement(MathF.BitIncrement(expected.Z))); + Assert.InRange(actual.W, MathF.BitDecrement(MathF.BitDecrement(expected.W)), MathF.BitIncrement(MathF.BitIncrement(expected.W))); + } +} + +public class A8AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Abgr32AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Abgr32PAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Argb32AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Argb32PAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Bgr24AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Bgr565AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Bgra32AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Bgra32PAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Bgra4444AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Bgra5551AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Byte4AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class HalfSingleAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class HalfVector2AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class HalfVector4AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class HalfVector4PAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class L16AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class L8AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class La16AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class La32AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class NormalizedByte2AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class NormalizedByte4AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class NormalizedByte4PAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class NormalizedShort2AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class NormalizedShort4AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rg32AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rgb24AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rgb48AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rgb96AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rgba1010102AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rgba128AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rgba32AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rgba32PAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class RgbaHalfAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class RgbaHalfPAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Rgba64AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class RgbaVectorAlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Short2AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class Short4AlphaRepresentationTests : PixelAlphaRepresentationTests { } +public class RgbaDoubleAlphaRepresentationTests : PixelAlphaRepresentationTests { } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs index 924e94d92..c3028b33b 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats.PixelBlenders; using SixLabors.ImageSharp.Tests.TestUtilities; @@ -12,26 +13,192 @@ public class PixelBlenderTests { public static TheoryData BlenderMappings = new() { - { new TestPixel(), typeof(DefaultPixelBlenders.NormalSrcOver), PixelColorBlendingMode.Normal }, - { new TestPixel(), typeof(DefaultPixelBlenders.ScreenSrcOver), PixelColorBlendingMode.Screen }, - { new TestPixel(), typeof(DefaultPixelBlenders.HardLightSrcOver), PixelColorBlendingMode.HardLight }, - { new TestPixel(), typeof(DefaultPixelBlenders.OverlaySrcOver), PixelColorBlendingMode.Overlay }, - { new TestPixel(), typeof(DefaultPixelBlenders.DarkenSrcOver), PixelColorBlendingMode.Darken }, - { new TestPixel(), typeof(DefaultPixelBlenders.LightenSrcOver), PixelColorBlendingMode.Lighten }, - { new TestPixel(), typeof(DefaultPixelBlenders.AddSrcOver), PixelColorBlendingMode.Add }, - { new TestPixel(), typeof(DefaultPixelBlenders.SubtractSrcOver), PixelColorBlendingMode.Subtract }, - { new TestPixel(), typeof(DefaultPixelBlenders.MultiplySrcOver), PixelColorBlendingMode.Multiply }, - { new TestPixel(), typeof(DefaultPixelBlenders.NormalSrcOver), PixelColorBlendingMode.Normal }, - { new TestPixel(), typeof(DefaultPixelBlenders.ScreenSrcOver), PixelColorBlendingMode.Screen }, - { new TestPixel(), typeof(DefaultPixelBlenders.HardLightSrcOver), PixelColorBlendingMode.HardLight }, - { new TestPixel(), typeof(DefaultPixelBlenders.OverlaySrcOver), PixelColorBlendingMode.Overlay }, - { new TestPixel(), typeof(DefaultPixelBlenders.DarkenSrcOver), PixelColorBlendingMode.Darken }, - { new TestPixel(), typeof(DefaultPixelBlenders.LightenSrcOver), PixelColorBlendingMode.Lighten }, - { new TestPixel(), typeof(DefaultPixelBlenders.AddSrcOver), PixelColorBlendingMode.Add }, - { new TestPixel(), typeof(DefaultPixelBlenders.SubtractSrcOver), PixelColorBlendingMode.Subtract }, - { new TestPixel(), typeof(DefaultPixelBlenders.MultiplySrcOver), PixelColorBlendingMode.Multiply }, + { new TestPixel(), DefaultPixelBlenders.NormalSrcOver.GetType(), PixelColorBlendingMode.Normal }, + { new TestPixel(), DefaultPixelBlenders.ScreenSrcOver.GetType(), PixelColorBlendingMode.Screen }, + { new TestPixel(), DefaultPixelBlenders.HardLightSrcOver.GetType(), PixelColorBlendingMode.HardLight }, + { new TestPixel(), DefaultPixelBlenders.OverlaySrcOver.GetType(), PixelColorBlendingMode.Overlay }, + { new TestPixel(), DefaultPixelBlenders.DarkenSrcOver.GetType(), PixelColorBlendingMode.Darken }, + { new TestPixel(), DefaultPixelBlenders.LightenSrcOver.GetType(), PixelColorBlendingMode.Lighten }, + { new TestPixel(), DefaultPixelBlenders.AddSrcOver.GetType(), PixelColorBlendingMode.Add }, + { new TestPixel(), DefaultPixelBlenders.SubtractSrcOver.GetType(), PixelColorBlendingMode.Subtract }, + { new TestPixel(), DefaultPixelBlenders.MultiplySrcOver.GetType(), PixelColorBlendingMode.Multiply }, + { new TestPixel(), DefaultPixelBlenders.NormalSrcOver.GetType(), PixelColorBlendingMode.Normal }, + { new TestPixel(), DefaultPixelBlenders.ScreenSrcOver.GetType(), PixelColorBlendingMode.Screen }, + { new TestPixel(), DefaultPixelBlenders.HardLightSrcOver.GetType(), PixelColorBlendingMode.HardLight }, + { new TestPixel(), DefaultPixelBlenders.OverlaySrcOver.GetType(), PixelColorBlendingMode.Overlay }, + { new TestPixel(), DefaultPixelBlenders.DarkenSrcOver.GetType(), PixelColorBlendingMode.Darken }, + { new TestPixel(), DefaultPixelBlenders.LightenSrcOver.GetType(), PixelColorBlendingMode.Lighten }, + { new TestPixel(), DefaultPixelBlenders.AddSrcOver.GetType(), PixelColorBlendingMode.Add }, + { new TestPixel(), DefaultPixelBlenders.SubtractSrcOver.GetType(), PixelColorBlendingMode.Subtract }, + { new TestPixel(), DefaultPixelBlenders.MultiplySrcOver.GetType(), PixelColorBlendingMode.Multiply }, }; + public static TheoryData BlenderModeMappings + { + get + { + TheoryData data = new(); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.Clear, + DefaultPixelBlenders.NormalClear.GetType(), + DefaultPixelBlenders.MultiplyClear.GetType(), + DefaultPixelBlenders.AddClear.GetType(), + DefaultPixelBlenders.SubtractClear.GetType(), + DefaultPixelBlenders.ScreenClear.GetType(), + DefaultPixelBlenders.DarkenClear.GetType(), + DefaultPixelBlenders.LightenClear.GetType(), + DefaultPixelBlenders.OverlayClear.GetType(), + DefaultPixelBlenders.HardLightClear.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.Xor, + DefaultPixelBlenders.NormalXor.GetType(), + DefaultPixelBlenders.MultiplyXor.GetType(), + DefaultPixelBlenders.AddXor.GetType(), + DefaultPixelBlenders.SubtractXor.GetType(), + DefaultPixelBlenders.ScreenXor.GetType(), + DefaultPixelBlenders.DarkenXor.GetType(), + DefaultPixelBlenders.LightenXor.GetType(), + DefaultPixelBlenders.OverlayXor.GetType(), + DefaultPixelBlenders.HardLightXor.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.Src, + DefaultPixelBlenders.NormalSrc.GetType(), + DefaultPixelBlenders.MultiplySrc.GetType(), + DefaultPixelBlenders.AddSrc.GetType(), + DefaultPixelBlenders.SubtractSrc.GetType(), + DefaultPixelBlenders.ScreenSrc.GetType(), + DefaultPixelBlenders.DarkenSrc.GetType(), + DefaultPixelBlenders.LightenSrc.GetType(), + DefaultPixelBlenders.OverlaySrc.GetType(), + DefaultPixelBlenders.HardLightSrc.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.SrcAtop, + DefaultPixelBlenders.NormalSrcAtop.GetType(), + DefaultPixelBlenders.MultiplySrcAtop.GetType(), + DefaultPixelBlenders.AddSrcAtop.GetType(), + DefaultPixelBlenders.SubtractSrcAtop.GetType(), + DefaultPixelBlenders.ScreenSrcAtop.GetType(), + DefaultPixelBlenders.DarkenSrcAtop.GetType(), + DefaultPixelBlenders.LightenSrcAtop.GetType(), + DefaultPixelBlenders.OverlaySrcAtop.GetType(), + DefaultPixelBlenders.HardLightSrcAtop.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.SrcIn, + DefaultPixelBlenders.NormalSrcIn.GetType(), + DefaultPixelBlenders.MultiplySrcIn.GetType(), + DefaultPixelBlenders.AddSrcIn.GetType(), + DefaultPixelBlenders.SubtractSrcIn.GetType(), + DefaultPixelBlenders.ScreenSrcIn.GetType(), + DefaultPixelBlenders.DarkenSrcIn.GetType(), + DefaultPixelBlenders.LightenSrcIn.GetType(), + DefaultPixelBlenders.OverlaySrcIn.GetType(), + DefaultPixelBlenders.HardLightSrcIn.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.SrcOut, + DefaultPixelBlenders.NormalSrcOut.GetType(), + DefaultPixelBlenders.MultiplySrcOut.GetType(), + DefaultPixelBlenders.AddSrcOut.GetType(), + DefaultPixelBlenders.SubtractSrcOut.GetType(), + DefaultPixelBlenders.ScreenSrcOut.GetType(), + DefaultPixelBlenders.DarkenSrcOut.GetType(), + DefaultPixelBlenders.LightenSrcOut.GetType(), + DefaultPixelBlenders.OverlaySrcOut.GetType(), + DefaultPixelBlenders.HardLightSrcOut.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.Dest, + DefaultPixelBlenders.NormalDest.GetType(), + DefaultPixelBlenders.MultiplyDest.GetType(), + DefaultPixelBlenders.AddDest.GetType(), + DefaultPixelBlenders.SubtractDest.GetType(), + DefaultPixelBlenders.ScreenDest.GetType(), + DefaultPixelBlenders.DarkenDest.GetType(), + DefaultPixelBlenders.LightenDest.GetType(), + DefaultPixelBlenders.OverlayDest.GetType(), + DefaultPixelBlenders.HardLightDest.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.DestAtop, + DefaultPixelBlenders.NormalDestAtop.GetType(), + DefaultPixelBlenders.MultiplyDestAtop.GetType(), + DefaultPixelBlenders.AddDestAtop.GetType(), + DefaultPixelBlenders.SubtractDestAtop.GetType(), + DefaultPixelBlenders.ScreenDestAtop.GetType(), + DefaultPixelBlenders.DarkenDestAtop.GetType(), + DefaultPixelBlenders.LightenDestAtop.GetType(), + DefaultPixelBlenders.OverlayDestAtop.GetType(), + DefaultPixelBlenders.HardLightDestAtop.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.DestIn, + DefaultPixelBlenders.NormalDestIn.GetType(), + DefaultPixelBlenders.MultiplyDestIn.GetType(), + DefaultPixelBlenders.AddDestIn.GetType(), + DefaultPixelBlenders.SubtractDestIn.GetType(), + DefaultPixelBlenders.ScreenDestIn.GetType(), + DefaultPixelBlenders.DarkenDestIn.GetType(), + DefaultPixelBlenders.LightenDestIn.GetType(), + DefaultPixelBlenders.OverlayDestIn.GetType(), + DefaultPixelBlenders.HardLightDestIn.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.DestOut, + DefaultPixelBlenders.NormalDestOut.GetType(), + DefaultPixelBlenders.MultiplyDestOut.GetType(), + DefaultPixelBlenders.AddDestOut.GetType(), + DefaultPixelBlenders.SubtractDestOut.GetType(), + DefaultPixelBlenders.ScreenDestOut.GetType(), + DefaultPixelBlenders.DarkenDestOut.GetType(), + DefaultPixelBlenders.LightenDestOut.GetType(), + DefaultPixelBlenders.OverlayDestOut.GetType(), + DefaultPixelBlenders.HardLightDestOut.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.DestOver, + DefaultPixelBlenders.NormalDestOver.GetType(), + DefaultPixelBlenders.MultiplyDestOver.GetType(), + DefaultPixelBlenders.AddDestOver.GetType(), + DefaultPixelBlenders.SubtractDestOver.GetType(), + DefaultPixelBlenders.ScreenDestOver.GetType(), + DefaultPixelBlenders.DarkenDestOver.GetType(), + DefaultPixelBlenders.LightenDestOver.GetType(), + DefaultPixelBlenders.OverlayDestOver.GetType(), + DefaultPixelBlenders.HardLightDestOver.GetType()); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.SrcOver, + DefaultPixelBlenders.NormalSrcOver.GetType(), + DefaultPixelBlenders.MultiplySrcOver.GetType(), + DefaultPixelBlenders.AddSrcOver.GetType(), + DefaultPixelBlenders.SubtractSrcOver.GetType(), + DefaultPixelBlenders.ScreenSrcOver.GetType(), + DefaultPixelBlenders.DarkenSrcOver.GetType(), + DefaultPixelBlenders.LightenSrcOver.GetType(), + DefaultPixelBlenders.OverlaySrcOver.GetType(), + DefaultPixelBlenders.HardLightSrcOver.GetType()); + + return data; + } + } + [Theory] [MemberData(nameof(BlenderMappings))] public void ReturnsCorrectBlender(TestPixel pixel, Type type, PixelColorBlendingMode mode) @@ -41,6 +208,355 @@ public class PixelBlenderTests Assert.IsType(type, blender); } + [Theory] + [MemberData(nameof(BlenderModeMappings))] + public void ReturnsCorrectBlenderForAllModeCombinations(PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode, Type type) + { + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); + Assert.IsType(type, blender); + } + + [Fact] + public void BlendFunctionsAreCalledForAllModeCombinations() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + ExerciseAllBlenderModeCombinations, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void AssociatedAlphaBlendFunctionsAreCalledForAllModeCombinations() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + ExerciseAllAssociatedAlphaBlenderModeCombinations, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void AssociatedHardLightDestAtopRoundsExactMidpointAwayFromZero() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunAssociatedHardLightDestAtopRoundsExactMidpointAwayFromZero, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + private static void RunAssociatedHardLightDestAtopRoundsExactMidpointAwayFromZero() + { + Rgba32P background = Rgba32P.FromRgba32(new Rgba32(220, 80, 40, 160)); + Rgba32P source = Rgba32P.FromRgba32(new Rgba32(20, 180, 120, 96)); + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.HardLight, PixelAlphaCompositionMode.DestAtop); + + Assert.Equal(new Rgba32P(30, 33, 16, 60), blender.Blend(background, source, .625F)); + } + + [Fact] + public void Blend_WithConstantSourceAndSingleAmount() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32 source = Color.Blue.ToPixel(); + + blender.Blend(Configuration.Default, destination, background, source, 1F); + + Assert.Equal(source, destination[0]); + Assert.Equal(source, destination[1]); + } + + [Fact] + public void Blend_WithConstantSourceSingleAmountAndWorkingBuffer() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32 source = Color.Blue.ToPixel(); + Vector4[] workingBuffer = new Vector4[destination.Length * 2]; + + blender.Blend(Configuration.Default, destination, background, source, 1F, workingBuffer); + + Assert.Equal(source, destination[0]); + Assert.Equal(source, destination[1]); + } + + [Fact] + public void Blend_WithConstantSourceAndAmountSpan() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32 source = Color.Blue.ToPixel(); + float[] amount = [1F, 1F]; + + blender.Blend(Configuration.Default, destination, background, source, amount); + + Assert.Equal(source, destination[0]); + Assert.Equal(source, destination[1]); + } + + [Fact] + public void Blend_WithConstantSourceAmountSpanAndWorkingBuffer() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32 source = Color.Blue.ToPixel(); + float[] amount = [1F, 1F]; + Vector4[] workingBuffer = new Vector4[destination.Length * 2]; + + blender.Blend(Configuration.Default, destination, background, source, amount, workingBuffer); + + Assert.Equal(source, destination[0]); + Assert.Equal(source, destination[1]); + } + + [Fact] + public void Blend_WithSourceSpanAmountSpanAndWorkingBuffer() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32[] source = + [ + Color.Blue.ToPixel(), + Color.Yellow.ToPixel() + ]; + + float[] amount = [1F, 1F]; + Vector4[] workingBuffer = new Vector4[destination.Length * 3]; + + blender.Blend(Configuration.Default, destination, background, source, amount, workingBuffer); + + Assert.Equal(source[0], destination[0]); + Assert.Equal(source[1], destination[1]); + } + + [Fact] + public void BlendWithCoverage_WithConstantSourceAndSingleAmount() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrc; + Rgba32[] destination = new Rgba32[3]; + Rgba32[] background = + [ + new(255, 0, 0), + new(255, 0, 0), + new(255, 0, 0) + ]; + + Rgba32 source = new(0, 0, 255); + float[] coverage = [0F, .5F, 1F]; + + blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage); + + Assert.Equal(background[0], destination[0]); + Assert.Equal(new Rgba32(128, 0, 128), destination[1]); + Assert.Equal(source, destination[2]); + } + + [Fact] + public void BlendWithCoverage_WithConstantSourceSingleAmountAndWorkingBuffer() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrc; + Rgba32[] destination = new Rgba32[3]; + Rgba32[] background = + [ + new(255, 0, 0), + new(255, 0, 0), + new(255, 0, 0) + ]; + + Rgba32 source = new(0, 0, 255); + float[] coverage = [0F, .5F, 1F]; + Vector4[] workingBuffer = new Vector4[destination.Length * 2]; + + blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage, workingBuffer); + + Assert.Equal(background[0], destination[0]); + Assert.Equal(new Rgba32(128, 0, 128), destination[1]); + Assert.Equal(source, destination[2]); + } + + [Fact] + public void BlendWithCoverage_WithSourceSpanAndSingleAmount() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrc; + Rgba32[] destination = new Rgba32[3]; + Rgba32[] background = + [ + new(255, 0, 0), + new(0, 255, 0), + new(0, 0, 255) + ]; + + Rgba32[] source = + [ + new(0, 0, 255), + new(255, 0, 0), + new(0, 255, 0) + ]; + + float[] coverage = [0F, .5F, 1F]; + + blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage); + + Assert.Equal(background[0], destination[0]); + Assert.Equal(new Rgba32(128, 128, 0), destination[1]); + Assert.Equal(source[2], destination[2]); + } + + [Fact] + public void BlendWithCoverage_WithSourceSpanSingleAmountAndWorkingBuffer() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrc; + Rgba32[] destination = new Rgba32[3]; + Rgba32[] background = + [ + new(255, 0, 0), + new(0, 255, 0), + new(0, 0, 255) + ]; + + Rgba32[] source = + [ + new(0, 0, 255), + new(255, 0, 0), + new(0, 255, 0) + ]; + + float[] coverage = [0F, .5F, 1F]; + Vector4[] workingBuffer = new Vector4[destination.Length * 3]; + + blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage, workingBuffer); + + Assert.Equal(background[0], destination[0]); + Assert.Equal(new Rgba32(128, 128, 0), destination[1]); + Assert.Equal(source[2], destination[2]); + } + + [Fact] + public void BlendWithCoverage_WithConstantSourceAndAmountSpan() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrc; + Rgba32[] destination = new Rgba32[3]; + Rgba32[] background = + [ + new(255, 0, 0), + new(255, 0, 0), + new(255, 0, 0) + ]; + + Rgba32 source = new(0, 0, 255); + float[] amount = [1F, 1F, 1F]; + float[] coverage = [0F, .5F, 1F]; + + blender.BlendWithCoverage(Configuration.Default, destination, background, source, amount, coverage); + + Assert.Equal(background[0], destination[0]); + Assert.Equal(new Rgba32(128, 0, 128), destination[1]); + Assert.Equal(source, destination[2]); + } + + [Fact] + public void BlendWithCoverage_WithConstantSourceAmountSpanAndWorkingBuffer() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrc; + Rgba32[] destination = new Rgba32[3]; + Rgba32[] background = + [ + new(255, 0, 0), + new(255, 0, 0), + new(255, 0, 0) + ]; + + Rgba32 source = new(0, 0, 255); + float[] amount = [1F, 1F, 1F]; + float[] coverage = [0F, .5F, 1F]; + Vector4[] workingBuffer = new Vector4[destination.Length * 2]; + + blender.BlendWithCoverage(Configuration.Default, destination, background, source, amount, coverage, workingBuffer); + + Assert.Equal(background[0], destination[0]); + Assert.Equal(new Rgba32(128, 0, 128), destination[1]); + Assert.Equal(source, destination[2]); + } + + [Fact] + public void BlendWithCoverage_WithSourceSpanAndAmountSpan() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrc; + Rgba32[] destination = new Rgba32[3]; + Rgba32[] background = + [ + new(255, 0, 0), + new(0, 255, 0), + new(0, 0, 255) + ]; + + Rgba32[] source = + [ + new(0, 0, 255), + new(255, 0, 0), + new(0, 255, 0) + ]; + + float[] amount = [1F, 1F, 1F]; + float[] coverage = [0F, .5F, 1F]; + + blender.BlendWithCoverage(Configuration.Default, destination, background, source, amount, coverage); + + Assert.Equal(background[0], destination[0]); + Assert.Equal(new Rgba32(128, 128, 0), destination[1]); + Assert.Equal(source[2], destination[2]); + } + + [Fact] + public void BlendWithCoverage_WithSourceSpanAmountSpanAndWorkingBuffer() + { + PixelBlender blender = DefaultPixelBlenders.NormalSrc; + Rgba32[] destination = new Rgba32[3]; + Rgba32[] background = + [ + new(255, 0, 0), + new(0, 255, 0), + new(0, 0, 255) + ]; + + Rgba32[] source = + [ + new(0, 0, 255), + new(255, 0, 0), + new(0, 255, 0) + ]; + + float[] amount = [1F, 1F, 1F]; + float[] coverage = [0F, .5F, 1F]; + Vector4[] workingBuffer = new Vector4[destination.Length * 3]; + + blender.BlendWithCoverage(Configuration.Default, destination, background, source, amount, coverage, workingBuffer); + + Assert.Equal(background[0], destination[0]); + Assert.Equal(new Rgba32(128, 128, 0), destination[1]); + Assert.Equal(source[2], destination[2]); + } + public static TheoryData ColorBlendingExpectedResults = new() { { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Normal, Color.MidnightBlue.ToPixel() }, @@ -92,4 +608,275 @@ public class PixelBlenderTests // var str = actualResult.Rgba.ToString("X8"); // used to extract expectedResults Assert.Equal(actualResult.ToVector4(), expectedResult.ToVector4()); } + + private static void AddBlenderModeMappings( + TheoryData data, + PixelAlphaCompositionMode alphaMode, + Type normal, + Type multiply, + Type add, + Type subtract, + Type screen, + Type darken, + Type lighten, + Type overlay, + Type hardLight) + { + data.Add(PixelColorBlendingMode.Normal, alphaMode, normal); + data.Add(PixelColorBlendingMode.Multiply, alphaMode, multiply); + data.Add(PixelColorBlendingMode.Add, alphaMode, add); + data.Add(PixelColorBlendingMode.Subtract, alphaMode, subtract); + data.Add(PixelColorBlendingMode.Screen, alphaMode, screen); + data.Add(PixelColorBlendingMode.Darken, alphaMode, darken); + data.Add(PixelColorBlendingMode.Lighten, alphaMode, lighten); + data.Add(PixelColorBlendingMode.Overlay, alphaMode, overlay); + data.Add(PixelColorBlendingMode.HardLight, alphaMode, hardLight); + } + + private static void ExerciseAllBlenderModeCombinations() + { + foreach (PixelAlphaCompositionMode alphaMode in Enum.GetValues()) + { + foreach (PixelColorBlendingMode colorMode in Enum.GetValues()) + { + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); + ExerciseBlender(blender); + } + } + } + + private static void ExerciseAllAssociatedAlphaBlenderModeCombinations() + { + Rgba32P[] background = + [ + Rgba32P.FromRgba32(new Rgba32(220, 80, 40, 160)), + Rgba32P.FromRgba32(new Rgba32(40, 200, 100, 192)), + Rgba32P.FromRgba32(new Rgba32(120, 60, 230, 224)), + Rgba32P.FromRgba32(new Rgba32(180, 160, 20, 128)), + Rgba32P.FromRgba32(new Rgba32(30, 140, 210, 96)), + ]; + + Rgba32P[] source = + [ + Rgba32P.FromRgba32(new Rgba32(20, 180, 120, 96)), + Rgba32P.FromRgba32(new Rgba32(210, 30, 150, 144)), + Rgba32P.FromRgba32(new Rgba32(80, 220, 40, 176)), + Rgba32P.FromRgba32(new Rgba32(240, 110, 60, 208)), + Rgba32P.FromRgba32(new Rgba32(100, 50, 200, 112)), + ]; + + foreach (PixelAlphaCompositionMode alphaMode in Enum.GetValues()) + { + foreach (PixelColorBlendingMode colorMode in Enum.GetValues()) + { + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); + AssertAssociatedBlenderMatchesScalar(blender, background, source, colorMode, alphaMode); + } + } + } + + private static void AssertAssociatedBlenderMatchesScalar( + PixelBlender blender, + Rgba32P[] associatedBackground, + Rgba32P[] associatedSource, + PixelColorBlendingMode colorMode, + PixelAlphaCompositionMode alphaMode) + { + const float amount = .625F; + float[] amounts = [.125F, .375F, .625F, .875F, 1F]; + float[] coverage = [.2F, .4F, .6F, .8F, 1F]; + Rgba32P constantAssociatedSource = associatedSource[2]; + + Rgba32P[] expected = new Rgba32P[associatedBackground.Length]; + Rgba32P[] actual = new Rgba32P[associatedBackground.Length]; + Vector4[] actualSourceSpanBuffer = new Vector4[associatedBackground.Length * 3]; + Vector4[] actualConstantSourceBuffer = new Vector4[associatedBackground.Length * 2]; + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = blender.Blend(associatedBackground[i], associatedSource[i], amount); + } + + blender.Blend(Configuration.Default, actual, associatedBackground, associatedSource, amount, actualSourceSpanBuffer); + AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "SourceSpanSingleAmount"); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = blender.Blend(associatedBackground[i], constantAssociatedSource, amount); + } + + blender.Blend(Configuration.Default, actual, associatedBackground, constantAssociatedSource, amount, actualConstantSourceBuffer); + AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "ConstantSourceSingleAmount"); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = blender.Blend(associatedBackground[i], associatedSource[i], amounts[i]); + } + + blender.Blend(Configuration.Default, actual, associatedBackground, associatedSource, amounts, actualSourceSpanBuffer); + AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "SourceSpanAmountSpan"); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = blender.Blend(associatedBackground[i], constantAssociatedSource, amounts[i]); + } + + blender.Blend(Configuration.Default, actual, associatedBackground, constantAssociatedSource, amounts, actualConstantSourceBuffer); + AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "ConstantSourceAmountSpan"); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = BlendWithCoverageScalar(blender, associatedBackground[i], associatedSource[i], amount, coverage[i]); + } + + blender.BlendWithCoverage(Configuration.Default, actual, associatedBackground, associatedSource, amount, coverage, actualSourceSpanBuffer); + AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "SourceSpanSingleAmountCoverage"); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = BlendWithCoverageScalar(blender, associatedBackground[i], constantAssociatedSource, amount, coverage[i]); + } + + blender.BlendWithCoverage(Configuration.Default, actual, associatedBackground, constantAssociatedSource, amount, coverage, actualConstantSourceBuffer); + AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "ConstantSourceSingleAmountCoverage"); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = BlendWithCoverageScalar(blender, associatedBackground[i], associatedSource[i], amounts[i], coverage[i]); + } + + blender.BlendWithCoverage(Configuration.Default, actual, associatedBackground, associatedSource, amounts, coverage, actualSourceSpanBuffer); + AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "SourceSpanAmountSpanCoverage"); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = BlendWithCoverageScalar(blender, associatedBackground[i], constantAssociatedSource, amounts[i], coverage[i]); + } + + blender.BlendWithCoverage(Configuration.Default, actual, associatedBackground, constantAssociatedSource, amounts, coverage, actualConstantSourceBuffer); + AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "ConstantSourceAmountSpanCoverage"); + } + + private static Rgba32P BlendWithCoverageScalar(PixelBlender blender, Rgba32P background, Rgba32P source, float amount, float coverage) + { + Span destination = stackalloc Rgba32P[1]; + Span backgroundSpan = stackalloc Rgba32P[1] { background }; + Span sourceSpan = stackalloc Rgba32P[1] { source }; + Span coverageSpan = stackalloc float[1] { coverage }; + Span buffer = stackalloc Vector4[3]; + + // A one-pixel span takes the scalar remainder path, providing an exact oracle for each SIMD coverage result. + blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, coverageSpan, buffer); + return destination[0]; + } + + private static void AssertRgba32PEqual( + ReadOnlySpan expected, + ReadOnlySpan actual, + PixelColorBlendingMode colorMode, + PixelAlphaCompositionMode alphaMode, + string scenario) + { + for (int i = 0; i < expected.Length; i++) + { + Assert.True(expected[i] == actual[i], $"{colorMode}/{alphaMode}/{scenario}[{i}]: expected {expected[i]}, actual {actual[i]}"); + } + } + + private static void ExerciseBlender(PixelBlender blender) + { + Rgba32 background = Color.MistyRose.ToPixel(); + Rgba32 source = Color.MidnightBlue.ToPixel(); + + ExerciseBlender(blender, background, source); + } + + /// + /// Exercises every bulk overload across vector-width boundaries and compares it with single-pixel composition. + /// + /// The pixel format. + /// The blender under test. + /// The background pixel. + /// The source pixel. + private static void ExerciseBlender(PixelBlender blender, TPixel background, TPixel source) + where TPixel : unmanaged, IPixel + { + const float amount = .625F; + + float[] amounts = [0F, .125F, .375F, .625F, .875F, 1F, .5F]; + float[] coverage = [1F, .8F, .6F, .4F, .2F, 0F, .5F]; + + // Seven pixels exercise one AVX-512 batch plus three tails, or three AVX2 batches plus one tail. + TPixel[] destination = new TPixel[7]; + TPixel[] expected = new TPixel[7]; + TPixel[] backgroundSpan = [background, background, background, background, background, background, background]; + TPixel[] sourceSpan = [source, source, source, source, source, source, source]; + Vector4[] sourceSpanBuffer = new Vector4[destination.Length * 3]; + Vector4[] constantSourceBuffer = new Vector4[destination.Length * 2]; + + Array.Fill(expected, blender.Blend(background, source, amount)); + + blender.Blend(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, sourceSpanBuffer); + Assert.Equal(expected, destination); + + blender.Blend(Configuration.Default, destination, backgroundSpan, source, amount, constantSourceBuffer); + Assert.Equal(expected, destination); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = blender.Blend(background, source, amounts[i]); + } + + blender.Blend(Configuration.Default, destination, backgroundSpan, sourceSpan, amounts, sourceSpanBuffer); + Assert.Equal(expected, destination); + + blender.Blend(Configuration.Default, destination, backgroundSpan, source, amounts, constantSourceBuffer); + Assert.Equal(expected, destination); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = BlendWithCoverageScalar(blender, background, source, amount, coverage[i]); + } + + blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, coverage, sourceSpanBuffer); + Assert.Equal(expected, destination); + + blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, source, amount, coverage, constantSourceBuffer); + Assert.Equal(expected, destination); + + for (int i = 0; i < expected.Length; i++) + { + expected[i] = BlendWithCoverageScalar(blender, background, source, amounts[i], coverage[i]); + } + + blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, sourceSpan, amounts, coverage, sourceSpanBuffer); + Assert.Equal(expected, destination); + + blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, source, amounts, coverage, constantSourceBuffer); + Assert.Equal(expected, destination); + } + + /// + /// Computes a one-pixel coverage result through the scalar remainder path. + /// + /// The pixel format. + /// The blender under test. + /// The background pixel. + /// The source pixel. + /// The source opacity. + /// The pixel coverage. + /// The blended pixel. + private static TPixel BlendWithCoverageScalar(PixelBlender blender, TPixel background, TPixel source, float amount, float coverage) + where TPixel : unmanaged, IPixel + { + Span destination = stackalloc TPixel[1]; + Span backgroundSpan = stackalloc TPixel[1] { background }; + Span sourceSpan = stackalloc TPixel[1] { source }; + Span coverageSpan = stackalloc float[1] { coverage }; + Span buffer = stackalloc Vector4[3]; + + blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, coverageSpan, buffer); + + return destination[0]; + } } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs index 1086afe76..f8c296846 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs @@ -59,7 +59,7 @@ public class PorterDuffCompositorTests FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX, provider, mode.ToString()); } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffFunctionsTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffFunctionsTests.cs index 976a272eb..0def09788 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffFunctionsTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffFunctionsTests.cs @@ -4,7 +4,6 @@ using System.Numerics; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; -using Castle.Components.DictionaryAdapter; using SixLabors.ImageSharp.PixelFormats.PixelBlenders; using SixLabors.ImageSharp.Tests.TestUtilities; @@ -45,6 +44,22 @@ public class PorterDuffFunctionsTests Assert.Equal(expected256, actual, FloatComparer); } + [Theory] + [MemberData(nameof(NormalBlendFunctionData))] + public void NormalBlendFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.NormalSrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + public static TheoryData MultiplyFunctionData { get; } = new() { { new TestVector4(1, 1, 1, 1), new TestVector4(1, 1, 1, 1), 1, new TestVector4(1, 1, 1, 1) }, @@ -77,6 +92,22 @@ public class PorterDuffFunctionsTests Assert.Equal(expected256, actual, FloatComparer); } + [Theory] + [MemberData(nameof(MultiplyFunctionData))] + public void MultiplyFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.MultiplySrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + public static TheoryData AddFunctionData { get; } = new() { { new TestVector4(1, 1, 1, 1), new TestVector4(1, 1, 1, 1), 1, new TestVector4(1, 1, 1, 1) }, @@ -109,6 +140,22 @@ public class PorterDuffFunctionsTests Assert.Equal(expected256, actual, FloatComparer); } + [Theory] + [MemberData(nameof(AddFunctionData))] + public void AddFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.AddSrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + public static TheoryData SubtractFunctionData { get; } = new() { { new TestVector4(1, 1, 1, 1), new TestVector4(1, 1, 1, 1), 1, new TestVector4(0, 0, 0, 1) }, @@ -141,6 +188,22 @@ public class PorterDuffFunctionsTests Assert.Equal(expected256, actual, FloatComparer); } + [Theory] + [MemberData(nameof(SubtractFunctionData))] + public void SubtractFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.SubtractSrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + public static TheoryData ScreenFunctionData { get; } = new() { { new TestVector4(1, 1, 1, 1), new TestVector4(1, 1, 1, 1), 1, new TestVector4(1, 1, 1, 1) }, @@ -173,6 +236,22 @@ public class PorterDuffFunctionsTests Assert.Equal(expected256, actual, FloatComparer); } + [Theory] + [MemberData(nameof(ScreenFunctionData))] + public void ScreenFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.ScreenSrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + public static TheoryData DarkenFunctionData { get; } = new() { { new TestVector4(1, 1, 1, 1), new TestVector4(1, 1, 1, 1), 1, new TestVector4(1, 1, 1, 1) }, @@ -205,6 +284,22 @@ public class PorterDuffFunctionsTests Assert.Equal(expected256, actual, FloatComparer); } + [Theory] + [MemberData(nameof(DarkenFunctionData))] + public void DarkenFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.DarkenSrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + public static TheoryData LightenFunctionData { get; } = new() { { new TestVector4(1, 1, 1, 1), new TestVector4(1, 1, 1, 1), 1, new TestVector4(1, 1, 1, 1) }, @@ -237,6 +332,22 @@ public class PorterDuffFunctionsTests Assert.Equal(expected256, actual, FloatComparer); } + [Theory] + [MemberData(nameof(LightenFunctionData))] + public void LightenFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.LightenSrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + public static TheoryData OverlayFunctionData { get; } = new() { { new TestVector4(1, 1, 1, 1), new TestVector4(1, 1, 1, 1), 1, new TestVector4(1, 1, 1, 1) }, @@ -269,6 +380,22 @@ public class PorterDuffFunctionsTests Assert.Equal(expected256, actual, FloatComparer); } + [Theory] + [MemberData(nameof(OverlayFunctionData))] + public void OverlayFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.OverlaySrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + public static TheoryData HardLightFunctionData { get; } = new() { { new TestVector4(1, 1, 1, 1), new TestVector4(1, 1, 1, 1), 1, new TestVector4(1, 1, 1, 1) }, @@ -300,4 +427,27 @@ public class PorterDuffFunctionsTests Vector256 actual = PorterDuffFunctions.HardLightSrcOver(back256, source256, Vector256.Create(amount)); Assert.Equal(expected256, actual, FloatComparer); } + + [Theory] + [MemberData(nameof(HardLightFunctionData))] + public void HardLightFunction512(TestVector4 back, TestVector4 source, float amount, TestVector4 expected) + { + if (!Avx512F.IsSupported) + { + return; + } + + Vector512 back512 = CreateVector512(back); + Vector512 source512 = CreateVector512(source); + Vector512 expected512 = CreateVector512(expected); + Vector512 actual = PorterDuffFunctions.HardLightSrcOver(back512, source512, Vector512.Create(amount)); + Assert.Equal(expected512, actual, FloatComparer); + } + + private static Vector512 CreateVector512(TestVector4 vector) + => Vector512.Create( + vector.X, vector.Y, vector.Z, vector.W, + vector.X, vector.Y, vector.Z, vector.W, + vector.X, vector.Y, vector.Z, vector.W, + vector.X, vector.Y, vector.Z, vector.W); } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffFunctionsTestsTPixel.cs b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffFunctionsTestsTPixel.cs index 2c97cbde0..a2d571ad3 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffFunctionsTestsTPixel.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffFunctionsTestsTPixel.cs @@ -9,12 +9,21 @@ namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelBlenders; public class PorterDuffFunctionsTestsTPixel { + private const int BulkBlendCount = 4; + private static Span AsSpan(T value) where T : struct { return new Span(new[] { value }); } + private static T[] CreateFilledArray(T value) + { + T[] values = new T[BulkBlendCount]; + values.AsSpan().Fill(value); + return values; + } + public static TheoryData NormalBlendFunctionData = new() { { new TestPixel(1, 1, 1, 1), new TestPixel(1, 1, 1, 1), 1, new TestPixel(1, 1, 1, 1) }, @@ -37,7 +46,7 @@ public class PorterDuffFunctionsTestsTPixel public void NormalBlendFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.NormalSrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.NormalSrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -46,9 +55,14 @@ public class PorterDuffFunctionsTestsTPixel public void NormalBlendFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.NormalSrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.NormalSrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } public static TheoryData MultiplyFunctionData = new() @@ -77,7 +91,7 @@ public class PorterDuffFunctionsTestsTPixel public void MultiplyFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.MultiplySrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.MultiplySrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -86,9 +100,14 @@ public class PorterDuffFunctionsTestsTPixel public void MultiplyFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.MultiplySrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.MultiplySrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } public static TheoryData AddFunctionData = new() @@ -127,7 +146,7 @@ public class PorterDuffFunctionsTestsTPixel public void AddFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.AddSrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.AddSrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -136,9 +155,14 @@ public class PorterDuffFunctionsTestsTPixel public void AddFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.AddSrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.AddSrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } public static TheoryData SubtractFunctionData = new() @@ -167,7 +191,7 @@ public class PorterDuffFunctionsTestsTPixel public void SubtractFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.SubtractSrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.SubtractSrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -176,9 +200,14 @@ public class PorterDuffFunctionsTestsTPixel public void SubtractFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.SubtractSrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.SubtractSrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } public static TheoryData ScreenFunctionData = new() @@ -207,7 +236,7 @@ public class PorterDuffFunctionsTestsTPixel public void ScreenFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.ScreenSrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.ScreenSrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -216,9 +245,14 @@ public class PorterDuffFunctionsTestsTPixel public void ScreenFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.ScreenSrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.ScreenSrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } public static TheoryData DarkenFunctionData = new() @@ -247,7 +281,7 @@ public class PorterDuffFunctionsTestsTPixel public void DarkenFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.DarkenSrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.DarkenSrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -256,9 +290,14 @@ public class PorterDuffFunctionsTestsTPixel public void DarkenFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.DarkenSrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.DarkenSrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } public static TheoryData LightenFunctionData = new() @@ -287,7 +326,7 @@ public class PorterDuffFunctionsTestsTPixel public void LightenFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.LightenSrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.LightenSrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -296,9 +335,14 @@ public class PorterDuffFunctionsTestsTPixel public void LightenFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.LightenSrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.LightenSrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } public static TheoryData OverlayFunctionData = new() @@ -327,7 +371,7 @@ public class PorterDuffFunctionsTestsTPixel public void OverlayFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.OverlaySrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.OverlaySrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -336,9 +380,14 @@ public class PorterDuffFunctionsTestsTPixel public void OverlayFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.OverlaySrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.OverlaySrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } public static TheoryData HardLightFunctionData = new() @@ -367,7 +416,7 @@ public class PorterDuffFunctionsTestsTPixel public void HardLightFunctionBlender(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - TPixel actual = new DefaultPixelBlenders.HardLightSrcOver().Blend(back.AsPixel(), source.AsPixel(), amount); + TPixel actual = DefaultPixelBlenders.HardLightSrcOver.Blend(back.AsPixel(), source.AsPixel(), amount); VectorAssert.Equal(expected.AsPixel(), actual, 2); } @@ -376,8 +425,13 @@ public class PorterDuffFunctionsTestsTPixel public void HardLightFunctionBlenderBulk(TestPixel back, TestPixel source, float amount, TestPixel expected) where TPixel : unmanaged, IPixel { - Span dest = new(new TPixel[1]); - new DefaultPixelBlenders.HardLightSrcOver().Blend(this.Configuration, dest, back.AsSpan(), source.AsSpan(), AsSpan(amount)); - VectorAssert.Equal(expected.AsPixel(), dest[0], 2); + TPixel[] dest = new TPixel[BulkBlendCount]; + DefaultPixelBlenders.HardLightSrcOver.Blend(this.Configuration, dest, CreateFilledArray(back.AsPixel()), CreateFilledArray(source.AsPixel()), CreateFilledArray(amount)); + + TPixel expectedPixel = expected.AsPixel(); + foreach (TPixel pixel in dest) + { + VectorAssert.Equal(expectedPixel, pixel, 2); + } } } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelConverterTests.ReferenceImplementations.cs b/tests/ImageSharp.Tests/PixelFormats/PixelConverterTests.ReferenceImplementations.cs index 2a5c5765a..dbff54140 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelConverterTests.ReferenceImplementations.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelConverterTests.ReferenceImplementations.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats; @@ -73,13 +74,11 @@ public abstract partial class PixelConverterTests } internal static void To( - Configuration configuration, ReadOnlySpan sourcePixels, Span destinationPixels) where TSourcePixel : unmanaged, IPixel where TDestinationPixel : unmanaged, IPixel { - Guard.NotNull(configuration, nameof(configuration)); Guard.DestinationShouldNotBeTooShort(sourcePixels, destinationPixels, nameof(destinationPixels)); int count = sourcePixels.Length; @@ -92,13 +91,14 @@ public abstract partial class PixelConverterTests return; } - // Normal conversion + // Scalar source and destination boundaries are the reference for the bulk operation: each format owns any representation conversion and destination quantization it requires. ref TDestinationPixel destRef = ref MemoryMarshal.GetReference(destinationPixels); for (int i = 0; i < count; i++) { ref TSourcePixel sp = ref Unsafe.Add(ref sourceRef, i); ref TDestinationPixel dp = ref Unsafe.Add(ref destRef, i); - dp = TDestinationPixel.FromScaledVector4(sp.ToScaledVector4()); + Vector4 vector = sp.ToUnassociatedScaledVector4(); + dp = TDestinationPixel.FromUnassociatedScaledVector4(vector); } } } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs index b37282927..3747b41ee 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/PixelOperationsTests.Specialized.Generated.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. // @@ -7,7 +7,6 @@ using SixLabors.ImageSharp.PixelFormats; using Xunit; using Xunit.Abstractions; - namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; public partial class PixelOperationsTests @@ -43,6 +42,21 @@ public partial class PixelOperationsTests } } + public partial class Argb32P_OperationsTests : PixelOperationsTests + { + public Argb32P_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void PixelTypeInfoHasCorrectAlphaRepresentation() + { + var alphaRepresentation = Argb32P.GetPixelTypeInfo().AlphaRepresentation; + Assert.Equal(PixelAlphaRepresentation.Associated, alphaRepresentation); + } + } + public partial class Abgr32_OperationsTests : PixelOperationsTests { public Abgr32_OperationsTests(ITestOutputHelper output) @@ -58,6 +72,21 @@ public partial class PixelOperationsTests } } + public partial class Abgr32P_OperationsTests : PixelOperationsTests + { + public Abgr32P_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void PixelTypeInfoHasCorrectAlphaRepresentation() + { + var alphaRepresentation = Abgr32P.GetPixelTypeInfo().AlphaRepresentation; + Assert.Equal(PixelAlphaRepresentation.Associated, alphaRepresentation); + } + } + public partial class Bgr24_OperationsTests : PixelOperationsTests { public Bgr24_OperationsTests(ITestOutputHelper output) @@ -103,6 +132,21 @@ public partial class PixelOperationsTests } } + public partial class Bgra32P_OperationsTests : PixelOperationsTests + { + public Bgra32P_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void PixelTypeInfoHasCorrectAlphaRepresentation() + { + var alphaRepresentation = Bgra32P.GetPixelTypeInfo().AlphaRepresentation; + Assert.Equal(PixelAlphaRepresentation.Associated, alphaRepresentation); + } + } + public partial class Bgra4444_OperationsTests : PixelOperationsTests { public Bgra4444_OperationsTests(ITestOutputHelper output) @@ -193,6 +237,21 @@ public partial class PixelOperationsTests } } + public partial class HalfVector4P_OperationsTests : PixelOperationsTests + { + public HalfVector4P_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void PixelTypeInfoHasCorrectAlphaRepresentation() + { + var alphaRepresentation = HalfVector4P.GetPixelTypeInfo().AlphaRepresentation; + Assert.Equal(PixelAlphaRepresentation.Associated, alphaRepresentation); + } + } + public partial class L16_OperationsTests : PixelOperationsTests { public L16_OperationsTests(ITestOutputHelper output) @@ -283,6 +342,21 @@ public partial class PixelOperationsTests } } + public partial class NormalizedByte4P_OperationsTests : PixelOperationsTests + { + public NormalizedByte4P_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void PixelTypeInfoHasCorrectAlphaRepresentation() + { + var alphaRepresentation = NormalizedByte4P.GetPixelTypeInfo().AlphaRepresentation; + Assert.Equal(PixelAlphaRepresentation.Associated, alphaRepresentation); + } + } + public partial class NormalizedShort2_OperationsTests : PixelOperationsTests { public NormalizedShort2_OperationsTests(ITestOutputHelper output) @@ -388,6 +462,21 @@ public partial class PixelOperationsTests } } + public partial class Rgba32P_OperationsTests : PixelOperationsTests + { + public Rgba32P_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void PixelTypeInfoHasCorrectAlphaRepresentation() + { + var alphaRepresentation = Rgba32P.GetPixelTypeInfo().AlphaRepresentation; + Assert.Equal(PixelAlphaRepresentation.Associated, alphaRepresentation); + } + } + public partial class Rgba64_OperationsTests : PixelOperationsTests { public Rgba64_OperationsTests(ITestOutputHelper output) @@ -403,6 +492,36 @@ public partial class PixelOperationsTests } } + public partial class RgbaHalf_OperationsTests : PixelOperationsTests + { + public RgbaHalf_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void PixelTypeInfoHasCorrectAlphaRepresentation() + { + var alphaRepresentation = RgbaHalf.GetPixelTypeInfo().AlphaRepresentation; + Assert.Equal(PixelAlphaRepresentation.Unassociated, alphaRepresentation); + } + } + + public partial class RgbaHalfP_OperationsTests : PixelOperationsTests + { + public RgbaHalfP_OperationsTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void PixelTypeInfoHasCorrectAlphaRepresentation() + { + var alphaRepresentation = RgbaHalfP.GetPixelTypeInfo().AlphaRepresentation; + Assert.Equal(PixelAlphaRepresentation.Associated, alphaRepresentation); + } + } + public partial class RgbaVector_OperationsTests : PixelOperationsTests { public RgbaVector_OperationsTests(ITestOutputHelper output) diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude index ccf90fe40..e4a7e881e 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/Generated/_Common.ttinclude @@ -29,32 +29,47 @@ using Xunit.Abstractions; "Rgba1010102", "Rgba32", "Rgba64", + "RgbaHalf", "RgbaVector", "Short4" ]; - private static readonly string[] AssociatedAlphaPixelTypes = []; + private static readonly string[] AssociatedAlphaPixelTypes = + [ + "Argb32P", + "Abgr32P", + "Bgra32P", + "HalfVector4P", + "NormalizedByte4P", + "Rgba32P", + "RgbaHalfP" + ]; private static readonly string[] CommonPixelTypes = [ "A8", "Argb32", + "Argb32P", "Abgr32", + "Abgr32P", "Bgr24", "Bgr565", "Bgra32", + "Bgra32P", "Bgra4444", "Bgra5551", "Byte4", "HalfSingle", "HalfVector2", "HalfVector4", + "HalfVector4P", "L16", "L8", "La16", "La32", "NormalizedByte2", "NormalizedByte4", + "NormalizedByte4P", "NormalizedShort2", "NormalizedShort4", "Rg32", @@ -62,7 +77,10 @@ using Xunit.Abstractions; "Rgb48", "Rgba1010102", "Rgba32", + "Rgba32P", "Rgba64", + "RgbaHalf", + "RgbaHalfP", "RgbaVector", "Short2", "Short4" diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs index 32b62fc03..c569919f5 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs @@ -70,7 +70,7 @@ public abstract class PixelOperationsTests : MeasureFixture protected virtual PixelOperations Operations { get; } = PixelOperations.Instance; - protected bool HasUnassociatedAlpha => TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated; + protected bool HasAssociatedAlpha => TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated; internal static TPixel[] CreateExpectedPixelData(Vector4[] source, RefAction vectorModifier = null) { @@ -102,6 +102,35 @@ public abstract class PixelOperationsTests : MeasureFixture return expected; } + /// + /// Creates the scalar oracle for companded scaled input. + /// + /// The source vectors. + /// Whether the source vectors use associated alpha. + /// The converted pixels. + internal static TPixel[] CreateCompandedExpectedPixelData(Vector4[] source, bool associated) + { + TPixel[] expected = new TPixel[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + Vector4 vector = source[i]; + + if (associated) + { + Numerics.UnPremultiply(ref vector); + } + + vector = SRgbCompanding.Compress(vector); + + // Transfer functions operate on straight RGB. Let the destination pixel perform any required association so its stored + // alpha quantization participates exactly once, matching the public bulk conversion contract. + expected[i] = TPixel.FromUnassociatedScaledVector4(vector); + } + + return expected; + } + [Fact] public void PixelTypeInfoHasCorrectBitsPerPixel() { @@ -161,12 +190,18 @@ public abstract class PixelOperationsTests : MeasureFixture [MemberData(nameof(ArraySizesData))] public void FromCompandedScaledVector4(int count) { - void SourceAction(ref Vector4 v) => v = SRgbCompanding.Expand(v); + void SourceAction(ref Vector4 v) + { + v = SRgbCompanding.Expand(v); - void ExpectedAction(ref Vector4 v) => v = SRgbCompanding.Compress(v); + if (this.HasAssociatedAlpha) + { + Numerics.Premultiply(ref v); + } + } Vector4[] source = CreateVector4TestData(count, SourceAction); - TPixel[] expected = CreateScaledExpectedPixelData(source, ExpectedAction); + TPixel[] expected = CreateCompandedExpectedPixelData(source, this.HasAssociatedAlpha); TestOperation( source, @@ -183,35 +218,27 @@ public abstract class PixelOperationsTests : MeasureFixture [MemberData(nameof(ArraySizesData))] public void FromPremultipliedVector4(int count) { - void SourceAction(ref Vector4 v) - { - if (this.HasUnassociatedAlpha) - { - Numerics.Premultiply(ref v); - } - } + Vector4[] source = CreateVector4TestData(count); + TPixel[] expected = new TPixel[count]; - void ExpectedAction(ref Vector4 v) + for (int i = 0; i < source.Length; i++) { - if (this.HasUnassociatedAlpha) - { - Numerics.UnPremultiply(ref v); - } + // Native W is not necessarily opacity. Use the per-pixel representation contract to create and consume associated-native vectors. + TPixel pixel = TPixel.FromUnassociatedVector4(source[i]); + source[i] = pixel.ToAssociatedVector4(); + expected[i] = TPixel.FromAssociatedVector4(source[i]); } - Vector4[] source = CreateVector4TestData(count, (ref Vector4 v) => SourceAction(ref v)); - TPixel[] expected = CreateExpectedPixelData(source, (ref Vector4 v) => ExpectedAction(ref v)); - TestOperation( source, expected, (s, d) => { - PixelConversionModifiers modifiers = this.HasUnassociatedAlpha - ? PixelConversionModifiers.Premultiply - : PixelConversionModifiers.None; - - this.Operations.FromVector4Destructive(this.Configuration, s, d.GetSpan(), modifiers); + this.Operations.FromVector4Destructive( + this.Configuration, + s, + d.GetSpan(), + PixelConversionModifiers.Premultiply); }); } @@ -221,7 +248,7 @@ public abstract class PixelOperationsTests : MeasureFixture { void SourceAction(ref Vector4 v) { - if (this.HasUnassociatedAlpha) + if (!this.HasAssociatedAlpha) { Numerics.Premultiply(ref v); } @@ -229,7 +256,7 @@ public abstract class PixelOperationsTests : MeasureFixture void ExpectedAction(ref Vector4 v) { - if (this.HasUnassociatedAlpha) + if (!this.HasAssociatedAlpha) { Numerics.UnPremultiply(ref v); } @@ -243,15 +270,11 @@ public abstract class PixelOperationsTests : MeasureFixture expected, (s, d) => { - PixelConversionModifiers modifiers = this.HasUnassociatedAlpha - ? PixelConversionModifiers.Premultiply - : PixelConversionModifiers.None; - this.Operations.FromVector4Destructive( - this.Configuration, - s, - d.GetSpan(), - modifiers | PixelConversionModifiers.Scale); + this.Configuration, + s, + d.GetSpan(), + PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale); }); } @@ -262,40 +285,22 @@ public abstract class PixelOperationsTests : MeasureFixture void SourceAction(ref Vector4 v) { v = SRgbCompanding.Expand(v); - - if (this.HasUnassociatedAlpha) - { - Numerics.Premultiply(ref v); - } - } - - void ExpectedAction(ref Vector4 v) - { - if (this.HasUnassociatedAlpha) - { - Numerics.UnPremultiply(ref v); - } - - v = SRgbCompanding.Compress(v); + Numerics.Premultiply(ref v); } Vector4[] source = CreateVector4TestData(count, SourceAction); - TPixel[] expected = CreateScaledExpectedPixelData(source, ExpectedAction); + TPixel[] expected = CreateCompandedExpectedPixelData(source, true); TestOperation( source, expected, (s, d) => { - PixelConversionModifiers modifiers = this.HasUnassociatedAlpha - ? PixelConversionModifiers.Premultiply - : PixelConversionModifiers.None; - this.Operations.FromVector4Destructive( - this.Configuration, - s, - d.GetSpan(), - modifiers | PixelConversionModifiers.SRgbCompand | PixelConversionModifiers.Scale); + this.Configuration, + s, + d.GetSpan(), + PixelConversionModifiers.Premultiply | PixelConversionModifiers.SRgbCompand | PixelConversionModifiers.Scale); }, false); } @@ -317,22 +322,27 @@ public abstract class PixelOperationsTests : MeasureFixture { new TestPixel(), new TestPixel(), + new TestPixel(), new TestPixel(), + new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), + new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), + new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), + new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), @@ -340,7 +350,10 @@ public abstract class PixelOperationsTests : MeasureFixture new TestPixel(), new TestPixel(), new TestPixel(), + new TestPixel(), new TestPixel(), + new TestPixel(), + new TestPixel(), new TestPixel(), new TestPixel(), new TestPixel(), @@ -355,7 +368,7 @@ public abstract class PixelOperationsTests : MeasureFixture TPixel[] source = CreatePixelTestData(count); TDestPixel[] expected = new TDestPixel[count]; - PixelConverterTests.ReferenceImplementations.To(this.Configuration, source, expected); + PixelConverterTests.ReferenceImplementations.To(source, expected); TestOperation(source, expected, (s, d) => this.Operations.To(this.Configuration, s, d.GetSpan()), false); } @@ -383,9 +396,26 @@ public abstract class PixelOperationsTests : MeasureFixture { void SourceAction(ref Vector4 v) { + if (this.HasAssociatedAlpha) + { + Numerics.Premultiply(ref v); + } } - void ExpectedAction(ref Vector4 v) => v = SRgbCompanding.Expand(v); + void ExpectedAction(ref Vector4 v) + { + if (this.HasAssociatedAlpha) + { + Numerics.UnPremultiply(ref v); + } + + v = SRgbCompanding.Expand(v); + + if (this.HasAssociatedAlpha) + { + Numerics.Premultiply(ref v); + } + } TPixel[] source = CreateScaledPixelTestData(count, SourceAction); Vector4[] expected = CreateExpectedScaledVector4Data(source, ExpectedAction); @@ -404,15 +434,15 @@ public abstract class PixelOperationsTests : MeasureFixture [MemberData(nameof(ArraySizesData))] public void ToPremultipliedVector4(int count) { - void SourceAction(ref Vector4 v) + TPixel[] source = CreatePixelTestData(count); + Vector4[] expected = new Vector4[count]; + + for (int i = 0; i < source.Length; i++) { + // Association is defined by the pixel format because native W can be affine or integer-valued rather than normalized opacity. + expected[i] = source[i].ToAssociatedVector4(); } - void ExpectedAction(ref Vector4 v) => Numerics.Premultiply(ref v); - - TPixel[] source = CreatePixelTestData(count, SourceAction); - Vector4[] expected = CreateExpectedVector4Data(source, ExpectedAction); - TestOperation( source, expected, @@ -427,7 +457,13 @@ public abstract class PixelOperationsTests : MeasureFixture { } - void ExpectedAction(ref Vector4 v) => Numerics.Premultiply(ref v); + void ExpectedAction(ref Vector4 v) + { + if (!this.HasAssociatedAlpha) + { + Numerics.Premultiply(ref v); + } + } TPixel[] source = CreateScaledPixelTestData(count, SourceAction); Vector4[] expected = CreateExpectedScaledVector4Data(source, (ref Vector4 v) => ExpectedAction(ref v)); @@ -448,10 +484,19 @@ public abstract class PixelOperationsTests : MeasureFixture { void SourceAction(ref Vector4 v) { + if (this.HasAssociatedAlpha) + { + Numerics.Premultiply(ref v); + } } void ExpectedAction(ref Vector4 v) { + if (this.HasAssociatedAlpha) + { + Numerics.UnPremultiply(ref v); + } + v = SRgbCompanding.Expand(v); Numerics.Premultiply(ref v); } @@ -499,7 +544,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int i4 = i * 4; - Argb32 argb = Argb32.FromScaledVector4(source[i].ToScaledVector4()); + Argb32 argb = Argb32.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); expected[i4] = argb.A; expected[i4 + 1] = argb.R; @@ -543,7 +588,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int i3 = i * 3; - Bgr24 bgr = Bgr24.FromScaledVector4(source[i].ToScaledVector4()); + Bgr24 bgr = Bgr24.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); expected[i3] = bgr.B; expected[i3 + 1] = bgr.G; expected[i3 + 2] = bgr.R; @@ -585,7 +630,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int i4 = i * 4; - Bgra32 bgra = Bgra32.FromScaledVector4(source[i].ToScaledVector4()); + Bgra32 bgra = Bgra32.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); expected[i4] = bgra.B; expected[i4 + 1] = bgra.G; expected[i4 + 2] = bgra.R; @@ -628,7 +673,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int i4 = i * 4; - Abgr32 abgr = Abgr32.FromScaledVector4(source[i].ToScaledVector4()); + Abgr32 abgr = Abgr32.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); expected[i4] = abgr.A; expected[i4 + 1] = abgr.B; expected[i4 + 2] = abgr.G; @@ -674,7 +719,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int offset = i * size; - Bgra5551 bgra = Bgra5551.FromScaledVector4(source[i].ToScaledVector4()); + Bgra5551 bgra = Bgra5551.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); OctetBytes bytes = Unsafe.As(ref bgra); expected[offset] = bytes[0]; expected[offset + 1] = bytes[1]; @@ -714,7 +759,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { - expected[i] = L8.FromScaledVector4(source[i].ToScaledVector4()); + expected[i] = L8.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); } TestOperation( @@ -751,7 +796,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { - expected[i] = L16.FromScaledVector4(source[i].ToScaledVector4()); + expected[i] = L16.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); } TestOperation( @@ -793,7 +838,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int offset = i * size; - La16 la = La16.FromScaledVector4(source[i].ToScaledVector4()); + La16 la = La16.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); OctetBytes bytes = Unsafe.As(ref la); expected[offset] = bytes[0]; expected[offset + 1] = bytes[1]; @@ -838,7 +883,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int offset = i * size; - La32 la = La32.FromScaledVector4(source[i].ToScaledVector4()); + La32 la = La32.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); OctetBytes bytes = Unsafe.As(ref la); expected[offset] = bytes[0]; expected[offset + 1] = bytes[1]; @@ -882,7 +927,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int i3 = i * 3; - Rgb24 rgb = Rgb24.FromScaledVector4(source[i].ToScaledVector4()); + Rgb24 rgb = Rgb24.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); expected[i3] = rgb.R; expected[i3 + 1] = rgb.G; expected[i3 + 2] = rgb.B; @@ -924,7 +969,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int i4 = i * 4; - Rgba32 rgba = Rgba32.FromScaledVector4(source[i].ToScaledVector4()); + Rgba32 rgba = Rgba32.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); expected[i4] = rgba.R; expected[i4 + 1] = rgba.G; expected[i4 + 2] = rgba.B; @@ -967,7 +1012,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int i6 = i * 6; - Rgb48 rgb = Rgb48.FromScaledVector4(source[i].ToScaledVector4()); + Rgb48 rgb = Rgb48.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); OctetBytes rgb48Bytes = Unsafe.As(ref rgb); expected[i6] = rgb48Bytes[0]; expected[i6 + 1] = rgb48Bytes[1]; @@ -1013,7 +1058,7 @@ public abstract class PixelOperationsTests : MeasureFixture for (int i = 0; i < count; i++) { int i8 = i * 8; - Rgba64 rgba = Rgba64.FromScaledVector4(source[i].ToScaledVector4()); + Rgba64 rgba = Rgba64.FromScaledVector4(ToUnassociatedScaledVector4(source[i])); OctetBytes rgba64Bytes = Unsafe.As(ref rgba); expected[i8] = rgba64Bytes[0]; expected[i8 + 1] = rgba64Bytes[1]; @@ -1072,6 +1117,9 @@ public abstract class PixelOperationsTests : MeasureFixture return expected; } + // The scalar conversion boundary owns representation-specific rounding. Byte-export tests compare each bulk operation with that scalar result. + private static Vector4 ToUnassociatedScaledVector4(TPixel source) => source.ToUnassociatedScaledVector4(); + internal static void TestOperation( TSource[] source, TDest[] expected, @@ -1208,15 +1256,34 @@ public abstract class PixelOperationsTests : MeasureFixture Assert.Equal(expected[i], actual[i], comparer); } } - else if (!this.PreferExactComparison && typeof(IPixel).IsAssignableFrom(typeof(TDest)) && IsComplexPixel()) + else if (!this.PreferExactComparison && typeof(IPixel).IsAssignableFrom(typeof(TDest))) { Span expected = this.ExpectedDestBuffer.AsSpan(); Span actual = this.ActualDestBuffer.GetSpan(); - ApproximateFloatComparer comparer = new(TestEnvironment.Is64BitProcess ? 0.0001F : 0.001F); - for (int i = 0; i < count; i++) + if (IsComplexPixel()) + { + ApproximateFloatComparer comparer = new(TestEnvironment.Is64BitProcess ? 0.0001F : 0.001F); + + for (int i = 0; i < count; i++) + { + Assert.Equal(((IPixel)expected[i]).ToScaledVector4(), ((IPixel)actual[i]).ToScaledVector4(), comparer); + } + } + else { - Assert.Equal(((IPixel)expected[i]).ToScaledVector4(), ((IPixel)actual[i]).ToScaledVector4(), comparer); + // SIMD and scalar conversion can select adjacent packed values at a quantization boundary. + int tolerance = Unsafe.SizeOf() <= sizeof(ushort) ? 17 : 1; + + for (int i = 0; i < count; i++) + { + Rgba32 expectedPixel = ((IPixel)expected[i]).ToRgba32(); + Rgba32 actualPixel = ((IPixel)actual[i]).ToRgba32(); + Assert.InRange(Math.Abs(expectedPixel.R - actualPixel.R), 0, tolerance); + Assert.InRange(Math.Abs(expectedPixel.G - actualPixel.G), 0, tolerance); + Assert.InRange(Math.Abs(expectedPixel.B - actualPixel.B), 0, tolerance); + Assert.InRange(Math.Abs(expectedPixel.A - actualPixel.A), 0, tolerance); + } } } else diff --git a/tests/ImageSharp.Tests/PixelFormats/Rgb96Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Rgb96Tests.cs new file mode 100644 index 000000000..e70e2fa0f --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/Rgb96Tests.cs @@ -0,0 +1,311 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +[Trait("Category", "PixelFormats")] +public class Rgb96Tests +{ + [Theory] + [InlineData(1, 2, 3)] + [InlineData(50, 70, 80)] + [InlineData(1000, 2000, 3000)] + public void Constructor(uint r, uint g, uint b) + { + Rgb96 p = new(r, g, b); + + Assert.Equal(r, p.R); + Assert.Equal(g, p.G); + Assert.Equal(b, p.B); + } + + [Fact] + public void Rgb96_ToVector4() + { + Assert.Equal(Vector4.UnitW, new Rgb96(0, 0, 0).ToVector4()); + Assert.Equal(Vector4.One, new Rgb96(uint.MaxValue, uint.MaxValue, uint.MaxValue).ToVector4()); + Assert.Equal(new Vector4(0.5F, 0.5F, 0.5F, 1.0F), new Rgb96(uint.MaxValue / 2, uint.MaxValue / 2, uint.MaxValue / 2).ToVector4()); + } + + [Theory] + [InlineData(uint.MaxValue, uint.MaxValue, uint.MaxValue)] + [InlineData(0, 0, 0)] + [InlineData(uint.MaxValue / 2, 100, 2222)] + public void Rgb96_ToScaledVector4(uint r, uint g, uint b) + { + // arrange + Rgb96 rgb = new(r, g, b); + + float max = uint.MaxValue; + float rr = r / max; + float gg = g / max; + float bb = b / max; + + // act + Vector4 actual = rgb.ToScaledVector4(); + + // assert + Assert.Equal(rr, actual.X); + Assert.Equal(gg, actual.Y); + Assert.Equal(bb, actual.Z); + Assert.Equal(1.0F, actual.W); + } + + [Theory] + [InlineData(0, 0, 0)] + [InlineData(5000, 100, 2222)] + public void Rgb96_FromScaledVector4(uint r, uint g, uint b) + { + // arrange + Rgb96 source = new(r, g, b); + Vector4 scaled = source.ToScaledVector4(); + + // act + Rgb96 actual = Rgb96.FromScaledVector4(scaled); + + // assert + Assert.Equal(source, actual); + } + + [Fact] + public void Rgb96_ToRgba32() + { + // arrange + Rgb96 rgb96 = new((uint)(uint.MaxValue * 0.1), uint.MaxValue / 2, uint.MaxValue); + Rgba32 expected = new(25, 127, 255, 255); + + // act + Rgba32 actual = rgb96.ToRgba32(); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Equality_WhenTrue() + { + Rgb96 c1 = new(100, 2000, 3000); + Rgb96 c2 = new(100, 2000, 3000); + + Assert.True(c1.Equals(c2)); + Assert.True(c1.GetHashCode() == c2.GetHashCode()); + } + + [Fact] + public void Equality_WhenFalse() + { + Rgb96 c1 = new(100, 2000, 3000); + Rgb96 c2 = new(101, 2000, 3000); + Rgb96 c3 = new(100, 2000, 3001); + + Assert.False(c1.Equals(c2)); + Assert.False(c2.Equals(c3)); + Assert.False(c3.Equals(c1)); + } + + [Fact] + public void Rgb96_FromRgba32() + { + // arrange + Rgba32 source = new(25, 127, 255, 255); + Rgb96 expected = new(421075225, 2139062143, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromRgba32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromRgb24() + { + // arrange + Rgb24 source = new(25, 127, 255); + Rgb96 expected = new(421075225, 2139062143, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromRgb24(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromRgb48() + { + // arrange + Rgb48 source = new(0, 32767, ushort.MaxValue); + Rgb96 expected = new(0, 2147450879, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromRgb48(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromRgba64() + { + // arrange + Rgba64 source = new(0, 32767, ushort.MaxValue, ushort.MaxValue); + Rgb96 expected = new(0, 2147450879, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromRgba64(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromLa32() + { + // arrange + La32 source = new(32767, ushort.MaxValue); + Rgb96 expected = new(2147450879, 2147450879, 2147450879); + + // act + Rgb96 actual = Rgb96.FromLa32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromLa16() + { + // arrange + La16 source = new(127, byte.MaxValue); + Rgb96 expected = new(2139062143, 2139062143, 2139062143); + + // act + Rgb96 actual = Rgb96.FromLa16(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromL16() + { + // arrange + L16 source = new(32767); + Rgb96 expected = new(2147450879, 2147450879, 2147450879); + + // act + Rgb96 actual = Rgb96.FromL16(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromL8() + { + // arrange + L8 source = new(127); + Rgb96 expected = new(2139062143, 2139062143, 2139062143); + + // act + Rgb96 actual = Rgb96.FromL8(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromBgra32() + { + // arrange + Bgra32 source = new(127, 25, 255); + Rgb96 expected = new(2139062143, 421075225, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromBgra32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromBgr24() + { + // arrange + Bgr24 source = new(127, 25, 255); + Rgb96 expected = new(2139062143, 421075225, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromBgr24(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromBgra5551() + { + // arrange + Bgra5551 source = new(1.0f, 1.0f, 1.0f, 1.0f); + Rgb96 expected = new(uint.MaxValue, uint.MaxValue, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromBgra5551(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromArgb32() + { + // arrange + Argb32 source = new(127, 25, 255); + Rgb96 expected = new(2139062143, 421075225, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromArgb32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_FromAbgr32() + { + // arrange + Abgr32 source = new(127, 25, 255); + Rgb96 expected = new(2139062143, 421075225, uint.MaxValue); + + // act + Rgb96 actual = Rgb96.FromAbgr32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgb96_PixelInformation() + { + PixelTypeInfo info = Rgb96.GetPixelTypeInfo(); + Assert.Equal(Unsafe.SizeOf() * 8, info.BitsPerPixel); + Assert.Equal(PixelAlphaRepresentation.None, info.AlphaRepresentation); + Assert.Equal(PixelColorType.RGB, info.ColorType); + + PixelComponentInfo? maybeComponentInfo = info.ComponentInfo; + Assert.NotNull(maybeComponentInfo); + PixelComponentInfo componentInfo = maybeComponentInfo.Value; + + Assert.Equal(3, componentInfo.ComponentCount); + Assert.Equal(0, componentInfo.Padding); + Assert.Equal(32, componentInfo.GetComponentPrecision(0)); + Assert.Equal(32, componentInfo.GetComponentPrecision(1)); + Assert.Equal(32, componentInfo.GetComponentPrecision(2)); + Assert.Equal(32, componentInfo.GetMaximumComponentPrecision()); + } +} diff --git a/tests/ImageSharp.Tests/PixelFormats/Rgba128Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Rgba128Tests.cs new file mode 100644 index 000000000..339d57ed4 --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/Rgba128Tests.cs @@ -0,0 +1,313 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +[Trait("Category", "PixelFormats")] +public class Rgba128Tests +{ + [Theory] + [InlineData(1, 2, 3, 4)] + [InlineData(50, 70, 80, 90)] + [InlineData(1000, 2000, 3000, 4000)] + public void Constructor(uint r, uint g, uint b, uint a) + { + Rgba128 p = new(r, g, b, a); + + Assert.Equal(r, p.R); + Assert.Equal(g, p.G); + Assert.Equal(b, p.B); + } + + [Fact] + public void Rgba128_ToVector4() + { + Assert.Equal(Vector4.Zero, new Rgba128(0, 0, 0, 0).ToVector4()); + Assert.Equal(Vector4.One, new Rgba128(uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue).ToVector4()); + Assert.Equal(new Vector4(0.5F, 0.5F, 0.5F, 0.5F), new Rgba128(uint.MaxValue / 2, uint.MaxValue / 2, uint.MaxValue / 2, uint.MaxValue / 2).ToVector4()); + } + + [Theory] + [InlineData(uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue)] + [InlineData(0, 0, 0, 0)] + [InlineData(uint.MaxValue / 2, 100, 2222, 3333)] + public void Rgba128_ToScaledVector4(uint r, uint g, uint b, uint a) + { + // arrange + Rgba128 rgb = new(r, g, b, a); + + float max = uint.MaxValue; + float rr = r / max; + float gg = g / max; + float bb = b / max; + float aa = a / max; + + // act + Vector4 actual = rgb.ToScaledVector4(); + + // assert + Assert.Equal(rr, actual.X); + Assert.Equal(gg, actual.Y); + Assert.Equal(bb, actual.Z); + Assert.Equal(aa, actual.W); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(5000, 100, 2222, 3333)] + public void Rgba128_FromScaledVector4(uint r, uint g, uint b, uint a) + { + // arrange + Rgba128 source = new(r, g, b, a); + Vector4 scaled = source.ToScaledVector4(); + + // act + Rgba128 actual = Rgba128.FromScaledVector4(scaled); + + // assert + Assert.Equal(source, actual); + } + + [Fact] + public void Rgba128_ToRgba32() + { + // arrange + Rgba128 rgb96 = new((uint)(uint.MaxValue * 0.1), uint.MaxValue / 2, uint.MaxValue, uint.MaxValue); + Rgba32 expected = new(25, 127, 255, 255); + + // act + Rgba32 actual = rgb96.ToRgba32(); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Equality_WhenTrue() + { + Rgba128 c1 = new(100, 2000, 3000, 4000); + Rgba128 c2 = new(100, 2000, 3000, 4000); + + Assert.True(c1.Equals(c2)); + Assert.True(c1.GetHashCode() == c2.GetHashCode()); + } + + [Fact] + public void Equality_WhenFalse() + { + Rgba128 c1 = new(100, 2000, 3000, 4000); + Rgba128 c2 = new(101, 2000, 3000, 4000); + Rgba128 c3 = new(100, 2000, 3001, 4000); + + Assert.False(c1.Equals(c2)); + Assert.False(c2.Equals(c3)); + Assert.False(c3.Equals(c1)); + } + + [Fact] + public void Rgba128_FromRgba32() + { + // arrange + Rgba32 source = new(25, 127, 255, 255); + Rgba128 expected = new(421075225, 2139062143, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromRgba32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromRgb24() + { + // arrange + Rgb24 source = new(25, 127, 255); + Rgba128 expected = new(421075225, 2139062143, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromRgb24(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromRgb48() + { + // arrange + Rgb48 source = new(0, 32767, ushort.MaxValue); + Rgba128 expected = new(0, 2147450879, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromRgb48(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromRgba64() + { + // arrange + Rgba64 source = new(0, 32767, ushort.MaxValue, ushort.MaxValue); + Rgba128 expected = new(0, 2147450879, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromRgba64(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromLa32() + { + // arrange + La32 source = new(32767, ushort.MaxValue); + Rgba128 expected = new(2147450879, 2147450879, 2147450879, 2147450879); + + // act + Rgba128 actual = Rgba128.FromLa32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromLa16() + { + // arrange + La16 source = new(127, byte.MaxValue); + Rgba128 expected = new(2139062143, 2139062143, 2139062143, 2139062143); + + // act + Rgba128 actual = Rgba128.FromLa16(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromL16() + { + // arrange + L16 source = new(32767); + Rgba128 expected = new(2147450879, 2147450879, 2147450879, 2147450879); + + // act + Rgba128 actual = Rgba128.FromL16(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromL8() + { + // arrange + L8 source = new(127); + Rgba128 expected = new(2139062143, 2139062143, 2139062143, 2139062143); + + // act + Rgba128 actual = Rgba128.FromL8(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromBgra32() + { + // arrange + Bgra32 source = new(127, 25, 255); + Rgba128 expected = new(2139062143, 421075225, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromBgra32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromBgr24() + { + // arrange + Bgr24 source = new(127, 25, 255); + Rgba128 expected = new(2139062143, 421075225, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromBgr24(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromBgra5551() + { + // arrange + Bgra5551 source = new(1.0f, 1.0f, 1.0f, 1.0f); + Rgba128 expected = new(uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromBgra5551(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromArgb32() + { + // arrange + Argb32 source = new(127, 25, 255); + Rgba128 expected = new(2139062143, 421075225, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromArgb32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_FromAbgr32() + { + // arrange + Abgr32 source = new(127, 25, 255); + Rgba128 expected = new(2139062143, 421075225, uint.MaxValue, uint.MaxValue); + + // act + Rgba128 actual = Rgba128.FromAbgr32(source); + + // assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Rgba128_PixelInformation() + { + PixelTypeInfo info = Rgba128.GetPixelTypeInfo(); + Assert.Equal(Unsafe.SizeOf() * 8, info.BitsPerPixel); + Assert.Equal(PixelAlphaRepresentation.Unassociated, info.AlphaRepresentation); + Assert.Equal(PixelColorType.RGB | PixelColorType.Alpha, info.ColorType); + + PixelComponentInfo? maybeComponentInfo = info.ComponentInfo; + Assert.NotNull(maybeComponentInfo); + PixelComponentInfo componentInfo = maybeComponentInfo.Value; + + Assert.Equal(4, componentInfo.ComponentCount); + Assert.Equal(0, componentInfo.Padding); + Assert.Equal(32, componentInfo.GetComponentPrecision(0)); + Assert.Equal(32, componentInfo.GetComponentPrecision(1)); + Assert.Equal(32, componentInfo.GetComponentPrecision(2)); + Assert.Equal(32, componentInfo.GetComponentPrecision(3)); + Assert.Equal(32, componentInfo.GetMaximumComponentPrecision()); + } +} diff --git a/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs b/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs new file mode 100644 index 000000000..9b22a9f8c --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/RgbaHalfTests.cs @@ -0,0 +1,283 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +/// +/// Tests the unit-range binary16 RGBA pixel formats. +/// +[Trait("Category", "PixelFormats")] +public class RgbaHalfTests +{ + /// + /// Verifies that the unassociated format has the native layout required by RGBA binary16 surfaces. + /// + [Fact] + public void RgbaHalfHasRgbaBinary16Layout() + { + RgbaHalf pixel = new(.25F, .5F, .75F, 1F); + ulong expected = BitConverter.HalfToUInt16Bits((Half).25F) + | ((ulong)BitConverter.HalfToUInt16Bits((Half).5F) << 16) + | ((ulong)BitConverter.HalfToUInt16Bits((Half).75F) << 32) + | ((ulong)BitConverter.HalfToUInt16Bits((Half)1F) << 48); + + Assert.Equal(8, Unsafe.SizeOf()); + Assert.Equal(expected, pixel.PackedValue); + Assert.Equal(new Vector4(.25F, .5F, .75F, 1F), pixel.ToVector4()); + Assert.Equal(pixel.ToVector4(), pixel.ToScaledVector4()); + } + + /// + /// Verifies that zero-filled binary16 storage represents transparent black without affine remapping. + /// + [Fact] + public void RgbaHalfDefaultIsTransparentBlack() + { + RgbaHalf pixel = default; + + Assert.Equal(Vector4.Zero, pixel.ToVector4()); + Assert.Equal(Vector4.Zero, pixel.ToScaledVector4()); + } + + /// + /// Verifies that scaled input is clamped to the pixel format's unit color range. + /// + [Fact] + public void RgbaHalfFromScaledVector4ClampsToUnitRange() + { + RgbaHalf pixel = RgbaHalf.FromScaledVector4(new Vector4(-1F, .5F, 2F, 1F)); + + Assert.Equal(new Vector4(0F, .5F, 1F, 1F), pixel.ToScaledVector4()); + } + + /// + /// Verifies that the associated format stores associated binary16 components in the same RGBA order. + /// + [Fact] + public void RgbaHalfPHasAssociatedRgbaBinary16Layout() + { + RgbaHalfP pixel = new(.125F, .25F, .375F, .5F); + ulong expected = BitConverter.HalfToUInt16Bits((Half).125F) + | ((ulong)BitConverter.HalfToUInt16Bits((Half).25F) << 16) + | ((ulong)BitConverter.HalfToUInt16Bits((Half).375F) << 32) + | ((ulong)BitConverter.HalfToUInt16Bits((Half).5F) << 48); + + Assert.Equal(8, Unsafe.SizeOf()); + Assert.Equal(expected, pixel.PackedValue); + Assert.Equal(new Vector4(.125F, .25F, .375F, .5F), pixel.ToAssociatedScaledVector4()); + } + + /// + /// Verifies that zero-filled associated binary16 storage represents transparent black. + /// + [Fact] + public void RgbaHalfPDefaultIsTransparentBlack() + { + RgbaHalfP pixel = default; + + Assert.Equal(Vector4.Zero, pixel.ToVector4()); + Assert.Equal(Vector4.Zero, pixel.ToScaledVector4()); + Assert.Equal(Vector4.Zero, pixel.ToUnassociatedScaledVector4()); + } + + /// + /// Verifies that association uses the alpha value that survives binary16 quantization. + /// + [Fact] + public void RgbaHalfPQuantizesAlphaBeforeAssociation() + { + Vector4 source = new(.75F, .5F, .25F, 1F / 3F); + float storedAlpha = (float)(Half)source.W; + RgbaHalfP pixel = RgbaHalfP.FromUnassociatedScaledVector4(source); + + Assert.Equal((Half)(source.X * storedAlpha), pixel.R); + Assert.Equal((Half)(source.Y * storedAlpha), pixel.G); + Assert.Equal((Half)(source.Z * storedAlpha), pixel.B); + Assert.Equal((Half)storedAlpha, pixel.A); + } + + /// + /// Verifies every bulk representation path against its scalar pixel contract at each SIMD width and tail boundary. + /// + [Fact] + public void RgbaHalfBulkConversionsMatchScalarAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + static () => AssertBulkConversionsMatchScalar(), + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies every associated bulk representation path against its scalar pixel contract at each SIMD width and tail boundary. + /// + [Fact] + public void RgbaHalfPBulkConversionsMatchScalarAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + static () => AssertBulkConversionsMatchScalar(), + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies the declared component precision and alpha representation for the unassociated format. + /// + [Fact] + public void RgbaHalfPixelInformationIsCorrect() => AssertPixelInformation(PixelAlphaRepresentation.Unassociated); + + /// + /// Verifies the declared component precision and alpha representation for the associated format. + /// + [Fact] + public void RgbaHalfPPixelInformationIsCorrect() => AssertPixelInformation(PixelAlphaRepresentation.Associated); + + /// + /// Compares bulk conversions with the corresponding scalar pixel operations for representative vector widths and remainders. + /// + /// The binary16 pixel format to test. + private static void AssertBulkConversionsMatchScalar() + where TPixel : unmanaged, IPixel + { + int[] lengths = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 259]; + PixelConversionModifiers[] modifiers = + [ + PixelConversionModifiers.None, + PixelConversionModifiers.Scale, + PixelConversionModifiers.Premultiply, + PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply, + PixelConversionModifiers.UnPremultiply, + PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply + ]; + + foreach (int length in lengths) + { + TPixel[] pixels = new TPixel[length]; + + for (int i = 0; i < length; i++) + { + pixels[i] = TPixel.FromUnassociatedScaledVector4(CreateUnassociatedVector(i)); + } + + foreach (PixelConversionModifiers modifier in modifiers) + { + Vector4[] expectedVectors = new Vector4[length]; + Vector4[] actualVectors = new Vector4[length]; + Vector4[] sourceVectors = new Vector4[length]; + TPixel[] expectedPixels = new TPixel[length]; + TPixel[] actualPixels = new TPixel[length]; + bool associated = RequestsAssociated(modifier); + bool scaled = modifier.IsDefined(PixelConversionModifiers.Scale); + + for (int i = 0; i < length; i++) + { + expectedVectors[i] = ToScalarVector(pixels[i], associated, scaled); + sourceVectors[i] = CreateUnassociatedVector(i + 19); + + if (associated) + { + Numerics.Premultiply(ref sourceVectors[i]); + } + + expectedPixels[i] = FromScalarVector(sourceVectors[i], associated, scaled); + } + + PixelOperations.Instance.ToVector4(Configuration.Default, pixels, actualVectors, modifier); + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, sourceVectors.AsSpan(), actualPixels, modifier); + + Assert.Equal(expectedVectors, actualVectors); + Assert.Equal(expectedPixels, actualPixels); + } + } + } + + /// + /// Creates a deterministic unassociated unit-range vector for bulk conversion tests. + /// + /// The source index used to vary the component values. + /// The generated vector. + private static Vector4 CreateUnassociatedVector(int index) + => new( + ((index * 37) % 101) / 100F, + ((index * 53) % 101) / 100F, + ((index * 71) % 101) / 100F, + ((index * 89) % 101) / 100F); + + /// + /// Resolves whether a modifier combination requests associated output under the pixel format's native representation. + /// + /// The pixel format whose native representation participates in modifier resolution. + /// The conversion modifiers. + /// when the resulting vector representation is associated. + private static bool RequestsAssociated(PixelConversionModifiers modifiers) + where TPixel : unmanaged, IPixel + => modifiers.IsDefined(PixelConversionModifiers.Premultiply) + || (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated + && !modifiers.IsDefined(PixelConversionModifiers.UnPremultiply)); + + /// + /// Converts one pixel through the scalar API selected by the requested representation and range. + /// + /// The source pixel format. + /// The source pixel. + /// Whether the result should use associated alpha. + /// Whether the result should use the scaled range. + /// The converted vector. + private static Vector4 ToScalarVector(TPixel pixel, bool associated, bool scaled) + where TPixel : unmanaged, IPixel + => (associated, scaled) switch + { + (true, true) => pixel.ToAssociatedScaledVector4(), + (true, false) => pixel.ToAssociatedVector4(), + (false, true) => pixel.ToUnassociatedScaledVector4(), + _ => pixel.ToUnassociatedVector4() + }; + + /// + /// Converts one vector through the scalar API selected by its representation and range. + /// + /// The destination pixel format. + /// The source vector. + /// Whether the source uses associated alpha. + /// Whether the source uses the scaled range. + /// The converted pixel. + private static TPixel FromScalarVector(Vector4 vector, bool associated, bool scaled) + where TPixel : unmanaged, IPixel + => (associated, scaled) switch + { + (true, true) => TPixel.FromAssociatedScaledVector4(vector), + (true, false) => TPixel.FromAssociatedVector4(vector), + (false, true) => TPixel.FromUnassociatedScaledVector4(vector), + _ => TPixel.FromUnassociatedVector4(vector) + }; + + /// + /// Verifies the component layout and alpha metadata exposed by a binary16 pixel format. + /// + /// The pixel format to inspect. + /// The expected alpha representation. + private static void AssertPixelInformation(PixelAlphaRepresentation alphaRepresentation) + where TPixel : unmanaged, IPixel + { + PixelTypeInfo info = TPixel.GetPixelTypeInfo(); + PixelComponentInfo componentInfo = info.ComponentInfo.Value; + + Assert.Equal(64, info.BitsPerPixel); + Assert.Equal(alphaRepresentation, info.AlphaRepresentation); + Assert.Equal(PixelColorType.RGB | PixelColorType.Alpha, info.ColorType); + Assert.Equal(4, componentInfo.ComponentCount); + Assert.Equal(0, componentInfo.Padding); + + for (int i = 0; i < componentInfo.ComponentCount; i++) + { + Assert.Equal(16, componentInfo.GetComponentPrecision(i)); + } + } +} + +/// +/// Applies the shared associated-alpha contract to . +/// +public class RgbaHalfPAssociatedAlphaTests : AssociatedAlphaPixelTests +{ +} diff --git a/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs index f23da0c7a..86fc82830 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -51,33 +52,44 @@ public class Short2Tests [Fact] public void Short2_ToScaledVector4() { - // arrange - Short2 short2 = new(Vector2.One * 0x7FFF); - - // act - Vector4 actual = short2.ToScaledVector4(); - - // assert - Assert.Equal(1, actual.X); - Assert.Equal(1, actual.Y); - Assert.Equal(0, actual.Z); - Assert.Equal(1, actual.W); + Assert.Equal(new Vector4(1F, 1F, 0F, 1F), new Short2(Vector2.One * short.MaxValue).ToScaledVector4()); + Assert.Equal(new Vector4(0F, 0F, 0F, 1F), new Short2(Vector2.One * short.MinValue).ToScaledVector4()); } [Fact] public void Short2_FromScaledVector4() { - // arrange - Short2 short2 = new(Vector2.One * 0x7FFF); - const ulong expected = 0x7FFF7FFF; + Assert.Equal(0x80008000U, Short2.FromScaledVector4(new Vector4(0F, 0F, 0F, 1F)).PackedValue); + Assert.Equal(0x7FFF7FFFU, Short2.FromScaledVector4(Vector4.One).PackedValue); + } - // act - Vector4 scaled = short2.ToScaledVector4(); - Short2 pixel = Short2.FromScaledVector4(scaled); - uint actual = pixel.PackedValue; + [Fact] + public void Short2_BulkScaledConversionsCoverFullSignedRange() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertShort2BulkScaledConversionsCoverFullSignedRange, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); - // assert - Assert.Equal(expected, actual); + private static void AssertShort2BulkScaledConversionsCoverFullSignedRange() + { + const int length = 17; + Short2[] source = new Short2[length]; + Vector4[] expectedVectors = new Vector4[length]; + + for (int i = 0; i < length; i++) + { + bool minimum = (i & 1) == 0; + source[i].PackedValue = minimum ? 0x80008000U : 0x7FFF7FFFU; + expectedVectors[i] = minimum ? new Vector4(0F, 0F, 0F, 1F) : new Vector4(1F, 1F, 0F, 1F); + } + + Vector4[] actualVectors = new Vector4[length]; + PixelOperations.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + Short2[] actualPixels = new Short2[length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs b/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs index 819ff0e1e..c8502462d 100644 --- a/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; @@ -38,32 +39,44 @@ public class Short4Tests [Fact] public void Short4_ToScaledVector4() { - // arrange - Short4 short4 = new(Vector4.One * 0x7FFF); - - // act - Vector4 actual = short4.ToScaledVector4(); - - // assert - Assert.Equal(1, actual.X); - Assert.Equal(1, actual.Y); - Assert.Equal(1, actual.Z); - Assert.Equal(1, actual.W); + Assert.Equal(Vector4.One, new Short4(Vector4.One * short.MaxValue).ToScaledVector4()); + Assert.Equal(Vector4.Zero, new Short4(Vector4.One * short.MinValue).ToScaledVector4()); } [Fact] public void Short4_FromScaledVector4() { - // arrange - Short4 short4 = new(Vector4.One * 0x7FFF); - Vector4 scaled = short4.ToScaledVector4(); - const long expected = 0x7FFF7FFF7FFF7FFF; + Assert.Equal(0x8000800080008000UL, Short4.FromScaledVector4(Vector4.Zero).PackedValue); + Assert.Equal(0x7FFF7FFF7FFF7FFFUL, Short4.FromScaledVector4(Vector4.One).PackedValue); + } - // act - Short4 pixel = Short4.FromScaledVector4(scaled); + [Fact] + public void Short4_BulkScaledConversionsCoverFullSignedRange() => + FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertShort4BulkScaledConversionsCoverFullSignedRange, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); - // assert - Assert.Equal((ulong)expected, pixel.PackedValue); + private static void AssertShort4BulkScaledConversionsCoverFullSignedRange() + { + const int length = 17; + Short4[] source = new Short4[length]; + Vector4[] expectedVectors = new Vector4[length]; + + for (int i = 0; i < length; i++) + { + bool minimum = (i & 1) == 0; + source[i].PackedValue = minimum ? 0x8000800080008000UL : 0x7FFF7FFF7FFF7FFFUL; + expectedVectors[i] = minimum ? Vector4.Zero : Vector4.One; + } + + Vector4[] actualVectors = new Vector4[length]; + PixelOperations.Instance.ToVector4(Configuration.Default, source, actualVectors, PixelConversionModifiers.Scale); + Assert.Equal(expectedVectors, actualVectors); + + Vector4[] destructiveSource = [.. expectedVectors]; + Short4[] actualPixels = new Short4[length]; + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actualPixels, PixelConversionModifiers.Scale); + Assert.Equal(source, actualPixels); } [Fact] diff --git a/tests/ImageSharp.Tests/PixelFormats/Vector4ConvertersTests.cs b/tests/ImageSharp.Tests/PixelFormats/Vector4ConvertersTests.cs new file mode 100644 index 000000000..837d807f0 --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/Vector4ConvertersTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +/// +/// Verifies the shared stateful traversal used by affine pixel-vector conversions. +/// +[Trait("Category", "PixelFormats")] +public class Vector4ConvertersTests +{ + private static readonly int[] Lengths = [0, 1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 257]; + + /// + /// Verifies multiply-then-add behavior for every SIMD boundary and the software fallback. + /// + [Fact] + public void MultiplyThenAddMatchesComponentArithmeticAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertMultiplyThenAddMatchesComponentArithmetic, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies add-then-divide behavior for every SIMD boundary and the software fallback. + /// + [Fact] + public void AddThenDivideMatchesComponentArithmeticAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertAddThenDivideMatchesComponentArithmetic, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Compares the multiply-then-add traversal with independently evaluated component expressions. + /// + private static void AssertMultiplyThenAddMatchesComponentArithmetic() + { + Vector4 multiplier = new(2F, -3F, .5F, 4F); + Vector4 offset = new(-7F, 11F, 13F, -17F); + + foreach (int length in Lengths) + { + Vector4[] actual = CreateSource(length); + Vector4[] expected = new Vector4[length]; + + for (int i = 0; i < expected.Length; i++) + { + Vector4 value = actual[i]; + + expected[i] = new Vector4((value.X * multiplier.X) + offset.X, (value.Y * multiplier.Y) + offset.Y, (value.Z * multiplier.Z) + offset.Z, (value.W * multiplier.W) + offset.W); + } + + Vector4Converters.MultiplyThenAdd(actual, multiplier, offset); + + Assert.Equal(expected, actual); + } + } + + /// + /// Compares the add-then-divide traversal with independently evaluated component expressions. + /// + private static void AssertAddThenDivideMatchesComponentArithmetic() + { + Vector4 offset = new(-7F, 11F, 13F, -17F); + Vector4 divisor = new(2F, -3F, .5F, 4F); + + foreach (int length in Lengths) + { + Vector4[] actual = CreateSource(length); + Vector4[] expected = new Vector4[length]; + + for (int i = 0; i < expected.Length; i++) + { + Vector4 value = actual[i]; + + expected[i] = new Vector4((value.X + offset.X) / divisor.X, (value.Y + offset.Y) / divisor.Y, (value.Z + offset.Z) / divisor.Z, (value.W + offset.W) / divisor.W); + } + + Vector4Converters.AddThenDivide(actual, offset, divisor); + + Assert.Equal(expected, actual); + } + } + + /// + /// Creates non-uniform values that expose component ordering and traversal overlap errors. + /// + /// The number of vectors to create. + /// The populated vector buffer. + private static Vector4[] CreateSource(int length) + { + Vector4[] result = new Vector4[length]; + + for (int i = 0; i < result.Length; i++) + { + result[i] = new Vector4((i * 17F) - 31F, (i * -23F) + 37F, (i * .25F) - 41F, (i * 3F) + 43F); + } + + return result; + } +} diff --git a/tests/ImageSharp.Tests/Primitives/PointFTests.cs b/tests/ImageSharp.Tests/Primitives/PointFTests.cs index 8b574daf0..bea0f802f 100644 --- a/tests/ImageSharp.Tests/Primitives/PointFTests.cs +++ b/tests/ImageSharp.Tests/Primitives/PointFTests.cs @@ -133,6 +133,69 @@ public class PointFTests Assert.Equal(new PointF(30, 30), pout); } + [Fact] + public void TransformMatrix4x4_AffineMatchesMatrix3x2() + { + PointF p = new(13, 17); + Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, PointF.Empty); + Matrix4x4 m4 = new(m3); + + PointF r3 = PointF.Transform(p, m3); + PointF r4 = PointF.Transform(p, m4); + + Assert.Equal(r3.X, r4.X, ApproximateFloatComparer); + Assert.Equal(r3.Y, r4.Y, ApproximateFloatComparer); + } + + [Fact] + public void TransformMatrix4x4_Identity() + { + PointF p = new(42.5F, -17.3F); + PointF result = PointF.Transform(p, Matrix4x4.Identity); + + Assert.Equal(p.X, result.X, ApproximateFloatComparer); + Assert.Equal(p.Y, result.Y, ApproximateFloatComparer); + } + + [Fact] + public void TransformMatrix4x4_Translation() + { + PointF p = new(10, 20); + Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0); + PointF result = PointF.Transform(p, m); + + Assert.Equal(15F, result.X, ApproximateFloatComparer); + Assert.Equal(17F, result.Y, ApproximateFloatComparer); + } + + [Fact] + public void TransformMatrix4x4_Scale() + { + PointF p = new(10, 20); + Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1); + PointF result = PointF.Transform(p, m); + + Assert.Equal(20F, result.X, ApproximateFloatComparer); + Assert.Equal(60F, result.Y, ApproximateFloatComparer); + } + + [Fact] + public void TransformMatrix4x4_Projective() + { + // A taper matrix with M14 != 0 produces W != 1, requiring perspective divide. + PointF p = new(100, 50); + Matrix4x4 m = Matrix4x4.Identity; + m.M14 = 0.005F; // perspective component + + PointF result = PointF.Transform(p, m); + + // W = x*M14 + M44 = 100*0.005 + 1 = 1.5 + // X = x*M11 + M41 = 100, Y = y*M22 + M42 = 50 + // result = (100/1.5, 50/1.5) + Assert.Equal(100F / 1.5F, result.X, ApproximateFloatComparer); + Assert.Equal(50F / 1.5F, result.Y, ApproximateFloatComparer); + } + [Theory] [InlineData(float.MaxValue, float.MinValue)] [InlineData(float.MinValue, float.MaxValue)] diff --git a/tests/ImageSharp.Tests/Primitives/PointTests.cs b/tests/ImageSharp.Tests/Primitives/PointTests.cs index 3ad2a83b3..d2192f53b 100644 --- a/tests/ImageSharp.Tests/Primitives/PointTests.cs +++ b/tests/ImageSharp.Tests/Primitives/PointTests.cs @@ -159,9 +159,10 @@ public class PointTests Point p = new(13, 17); Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty); - Point pout = Point.Transform(p, matrix); + PointF pout = Point.Transform(p, matrix); - Assert.Equal(new Point(-3, 21), pout); + Assert.Equal(-2.828427F, pout.X, 4); + Assert.Equal(21.213203F, pout.Y, 4); } [Fact] @@ -170,8 +171,56 @@ public class PointTests Point p = new(13, 17); Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(45, 45, Point.Empty); - Point pout = Point.Transform(p, matrix); - Assert.Equal(new Point(30, 30), pout); + PointF pout = Point.Transform(p, matrix); + Assert.Equal(30F, pout.X, 4); + Assert.Equal(30F, pout.Y, 4); + } + + [Fact] + public void TransformMatrix4x4_AffineMatchesMatrix3x2() + { + Point p = new(13, 17); + Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty); + Matrix4x4 m4 = new(m3); + + PointF r3 = Point.Transform(p, m3); + PointF r4 = Point.Transform(p, m4); + + Assert.Equal(r3, r4); + } + + [Fact] + public void TransformMatrix4x4_Identity() + { + Point p = new(42, -17); + PointF result = Point.Transform(p, Matrix4x4.Identity); + + Assert.Equal((PointF)p, result); + } + + [Fact] + public void TransformMatrix4x4_Translation() + { + Point p = new(10, 20); + Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0); + PointF result = Point.Transform(p, m); + + Assert.Equal(15F, result.X, 4); + Assert.Equal(17F, result.Y, 4); + } + + [Fact] + public void TransformMatrix4x4_Projective() + { + Point p = new(100, 50); + Matrix4x4 m = Matrix4x4.Identity; + m.M14 = 0.005F; + + PointF result = Point.Transform(p, m); + + // W = 100*0.005 + 1 = 1.5 => (100/1.5, 50/1.5) + Assert.Equal(100F / 1.5F, result.X, 4); + Assert.Equal(50F / 1.5F, result.Y, 4); } [Theory] diff --git a/tests/ImageSharp.Tests/Primitives/RectangleFTests.cs b/tests/ImageSharp.Tests/Primitives/RectangleFTests.cs index 4122daaa5..9a6ff0957 100644 --- a/tests/ImageSharp.Tests/Primitives/RectangleFTests.cs +++ b/tests/ImageSharp.Tests/Primitives/RectangleFTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Globalization; +using System.Numerics; namespace SixLabors.ImageSharp.Tests; @@ -243,6 +244,61 @@ public class RectangleFTests Assert.Equal(expectedRect, r1); } + [Fact] + public void TransformMatrix4x4_AffineMatchesMatrix3x2() + { + RectangleF rect = new(10, 20, 100, 50); + Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3); + Matrix4x4 m4 = new(m3); + + RectangleF r3 = RectangleF.Transform(rect, m3); + RectangleF r4 = RectangleF.Transform(rect, m4); + + Assert.Equal(r3, r4); + } + + [Fact] + public void TransformMatrix4x4_Identity() + { + RectangleF rect = new(10, 20, 100, 50); + RectangleF result = RectangleF.Transform(rect, Matrix4x4.Identity); + + Assert.Equal(rect, result); + } + + [Fact] + public void TransformMatrix4x4_Translation() + { + RectangleF rect = new(10, 20, 100, 50); + Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0); + RectangleF result = RectangleF.Transform(rect, m); + + Assert.Equal(new RectangleF(15, 17, 100, 50), result); + } + + [Fact] + public void TransformMatrix4x4_Scale() + { + RectangleF rect = new(10, 20, 100, 50); + Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1); + RectangleF result = RectangleF.Transform(rect, m); + + Assert.Equal(new RectangleF(20, 60, 200, 150), result); + } + + [Fact] + public void TransformMatrix4x4_RotationReturnsBoundingBox() + { + RectangleF rect = new(10, 20, 100, 50); + Matrix4x4 m = Matrix4x4.CreateRotationZ(MathF.PI / 2F); + RectangleF result = RectangleF.Transform(rect, m); + + Assert.Equal(-70F, result.X, 4); + Assert.Equal(10F, result.Y, 4); + Assert.Equal(50F, result.Width, 4); + Assert.Equal(100F, result.Height, 4); + } + [Fact] public void ToStringTest() { diff --git a/tests/ImageSharp.Tests/Primitives/RectangleTests.cs b/tests/ImageSharp.Tests/Primitives/RectangleTests.cs index 2800852af..a8ce99078 100644 --- a/tests/ImageSharp.Tests/Primitives/RectangleTests.cs +++ b/tests/ImageSharp.Tests/Primitives/RectangleTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Globalization; +using System.Numerics; namespace SixLabors.ImageSharp.Tests; @@ -294,6 +295,51 @@ public class RectangleTests Assert.Equal(expectedRect, r1); } + [Fact] + public void TransformMatrix4x4_AffineMatchesMatrix3x2() + { + Rectangle rect = new(10, 20, 100, 50); + Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3); + Matrix4x4 m4 = new(m3); + + RectangleF r3 = Rectangle.Transform(rect, m3); + RectangleF r4 = Rectangle.Transform(rect, m4); + + Assert.Equal(r3, r4); + } + + [Fact] + public void TransformMatrix4x4_Identity() + { + Rectangle rect = new(10, 20, 100, 50); + RectangleF result = Rectangle.Transform(rect, Matrix4x4.Identity); + + Assert.Equal(new RectangleF(10, 20, 100, 50), result); + } + + [Fact] + public void TransformMatrix4x4_Translation() + { + Rectangle rect = new(10, 20, 100, 50); + Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0); + RectangleF result = Rectangle.Transform(rect, m); + + Assert.Equal(new RectangleF(15, 17, 100, 50), result); + } + + [Fact] + public void TransformMatrix4x4_RotationReturnsBoundingBox() + { + Rectangle rect = new(10, 20, 100, 50); + Matrix4x4 m = Matrix4x4.CreateRotationZ(MathF.PI / 2F); + RectangleF result = Rectangle.Transform(rect, m); + + Assert.Equal(-70F, result.X, 4); + Assert.Equal(10F, result.Y, 4); + Assert.Equal(50F, result.Width, 4); + Assert.Equal(100F, result.Height, 4); + } + [Fact] public void ToStringTest() { diff --git a/tests/ImageSharp.Tests/Processing/AlphaAssociationProcessorTests.cs b/tests/ImageSharp.Tests/Processing/AlphaAssociationProcessorTests.cs new file mode 100644 index 000000000..485f64174 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/AlphaAssociationProcessorTests.cs @@ -0,0 +1,441 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Extensions.Convolution; +using SixLabors.ImageSharp.Processing.Processors.Convolution; +using SixLabors.ImageSharp.Processing.Processors.Dithering; +using SixLabors.ImageSharp.Processing.Processors.Filters; +using SixLabors.ImageSharp.Processing.Processors.Normalization; + +namespace SixLabors.ImageSharp.Tests.Processing; + +[Trait("Category", "Processors")] +public class AlphaAssociationProcessorTests +{ + [Theory] + [InlineData(ProcessorCase.Opaque)] + [InlineData(ProcessorCase.Invert)] + [InlineData(ProcessorCase.ColorMatrix)] + [InlineData(ProcessorCase.Convolution)] + [InlineData(ProcessorCase.ConvolutionPreserveAlpha)] + [InlineData(ProcessorCase.Convolution2D)] + [InlineData(ProcessorCase.Convolution2DPreserveAlpha)] + [InlineData(ProcessorCase.Convolution2Pass)] + [InlineData(ProcessorCase.Convolution2PassPreserveAlpha)] + [InlineData(ProcessorCase.Median)] + [InlineData(ProcessorCase.MedianPreserveAlpha)] + [InlineData(ProcessorCase.BokehBlur)] + [InlineData(ProcessorCase.OilPaint)] + [InlineData(ProcessorCase.HistogramGlobal)] + [InlineData(ProcessorCase.HistogramAdaptiveSlidingWindow)] + [InlineData(ProcessorCase.HistogramAdaptiveTileInterpolation)] + [InlineData(ProcessorCase.HistogramAutoLevel)] + [InlineData(ProcessorCase.HistogramAutoLevelSeparateChannels)] + [InlineData(ProcessorCase.ResizeCompanded)] + [InlineData(ProcessorCase.ResizeUnassociated)] + [InlineData(ProcessorCase.AffineTransform)] + [InlineData(ProcessorCase.ProjectiveTransform)] + [InlineData(ProcessorCase.OrderedDither)] + [InlineData(ProcessorCase.EntropyCrop)] + public void EquivalentAlphaRepresentationsProduceEquivalentResults(ProcessorCase processor) + { + using Image unassociated = processor == ProcessorCase.EntropyCrop ? CreateEntropyCropImage() : CreateTestImage(); + using Image associated = CreateAssociatedImage(unassociated); + + ApplyProcessor(unassociated, processor); + ApplyProcessor(associated, processor); + + AssertEquivalent(unassociated, associated); + } + + /// + /// Verifies the native-range bulk hooks required by the public associated-alpha operation contract. + /// + [Fact] + public void ScaledAssociatedPixelNativeBulkConversionsMatchScalarConversions() + { + const int length = 259; + ScaledRgbaVectorP pixel = ScaledRgbaVectorP.FromUnassociatedScaledVector4(new Vector4(.8F, .4F, .2F, .5F)); + ScaledRgbaVectorP[] pixels = new ScaledRgbaVectorP[length]; + Vector4[] expectedVectors = new Vector4[length]; + Vector4[] actualVectors = new Vector4[length]; + ScaledRgbaVectorP[] expectedPixels = new ScaledRgbaVectorP[length]; + ScaledRgbaVectorP[] actualPixels = new ScaledRgbaVectorP[length]; + PixelOperations operations = PixelOperations.Instance; + + Array.Fill(pixels, pixel); + + for (int i = 0; i < length; i++) + { + expectedVectors[i] = pixels[i].ToUnassociatedVector4(); + } + + operations.ToVector4(Configuration.Default, pixels, actualVectors, PixelConversionModifiers.UnPremultiply); + Assert.Equal(expectedVectors, actualVectors); + + for (int i = 0; i < length; i++) + { + expectedVectors[i] = pixels[i].ToAssociatedVector4(); + } + + operations.ToVector4(Configuration.Default, pixels, actualVectors, PixelConversionModifiers.Premultiply); + Assert.Equal(expectedVectors, actualVectors); + + for (int i = 0; i < length; i++) + { + expectedVectors[i] = pixels[i].ToUnassociatedVector4(); + expectedPixels[i] = ScaledRgbaVectorP.FromUnassociatedVector4(expectedVectors[i]); + } + + operations.FromVector4Destructive(Configuration.Default, expectedVectors, actualPixels, PixelConversionModifiers.UnPremultiply); + Assert.Equal(expectedPixels, actualPixels); + + for (int i = 0; i < length; i++) + { + expectedVectors[i] = pixels[i].ToAssociatedVector4(); + expectedPixels[i] = ScaledRgbaVectorP.FromAssociatedVector4(expectedVectors[i]); + } + + operations.FromVector4Destructive(Configuration.Default, expectedVectors, actualPixels, PixelConversionModifiers.Premultiply); + Assert.Equal(expectedPixels, actualPixels); + } + + [Fact] + public void ErrorDiffusionUsesLogicalUnassociatedError() + { + Vector4 background = new(.25F, .25F, .25F, .5F); + using Image unassociated = new(4, 4, RgbaVector.FromScaledVector4(background)); + using Image associated = CreateAssociatedImage(unassociated); + + Vector4 source = new(.5F, .5F, .5F, .5F); + Vector4 transformed = new(.25F, .25F, .25F, .5F); + RgbaVector unassociatedSource = RgbaVector.FromScaledVector4(source); + RgbaVector unassociatedTransformed = RgbaVector.FromScaledVector4(transformed); + ScaledRgbaVectorP associatedSource = ScaledRgbaVectorP.FromUnassociatedScaledVector4(source); + ScaledRgbaVectorP associatedTransformed = ScaledRgbaVectorP.FromUnassociatedScaledVector4(transformed); + + ErrorDither.FloydSteinberg.Dither(unassociated.Frames.RootFrame, unassociated.Bounds, unassociatedSource, unassociatedTransformed, 1, 1, 1F); + ErrorDither.FloydSteinberg.Dither(associated.Frames.RootFrame, associated.Bounds, associatedSource, associatedTransformed, 1, 1, 1F); + + AssertEquivalent(unassociated, associated); + } + + [Fact] + public void AffineTransformFractionalEdgesMatchNormalPixelConversion() + { + using Image rgba = new(4, 4, new Rgba32(255, 64, 16, 255)); + using Image bgra = rgba.CloneAs(); + using Image rgb = rgba.CloneAs(); + + ApplyProcessor(rgba, ProcessorCase.AffineTransform); + ApplyProcessor(bgra, ProcessorCase.AffineTransform); + ApplyProcessor(rgb, ProcessorCase.AffineTransform); + + // Alpha-less output drops fractional coverage after unassociation, just like a normal pixel-format conversion. + using Image expectedBgra = rgba.CloneAs(); + using Image expectedRgb = rgba.CloneAs(); + + Assert.Equal(rgba.Size, bgra.Size); + Assert.Equal(rgba.Size, rgb.Size); + + bool foundFractionalCoverage = false; + for (int y = 0; y < rgba.Height; y++) + { + for (int x = 0; x < rgba.Width; x++) + { + Rgba32 pixel = rgba[x, y]; + + if (pixel.A is > 0 and < 255) + { + foundFractionalCoverage = true; + Assert.Equal(new Rgba32(255, 64, 16, pixel.A), pixel); + } + + Assert.Equal(expectedBgra[x, y], bgra[x, y]); + Assert.Equal(expectedRgb[x, y], rgb[x, y]); + } + } + + Assert.True(foundFractionalCoverage); + } + + [Fact] + public void ConvolutionDoesNotObserveColorBehindZeroAlpha() + { + using Image hiddenColor = new(3, 1); + using Image transparentBlack = new(3, 1); + + hiddenColor[0, 0] = new RgbaVector(1, 0, 0, 0); + hiddenColor[1, 0] = new RgbaVector(0, 0, 1, 1); + hiddenColor[2, 0] = new RgbaVector(1, 0, 0, 0); + transparentBlack[1, 0] = hiddenColor[1, 0]; + + ApplyProcessor(hiddenColor, ProcessorCase.Convolution); + ApplyProcessor(transparentBlack, ProcessorCase.Convolution); + + // Associated filtering makes fully transparent RGB unobservable before the kernel is evaluated. + for (int x = 0; x < hiddenColor.Width; x++) + { + Assert.Equal(transparentBlack[x, 0], hiddenColor[x, 0]); + } + } + + private static Image CreateTestImage() + { + Image image = new(8, 8); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + float alpha = ((x + (2 * y)) % 3) switch + { + 0 => .25F, + 1 => .5F, + _ => 1F + }; + + // Dyadic components and alpha make the initial association round trip exact in binary floating point. + float red = ((x + y) & 7) / 8F; + float green = (((3 * x) + y + 1) & 7) / 8F; + float blue = ((x + (5 * y) + 2) & 7) / 8F; + image[x, y] = new RgbaVector(red, green, blue, alpha); + } + } + + return image; + } + + private static Image CreateEntropyCropImage() + { + Image image = new(8, 8, new RgbaVector(0, 0, 0, 1)); + + for (int y = 2; y < 6; y++) + { + for (int x = 2; x < 6; x++) + { + image[x, y] = new RgbaVector(1, 1, 1, 1); + } + } + + return image; + } + + private static Image CreateAssociatedImage(Image source) + { + Image image = new(source.Width, source.Height); + + for (int y = 0; y < source.Height; y++) + { + for (int x = 0; x < source.Width; x++) + { + Vector4 vector = source[x, y].ToScaledVector4(); + Numerics.Premultiply(ref vector); + image[x, y] = ScaledRgbaVectorP.FromScaledVector4(vector); + } + } + + return image; + } + + private static void ApplyProcessor(Image image, ProcessorCase processor) + where TPixel : unmanaged, IPixel + { + if (processor == ProcessorCase.Opaque) + { + using OpaqueProcessor opaque = new(image.Configuration, image, image.Bounds); + opaque.Apply(image.Frames.RootFrame); + return; + } + + if (processor is ProcessorCase.Convolution2D or ProcessorCase.Convolution2DPreserveAlpha) + { + DenseMatrix kernelX = new float[,] + { + { -.25F, 0, .25F }, + { -.5F, 0, .5F }, + { -.25F, 0, .25F } + }; + DenseMatrix kernelY = new float[,] + { + { -.25F, -.5F, -.25F }, + { 0, 0, 0 }, + { .25F, .5F, .25F } + }; + + using Convolution2DProcessor convolution = new( + image.Configuration, + kernelX, + kernelY, + processor == ProcessorCase.Convolution2DPreserveAlpha, + image, + image.Bounds); + + convolution.Apply(image.Frames.RootFrame); + return; + } + + if (processor is ProcessorCase.Convolution2Pass or ProcessorCase.Convolution2PassPreserveAlpha) + { + float[] kernel = [.25F, .5F, .25F]; + using Convolution2PassProcessor convolution = new( + image.Configuration, + kernel, + processor == ProcessorCase.Convolution2PassPreserveAlpha, + image, + image.Bounds, + BorderWrappingMode.Repeat, + BorderWrappingMode.Repeat); + + convolution.Apply(image.Frames.RootFrame); + return; + } + + image.Mutate(context => + { + switch (processor) + { + case ProcessorCase.Invert: + context.Invert(); + break; + case ProcessorCase.ColorMatrix: + ColorMatrix matrix = new() + { + M11 = .5F, + M22 = .5F, + M33 = .5F, + M41 = .25F, + M42 = .25F, + M43 = .25F, + M14 = .125F, + M24 = .125F, + M34 = .125F, + M44 = .5F + }; + + context.Filter(matrix); + break; + case ProcessorCase.Convolution: + context.Convolve(new float[,] { { .25F, .5F, .25F } }); + break; + case ProcessorCase.ConvolutionPreserveAlpha: + context.Convolve(new float[,] { { .25F, .5F, .25F } }, true); + break; + case ProcessorCase.Median: + context.MedianBlur(1, false); + break; + case ProcessorCase.MedianPreserveAlpha: + context.MedianBlur(1, true); + break; + case ProcessorCase.BokehBlur: + context.BokehBlur(2, 2, 2F); + break; + case ProcessorCase.OilPaint: + context.OilPaint(4, 3); + break; + case ProcessorCase.HistogramGlobal: + context.HistogramEqualization(CreateHistogramOptions(HistogramEqualizationMethod.Global)); + break; + case ProcessorCase.HistogramAdaptiveSlidingWindow: + context.HistogramEqualization(CreateHistogramOptions(HistogramEqualizationMethod.AdaptiveSlidingWindow)); + break; + case ProcessorCase.HistogramAdaptiveTileInterpolation: + context.HistogramEqualization(CreateHistogramOptions(HistogramEqualizationMethod.AdaptiveTileInterpolation)); + break; + case ProcessorCase.HistogramAutoLevel: + context.HistogramEqualization(CreateHistogramOptions(HistogramEqualizationMethod.AutoLevel)); + break; + case ProcessorCase.HistogramAutoLevelSeparateChannels: + HistogramEqualizationOptions options = CreateHistogramOptions(HistogramEqualizationMethod.AutoLevel); + options.SyncChannels = false; + context.HistogramEqualization(options); + break; + case ProcessorCase.ResizeCompanded: + context.Resize(new ResizeOptions + { + Size = new Size(5, 5), + Sampler = KnownResamplers.Box, + Compand = true, + PremultiplyAlpha = true + }); + break; + case ProcessorCase.ResizeUnassociated: + context.Resize(new ResizeOptions + { + Size = new Size(5, 5), + Sampler = KnownResamplers.Box, + PremultiplyAlpha = false + }); + break; + case ProcessorCase.AffineTransform: + context.Transform(image.Bounds, Matrix3x2.CreateTranslation(.5F, .5F), image.Size, KnownResamplers.Bicubic); + break; + case ProcessorCase.ProjectiveTransform: + context.Transform(image.Bounds, Matrix4x4.CreateTranslation(.5F, .5F, 0), image.Size, KnownResamplers.Bicubic); + break; + case ProcessorCase.OrderedDither: + context.Dither(KnownDitherings.Bayer4x4); + break; + case ProcessorCase.EntropyCrop: + context.EntropyCrop(.5F); + break; + } + }); + } + + private static HistogramEqualizationOptions CreateHistogramOptions(HistogramEqualizationMethod method) + => new() + { + Method = method, + LuminanceLevels = 16, + NumberOfTiles = 2 + }; + + private static void AssertEquivalent(Image unassociated, Image associated) + { + // Materialize both representations through the same 16-bit logical-color boundary. Their floating-point storage paths can differ by one rounding operation when associated results are unassociated for RgbaVector storage. + using Image expected = unassociated.CloneAs(); + using Image actual = associated.CloneAs(); + + Assert.Equal(expected.Size, actual.Size); + + for (int y = 0; y < expected.Height; y++) + { + for (int x = 0; x < expected.Width; x++) + { + Assert.Equal(expected[x, y], actual[x, y]); + } + } + } + + public enum ProcessorCase + { + Opaque, + Invert, + ColorMatrix, + Convolution, + ConvolutionPreserveAlpha, + Convolution2D, + Convolution2DPreserveAlpha, + Convolution2Pass, + Convolution2PassPreserveAlpha, + Median, + MedianPreserveAlpha, + BokehBlur, + OilPaint, + HistogramGlobal, + HistogramAdaptiveSlidingWindow, + HistogramAdaptiveTileInterpolation, + HistogramAutoLevel, + HistogramAutoLevelSeparateChannels, + ResizeCompanded, + ResizeUnassociated, + AffineTransform, + ProjectiveTransform, + OrderedDither, + EntropyCrop + } +} diff --git a/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs b/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs index 135212400..d5f7f69d9 100644 --- a/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs +++ b/tests/ImageSharp.Tests/Processing/Filters/BrightnessTest.cs @@ -43,13 +43,14 @@ public class BrightnessTest : BaseImageOperationsExtensionTest Assert.Equal(new Rgb24(20, 20, 20), rgbImage[0, 0]); - Image halfSingleImage = new(Configuration.Default, 100, 100, new HalfSingle(-1)); + // HalfSingle normalizes the complete finite binary16 interval, making -65504 logical zero and -32752 logical .25. + Image halfSingleImage = new(Configuration.Default, 100, 100, new HalfSingle((float)Half.MinValue)); halfSingleImage.Mutate(x => x.ApplyProcessor(new BrightnessProcessor(2))); - Assert.Equal(new HalfSingle(-1), halfSingleImage[0, 0]); + Assert.Equal(new HalfSingle((float)Half.MinValue), halfSingleImage[0, 0]); - halfSingleImage = new Image(Configuration.Default, 100, 100, new HalfSingle(-0.5f)); + halfSingleImage = new Image(Configuration.Default, 100, 100, new HalfSingle(-32752F)); halfSingleImage.Mutate(x => x.ApplyProcessor(new BrightnessProcessor(2))); diff --git a/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs b/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs index 81ba1693d..a0ea99a9d 100644 --- a/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs +++ b/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs @@ -76,7 +76,7 @@ public class IntegralImageTests : BaseImageOperationsExtensionTest Buffer2D integralBuffer, Func getPixel) where TPixel : unmanaged, IPixel - => VerifySumValues(provider, integralBuffer, integralBuffer.Bounds(), getPixel); + => VerifySumValues(provider, integralBuffer, integralBuffer.Bounds, getPixel); private static void VerifySumValues( TestImageProvider provider, diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.cs index 574c98371..38c635004 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.cs @@ -41,6 +41,41 @@ public class ConvolutionProcessorHelpersTest } } + /// + /// Verifies that Gaussian sharpening preserves the scalar kernel formula across scalar and SIMD lengths. + /// + /// The kernel radius. + [Theory] + [InlineData(1)] + [InlineData(3)] + [InlineData(9)] + [InlineData(32)] + [InlineData(80)] + public void VerifyGaussianSharpenKernel(int radius) + { + int kernelSize = (radius * 2) + 1; + float sigma = radius / 3F; + float[] expected = new float[kernelSize]; + float sum = 0F; + + for (int i = 0; i < kernelSize; i++) + { + float value = Numerics.Gaussian(i - radius, sigma); + expected[i] = value; + sum += value; + } + + for (int i = 0; i < kernelSize; i++) + { + expected[i] = i == radius ? (2F * sum) - expected[i] : -expected[i]; + expected[i] /= sum; + } + + float[] actual = ConvolutionProcessorHelpers.CreateGaussianSharpenKernel(kernelSize, sigma); + + Assert.Equal(expected, actual); + } + [Fact] public void VerifyNonSeparableMatrix() { diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianBlurTest.cs index 0728bb5d8..f9f10cb70 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/GaussianBlurTest.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Convolution; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution; @@ -13,4 +15,32 @@ public class GaussianBlurTest : Basic1ParameterConvolutionTests protected override void Apply(IImageProcessingContext ctx, int value, Rectangle bounds) => ctx.GaussianBlur(bounds, value); + + [Theory] + [InlineData(BorderWrappingMode.Repeat)] + [InlineData(BorderWrappingMode.Mirror)] + [InlineData(BorderWrappingMode.Bounce)] + [InlineData(BorderWrappingMode.Wrap)] + public void OnImageSmallerThanKernelRadius_LeavesSolidColorUnchanged(BorderWrappingMode mode) + { + // 8 rows against sigma 10 (61-tap kernel, radius 30). A normalized blur of a solid + // image must remain that color no matter how the border sampling folds. + Rgba32 red = new(255, 0, 0); + using Image image = new(130, 8, red); + image.Mutate(x => x.GaussianBlur(image.Bounds, 10F, mode, mode)); + + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + foreach (Rgba32 pixel in accessor.GetRowSpan(y)) + { + Assert.InRange(pixel.R, 254, 255); + Assert.InRange(pixel.G, 0, 1); + Assert.InRange(pixel.B, 0, 1); + Assert.Equal(255, pixel.A); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/KernelSamplingMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/KernelSamplingMapTests.cs new file mode 100644 index 000000000..5194fa001 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/KernelSamplingMapTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution; + +[Trait("Category", "Processors")] +[GroupOutput("Convolution")] +public class KernelSamplingMapTests +{ + public static readonly TheoryData BoundsSmallerThanKernel = new() + { + { BorderWrappingMode.Repeat, 8, 5 }, + { BorderWrappingMode.Mirror, 8, 5 }, + { BorderWrappingMode.Bounce, 8, 5 }, + { BorderWrappingMode.Wrap, 8, 5 }, + { BorderWrappingMode.Repeat, 1, 1 }, + { BorderWrappingMode.Mirror, 1, 1 }, + { BorderWrappingMode.Bounce, 1, 1 }, + { BorderWrappingMode.Wrap, 1, 1 }, + { BorderWrappingMode.Repeat, 30, 30 }, + { BorderWrappingMode.Mirror, 30, 30 }, + { BorderWrappingMode.Bounce, 30, 30 }, + { BorderWrappingMode.Wrap, 30, 30 }, + }; + + [Theory] + [MemberData(nameof(BoundsSmallerThanKernel))] + public void BuildSamplingOffsetMap_BoundsSmallerThanKernelRadius_OffsetsStayInBounds(BorderWrappingMode mode, int width, int height) + { + // A 61-tap kernel has radius 30, so these bounds are covered entirely by the border + // regions and offsets can overshoot the bounds by more than one sampling extent. + const int kernelSize = 61; + Rectangle bounds = new(100, 200, width, height); + + using KernelSamplingMap map = new(Configuration.Default.MemoryAllocator); + map.BuildSamplingOffsetMap(kernelSize, kernelSize, bounds, mode, mode); + + foreach (int x in map.GetColumnOffsetSpan()) + { + Assert.InRange(x, bounds.Left, bounds.Right - 1); + } + + foreach (int y in map.GetRowOffsetSpan()) + { + Assert.InRange(y, bounds.Top, bounds.Bottom - 1); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/HexadecatreeQuantizerTests.cs similarity index 65% rename from tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs rename to tests/ImageSharp.Tests/Processing/Processors/Quantization/HexadecatreeQuantizerTests.cs index c9f3daf0f..b154551e2 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/HexadecatreeQuantizerTests.cs @@ -8,37 +8,37 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization; [Trait("Category", "Processors")] -public class OctreeQuantizerTests +public class HexadecatreeQuantizerTests { [Fact] - public void OctreeQuantizerConstructor() + public void HexadecatreeQuantizerConstructor() { QuantizerOptions expected = new() { MaxColors = 128 }; - OctreeQuantizer quantizer = new(expected); + HexadecatreeQuantizer quantizer = new(expected); Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors); Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); expected = new QuantizerOptions { Dither = null }; - quantizer = new OctreeQuantizer(expected); + quantizer = new HexadecatreeQuantizer(expected); Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); Assert.Null(quantizer.Options.Dither); expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson }; - quantizer = new OctreeQuantizer(expected); + quantizer = new HexadecatreeQuantizer(expected); Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 }; - quantizer = new OctreeQuantizer(expected); + quantizer = new HexadecatreeQuantizer(expected); Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors); Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); } [Fact] - public void OctreeQuantizerCanCreateFrameQuantizer() + public void HexadecatreeQuantizerCanCreateFrameQuantizer() { - OctreeQuantizer quantizer = new(); + HexadecatreeQuantizer quantizer = new(); IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); @@ -46,17 +46,31 @@ public class OctreeQuantizerTests Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }); + quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }); frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Null(frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); + quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); } + + [Fact] + public void PaletteUsesNormalizedTransparencyThreshold() + { + using HexadecatreeQuantizer.Hexadecatree tree = new(Configuration.Default, 8, 2, .5F); + tree.AddColors([new Rgba32(255, 255, 255, 127)]); + + Span palette = stackalloc Rgba32[2]; + short paletteIndex = 0; + tree.Palettize(palette, ref paletteIndex); + + Assert.Equal(1, paletteIndex); + Assert.Equal(default, palette[0]); + } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs index f2a4b079b..07e9a4b0d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -75,4 +76,161 @@ public class PaletteQuantizerTests IQuantizer quantizer = KnownQuantizers.Werner; Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); } + + [Fact] + public void ExactColorMatchingMatchesUncachedAfterCacheOverflow() + { + Rgba32[] palette = + [ + new Rgba32(0, 0, 0), + new Rgba32(7, 0, 0) + ]; + + using PixelMap exact = CreatePixelMap(palette); + using PixelMap cachedExact = CreatePixelMap(palette); + + for (int i = 0; i < AccurateCache.Capacity; i++) + { + cachedExact.GetClosestColor(CreateOverflowFillerColor(i), out _); + } + + Rgba32 first = new(1, 0, 0); + Rgba32 second = new(6, 0, 0); + + AssertMatchesUncached(exact, cachedExact, first); + AssertMatchesUncached(exact, cachedExact, second); + } + + [Fact] + public void ExactColorMatchingMatchesUncachedAcrossManyProbeBinsAfterRepeatedEviction() + { + Rgba32[] palette = CreateGrayscalePalette(256); + + using PixelMap exact = CreatePixelMap(palette); + using PixelMap cachedExact = CreatePixelMap(palette); + + for (int i = 0; i < AccurateCache.Capacity * 2; i++) + { + cachedExact.GetClosestColor(CreateEvictionFillerColor(i), out _); + } + + for (int i = 0; i < AccurateCache.Capacity; i++) + { + AssertMatchesUncached(exact, cachedExact, CreateEvictionProbeColor(i)); + } + } + + [Fact] + public void ExactColorMatchingMatchesUncachedForDitherStressColorSequence() + { + Rgba32[] palette = CreateGrayscalePalette(16); + + using Image source = CreateDitherStressImage(); + using PixelMap exact = CreatePixelMap(palette); + using PixelMap cachedExact = CreatePixelMap(palette); + + for (int y = 0; y < source.Height; y++) + { + for (int x = 0; x < source.Width; x++) + { + AssertMatchesUncached(exact, cachedExact, source[x, y]); + } + } + } + + // Split the first 512 integers across R and G so the warmup loop produces 512 distinct exact colors: + // the low 8 bits go into R, and the ninth bit spills into G once R wraps after 255. + // Keeping B fixed and G offset away from zero also avoids accidentally probing the red-axis test colors below. + private static Rgba32 CreateOverflowFillerColor(int i) + => new((byte)i, (byte)(16 + (i >> 8)), 32); + + // Treat i as three packed 5-bit coordinates and expand each coordinate back to an 8-bit channel by + // shifting left by 3. That lands on the lower edge of each 5-bit coarse bucket, giving the test a + // deterministic way to fill many distinct coarse buckets before probing nearby exact colors. + private static Rgba32 CreateEvictionFillerColor(int i) + { + byte r = (byte)((i & 31) << 3); + byte g = (byte)(((i >> 5) & 31) << 3); + byte b = (byte)(((i >> 10) & 31) << 3); + return new(r, g, b); + } + + // Reconstruct the same 5-bit RGB bucket coordinates used by CreateEvictionFillerColor, then set the + // low 3 bits in each channel to 0b111. That keeps the probe inside the same coarse bucket while making + // it a different exact color, which is the shape that used to expose coarse-fallback false hits. + private static Rgba32 CreateEvictionProbeColor(int i) + { + byte r = (byte)(((i & 31) << 3) | 0x07); + byte g = (byte)((((i >> 5) & 31) << 3) | 0x07); + byte b = (byte)((((i >> 10) & 31) << 3) | 0x07); + return new(r, g, b); + } + + private static PixelMap CreatePixelMap(Rgba32[] palette) + where TCache : struct, IColorIndexCache + => new EuclideanPixelMap(Configuration.Default, palette); + + private static void AssertMatchesUncached(PixelMap exact, PixelMap cachedExact, Rgba32 color) + { + int exactIndex = exact.GetClosestColor(color, out Rgba32 exactMatch); + int cachedIndex = cachedExact.GetClosestColor(color, out Rgba32 cachedMatch); + + Assert.Equal(exactIndex, cachedIndex); + Assert.Equal(exactMatch, cachedMatch); + } + + private static Rgba32[] CreateGrayscalePalette(int count) + { + Rgba32[] palette = new Rgba32[count]; + for (int i = 0; i < count; i++) + { + byte value = count == 1 ? (byte)0 : (byte)((i * 255) / (count - 1)); + palette[i] = new Rgba32(value, value, value); + } + + return palette; + } + + // Generate a deterministic pseudo-image where each channel uses a different x/y slope. + // Neighboring pixels stay correlated, like real image content, but the combined RGB values + // churn heavily enough that exact repeats are rare. That makes this a useful stress input + // for verifying cached exact matching against an uncached baseline under dither-like access. + private static Image CreateDitherStressImage() + { + Image image = new(192, 96); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + image[x, y] = new Rgba32( + (byte)((x * 17) + (y * 13)), + (byte)((x * 29) + (y * 7)), + (byte)((x * 11) + (y * 23))); + } + } + + return image; + } + + private readonly struct UncachedCache : IColorIndexCache + { + public static UncachedCache Create(MemoryAllocator allocator) => default; + + public bool TryAdd(Rgba32 color, short value) => true; + + public bool TryGetValue(Rgba32 color, out short value) + { + value = -1; + return false; + } + + public void Clear() + { + } + + public void Dispose() + { + } + } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs index 2ba757c11..00e09d83b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs @@ -74,15 +74,15 @@ public class QuantizerTests = new() { // Known uses error diffusion by default. - KnownQuantizers.Octree, + KnownQuantizers.Hexadecatree, KnownQuantizers.WebSafe, KnownQuantizers.Werner, KnownQuantizers.Wu, - new OctreeQuantizer(NoDitherOptions), + new HexadecatreeQuantizer(NoDitherOptions), new WebSafePaletteQuantizer(NoDitherOptions), new WernerPaletteQuantizer(NoDitherOptions), new WuQuantizer(NoDitherOptions), - new OctreeQuantizer(OrderedDitherOptions), + new HexadecatreeQuantizer(OrderedDitherOptions), new WebSafePaletteQuantizer(OrderedDitherOptions), new WernerPaletteQuantizer(OrderedDitherOptions), new WuQuantizer(OrderedDitherOptions) @@ -91,52 +91,52 @@ public class QuantizerTests public static readonly TheoryData DitherScaleQuantizers = new() { - new OctreeQuantizer(Diffuser0_ScaleDitherOptions), + new HexadecatreeQuantizer(Diffuser0_ScaleDitherOptions), new WebSafePaletteQuantizer(Diffuser0_ScaleDitherOptions), new WernerPaletteQuantizer(Diffuser0_ScaleDitherOptions), new WuQuantizer(Diffuser0_ScaleDitherOptions), - new OctreeQuantizer(Diffuser0_25_ScaleDitherOptions), + new HexadecatreeQuantizer(Diffuser0_25_ScaleDitherOptions), new WebSafePaletteQuantizer(Diffuser0_25_ScaleDitherOptions), new WernerPaletteQuantizer(Diffuser0_25_ScaleDitherOptions), new WuQuantizer(Diffuser0_25_ScaleDitherOptions), - new OctreeQuantizer(Diffuser0_5_ScaleDitherOptions), + new HexadecatreeQuantizer(Diffuser0_5_ScaleDitherOptions), new WebSafePaletteQuantizer(Diffuser0_5_ScaleDitherOptions), new WernerPaletteQuantizer(Diffuser0_5_ScaleDitherOptions), new WuQuantizer(Diffuser0_5_ScaleDitherOptions), - new OctreeQuantizer(Diffuser0_75_ScaleDitherOptions), + new HexadecatreeQuantizer(Diffuser0_75_ScaleDitherOptions), new WebSafePaletteQuantizer(Diffuser0_75_ScaleDitherOptions), new WernerPaletteQuantizer(Diffuser0_75_ScaleDitherOptions), new WuQuantizer(Diffuser0_75_ScaleDitherOptions), - new OctreeQuantizer(DiffuserDitherOptions), + new HexadecatreeQuantizer(DiffuserDitherOptions), new WebSafePaletteQuantizer(DiffuserDitherOptions), new WernerPaletteQuantizer(DiffuserDitherOptions), new WuQuantizer(DiffuserDitherOptions), - new OctreeQuantizer(Ordered0_ScaleDitherOptions), + new HexadecatreeQuantizer(Ordered0_ScaleDitherOptions), new WebSafePaletteQuantizer(Ordered0_ScaleDitherOptions), new WernerPaletteQuantizer(Ordered0_ScaleDitherOptions), new WuQuantizer(Ordered0_ScaleDitherOptions), - new OctreeQuantizer(Ordered0_25_ScaleDitherOptions), + new HexadecatreeQuantizer(Ordered0_25_ScaleDitherOptions), new WebSafePaletteQuantizer(Ordered0_25_ScaleDitherOptions), new WernerPaletteQuantizer(Ordered0_25_ScaleDitherOptions), new WuQuantizer(Ordered0_25_ScaleDitherOptions), - new OctreeQuantizer(Ordered0_5_ScaleDitherOptions), + new HexadecatreeQuantizer(Ordered0_5_ScaleDitherOptions), new WebSafePaletteQuantizer(Ordered0_5_ScaleDitherOptions), new WernerPaletteQuantizer(Ordered0_5_ScaleDitherOptions), new WuQuantizer(Ordered0_5_ScaleDitherOptions), - new OctreeQuantizer(Ordered0_75_ScaleDitherOptions), + new HexadecatreeQuantizer(Ordered0_75_ScaleDitherOptions), new WebSafePaletteQuantizer(Ordered0_75_ScaleDitherOptions), new WernerPaletteQuantizer(Ordered0_75_ScaleDitherOptions), new WuQuantizer(Ordered0_75_ScaleDitherOptions), - new OctreeQuantizer(OrderedDitherOptions), + new HexadecatreeQuantizer(OrderedDitherOptions), new WebSafePaletteQuantizer(OrderedDitherOptions), new WernerPaletteQuantizer(OrderedDitherOptions), new WuQuantizer(OrderedDitherOptions), diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 023d47886..4214cc5e0 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.ColorProfiles.Companding; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -228,6 +229,72 @@ public class ResizeTests appendSourceFileOrDescription: false); } + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Resize_AppliesAlphaOptionsForUnassociatedPixels(bool compand, bool premultiplyAlpha) + => AssertResizeAlphaOptions(compand, premultiplyAlpha); + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Resize_AppliesAlphaOptionsForAssociatedPixels(bool compand, bool premultiplyAlpha) + => AssertResizeAlphaOptions(compand, premultiplyAlpha); + + private static void AssertResizeAlphaOptions(bool compand, bool premultiplyAlpha) + where TPixel : unmanaged, IPixel + { + using Image image = new(2, 1); + image[0, 0] = TPixel.FromUnassociatedScaledVector4(new Vector4(0.9F, 0.2F, 0.1F, 0.8F)); + image[1, 0] = TPixel.FromUnassociatedScaledVector4(new Vector4(0.1F, 0.8F, 0.6F, 0.2F)); + + // Read the stored pixels back through the scalar contract so the oracle includes the source format's actual quantization. + Vector4 expectedVector = image[0, 0].ToUnassociatedScaledVector4(); + Vector4 second = image[1, 0].ToUnassociatedScaledVector4(); + + if (compand) + { + expectedVector = SRgbCompanding.Expand(expectedVector); + second = SRgbCompanding.Expand(second); + } + + if (premultiplyAlpha) + { + Numerics.Premultiply(ref expectedVector); + Numerics.Premultiply(ref second); + } + + expectedVector = (expectedVector + second) / 2F; + + if (premultiplyAlpha) + { + Numerics.UnPremultiply(ref expectedVector); + } + + if (compand) + { + expectedVector = SRgbCompanding.Compress(expectedVector); + } + + TPixel expected = TPixel.FromUnassociatedScaledVector4(expectedVector); + ResizeOptions options = new() + { + Size = new Size(1, 1), + Mode = ResizeMode.Stretch, + Sampler = KnownResamplers.Box, + Compand = compand, + PremultiplyAlpha = premultiplyAlpha + }; + + image.Mutate(context => context.Resize(options)); + + Assert.Equal(expected, image[0, 0]); + } + [Theory] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] public void Resize_IsAppliedToAllFrames(TestImageProvider provider) @@ -632,6 +699,20 @@ public class ResizeTests appendSourceFileOrDescription: false); } + [Theory] + [InlineData(1024)] + [InlineData(2048)] + public void Issue3156_CanResizeLargeSquareToSinglePixel(int length) + { + Rgba32 white = Color.White.ToPixel(); + using Image image = new(length, length, white); + + image.Mutate(x => x.Resize(1, 1)); + + Assert.Equal(new Size(1, 1), image.Size); + Assert.Equal(white, image[0, 0]); + } + [Theory] [WithTestPatternImages(100, 100, PixelTypes.Rgba32)] public void ResizeUpdatesSubject(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs index 232571d4d..d0e4cf566 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs @@ -58,6 +58,9 @@ public class AffineTransformBuilderTests : TransformBuilderTestBase builder.PrependTranslation(translate); + protected override void ClearBuilder(AffineTransformBuilder builder) + => builder.Clear(); + protected override Vector2 Execute( AffineTransformBuilder builder, Rectangle rectangle, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs index a0380033f..dcc653316 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs @@ -55,6 +55,9 @@ public class ProjectiveTransformBuilderTests : TransformBuilderTestBase builder.PrependRotationRadians(radians, origin); + protected override void ClearBuilder(ProjectiveTransformBuilder builder) + => builder.Clear(); + protected override Vector2 Execute( ProjectiveTransformBuilder builder, Rectangle rectangle, diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 73215b1c6..12f8326fc 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -248,6 +248,48 @@ public abstract class TransformBuilderTestBase }); } + [Fact] + public void Clear_ResetsBuilderToIdentity() + { + Size size = new(100, 100); + Rectangle rectangle = new(Point.Empty, size); + Vector2 source = new(10, 20); + + TBuilder builder = this.CreateBuilder(); + + // Apply a transform that changes the point. + this.AppendScale(builder, new SizeF(2, 3)); + this.AppendTranslation(builder, new PointF(50, 50)); + Vector2 transformed = this.Execute(builder, rectangle, source); + Assert.NotEqual(source, transformed, Comparer); + + // Clear and verify the builder produces identity behavior. + this.ClearBuilder(builder); + Vector2 afterClear = this.Execute(builder, rectangle, source); + Assert.Equal(source, afterClear, Comparer); + } + + [Fact] + public void Clear_AllowsReuse() + { + Size size = new(100, 100); + Rectangle rectangle = new(Point.Empty, size); + Vector2 source = new(10, 20); + + TBuilder builder = this.CreateBuilder(); + + // First transform: scale by 2. + this.AppendScale(builder, new SizeF(2, 2)); + Vector2 scaled = this.Execute(builder, rectangle, source); + Assert.Equal(new Vector2(20, 40), scaled, Comparer); + + // Clear and apply a different transform: translate. + this.ClearBuilder(builder); + this.AppendTranslation(builder, new PointF(5, 10)); + Vector2 translated = this.Execute(builder, rectangle, source); + Assert.Equal(new Vector2(15, 30), translated, Comparer); + } + protected abstract TBuilder CreateBuilder(); protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); @@ -282,5 +324,7 @@ public abstract class TransformBuilderTestBase protected abstract void PrependTranslation(TBuilder builder, PointF translate); + protected abstract void ClearBuilder(TBuilder builder); + protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); } diff --git a/tests/ImageSharp.Tests/Quantization/AlphaAssociationQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/AlphaAssociationQuantizerTests.cs new file mode 100644 index 000000000..ddad67b94 --- /dev/null +++ b/tests/ImageSharp.Tests/Quantization/AlphaAssociationQuantizerTests.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Dithering; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Tests.Quantization; + +public class AlphaAssociationQuantizerTests +{ + public static TheoryData Dithers { get; } = new() + { + null, + KnownDitherings.Bayer4x4, + KnownDitherings.FloydSteinberg + }; + + [Theory] + [MemberData(nameof(Dithers))] + public void WuQuantizerProducesEquivalentResultsForAssociatedPixels(IDither? dither) + { + byte[] alphaValues = [0, 1, 63, 64, 65, 127, 128, 254, 255]; + Rgba32[] source = new Rgba32[alphaValues.Length * 4]; + + for (int i = 0; i < source.Length; i++) + { + byte alpha = alphaValues[i % alphaValues.Length]; + source[i] = alpha == 0 ? default : new Rgba32(255, 255, 255, alpha); + } + + QuantizerOptions options = new() + { + Dither = dither, + MaxColors = 8, + TransparencyThreshold = .5F + }; + + Rgba32[] expected = Quantize(source, options); + Rgba32[] actual = Quantize(source, options); + + Assert.Equal(expected, actual); + } + + [Fact] + public void PaletteQuantizerAppliesTransparencyThresholdToAssociatedPixels() + { + Configuration configuration = Configuration.Default; + Rgba32P source = new(127, 0, 0, 127); + Color[] palette = [Color.Transparent, Color.FromPixel(source)]; + QuantizerOptions options = new() { Dither = null, TransparencyThreshold = .5F }; + PaletteQuantizer quantizer = new(palette, options); + + using Image image = new(configuration, 1, 1, source); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(configuration); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds); + + Assert.Equal(0, result.DangerousGetRowSpan(0)[0]); + Assert.Equal(default, result.Palette.Span[0]); + } + + [Theory] + [InlineData(64 / 255F, (byte)63, (byte)64)] + [InlineData(.5F, (byte)127, (byte)128)] + [InlineData(1F, (byte)254, (byte)255)] + public void WuQuantizerAppliesNormalizedTransparencyThresholdToAssociatedPixels(float threshold, byte below, byte retained) + { + Rgba32[] source = + [ + new Rgba32(255, 255, 255, below), + new Rgba32(255, 255, 255, retained) + ]; + + QuantizerOptions options = new() + { + Dither = null, + MaxColors = 2, + TransparencyThreshold = threshold + }; + + Rgba32[] expected = Quantize(source, options); + Rgba32[] actual = Quantize(source, options); + + Assert.Equal(expected, actual); + Assert.Equal(default, actual[0]); + Assert.NotEqual(0, actual[1].A); + } + + private static Rgba32[] Quantize(Rgba32[] source, QuantizerOptions options) + where TPixel : unmanaged, IPixel + { + Configuration configuration = Configuration.Default; + using Image image = new(configuration, source.Length, 1); + + for (int x = 0; x < source.Length; x++) + { + image[x, 0] = TPixel.FromRgba32(source[x]); + } + + WuQuantizer quantizer = new(options); + ImageFrame frame = image.Frames.RootFrame; + + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(configuration); + using IndexedImageFrame result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); + + ReadOnlySpan palette = result.Palette.Span; + ReadOnlySpan indices = result.DangerousGetRowSpan(0); + Rgba32[] colors = new Rgba32[source.Length]; + + for (int x = 0; x < colors.Length; x++) + { + colors[x] = palette[indices[x]].ToRgba32(); + } + + return colors; + } +} diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index d832136a9..e94088652 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -15,12 +15,12 @@ public class QuantizedImageTests { WernerPaletteQuantizer werner = new(); WebSafePaletteQuantizer webSafe = new(); - OctreeQuantizer octree = new(); + HexadecatreeQuantizer hexadecatree = new(); WuQuantizer wu = new(); Assert.NotNull(werner.Options.Dither); Assert.NotNull(webSafe.Options.Dither); - Assert.NotNull(octree.Options.Dither); + Assert.NotNull(hexadecatree.Options.Dither); Assert.NotNull(wu.Options.Dither); using (IQuantizer quantizer = werner.CreatePixelSpecificQuantizer(this.Configuration)) @@ -33,7 +33,7 @@ public class QuantizedImageTests Assert.NotNull(quantizer.Options.Dither); } - using (IQuantizer quantizer = octree.CreatePixelSpecificQuantizer(this.Configuration)) + using (IQuantizer quantizer = hexadecatree.CreatePixelSpecificQuantizer(this.Configuration)) { Assert.NotNull(quantizer.Options.Dither); } @@ -47,7 +47,7 @@ public class QuantizedImageTests [Theory] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32, true)] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32, false)] - public void OctreeQuantizerYieldsCorrectTransparentPixel( + public void HexadecatreeQuantizerYieldsCorrectTransparentPixel( TestImageProvider provider, bool dither) where TPixel : unmanaged, IPixel @@ -60,7 +60,7 @@ public class QuantizedImageTests options.Dither = null; } - OctreeQuantizer quantizer = new(options); + HexadecatreeQuantizer quantizer = new(options); foreach (ImageFrame frame in image.Frames) { @@ -103,8 +103,8 @@ public class QuantizedImageTests where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - OctreeQuantizer octreeQuantizer = new(); - IQuantizer quantizer = octreeQuantizer.CreatePixelSpecificQuantizer(Configuration.Default, new QuantizerOptions { MaxColors = 128 }); + HexadecatreeQuantizer hexadecatreeQuantizer = new(); + IQuantizer quantizer = hexadecatreeQuantizer.CreatePixelSpecificQuantizer(Configuration.Default, new QuantizerOptions { MaxColors = 128 }); ImageFrame frame = image.Frames[0]; quantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); } diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index 28bfcfaa8..a3de7b3fe 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -271,6 +271,18 @@ public class TestFormat : IImageFormatConfigurationModule, IImageFormat public readonly Vector4 ToVector4() => default; + /// + public readonly Vector4 ToUnassociatedScaledVector4() => default; + + /// + public readonly Vector4 ToAssociatedScaledVector4() => default; + + /// + public readonly Vector4 ToUnassociatedVector4() => default; + + /// + public readonly Vector4 ToAssociatedVector4() => default; + public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( PixelComponentInfo.Create(2, 8, 8), @@ -283,6 +295,18 @@ public class TestFormat : IImageFormatConfigurationModule, IImageFormat public static TestPixelForAgnosticDecode FromVector4(Vector4 vector) => default; + /// + public static TestPixelForAgnosticDecode FromUnassociatedScaledVector4(Vector4 source) => default; + + /// + public static TestPixelForAgnosticDecode FromAssociatedScaledVector4(Vector4 source) => default; + + /// + public static TestPixelForAgnosticDecode FromUnassociatedVector4(Vector4 source) => default; + + /// + public static TestPixelForAgnosticDecode FromAssociatedVector4(Vector4 source) => default; + public static TestPixelForAgnosticDecode FromAbgr32(Abgr32 source) => default; public static TestPixelForAgnosticDecode FromArgb32(Argb32 source) => default; diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 6ba35f41b..2622a0fb9 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -76,8 +76,12 @@ public static class TestImages public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png"; public const string FrameOffset = "Png/animated/frame-offset.png"; public const string DefaultNotAnimated = "Png/animated/default-not-animated.png"; + public const string AnimatedFrameCount = "Png/animated/issue-animated-frame-count.png"; public const string Issue2666 = "Png/issues/Issue_2666.png"; public const string Issue2882 = "Png/issues/Issue_2882.png"; + public const string Issue396Dragon = "Png/issues/Issue_396_dragon.png"; + public const string Issue396Hand1 = "Png/issues/Issue_396_hand_1.png"; + public const string Issue396Hand2 = "Png/issues/Issue_396_hand_2.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; @@ -176,6 +180,21 @@ public static class TestImages public const string PerceptualcLUTOnly = "Png/icc-profiles/Perceptual-cLUT-only.png"; } + public static class Cgbi + { + public const string Colors = "Png/cgbi/colors.png"; + public const string Clocks = "Png/cgbi/clocks.png"; + public const string Flecks = "Png/cgbi/flecks.png"; + public const string Screen = "Png/cgbi/screen.png"; + + // Issue 410: https://github.com/SixLabors/ImageSharp/issues/410 + public const string Issue410 = "Png/issues/Issue_410.png"; + + // Synthetic fixtures derived from colors.png to exercise CgBI validation. + public const string BitDepth16 = "Png/cgbi/colors-cgbi-bitdepth16.png"; + public const string Palette = "Png/cgbi/colors-cgbi-palette.png"; + } + public static class Bad { public const string MissingDataChunk = "Png/xdtn0g01.png"; @@ -198,9 +217,6 @@ public static class TestImages // Issue 1047: https://github.com/SixLabors/ImageSharp/issues/1047 public const string Issue1047_BadEndChunk = "Png/issues/Issue_1047.png"; - // Issue 410: https://github.com/SixLabors/ImageSharp/issues/410 - public const string Issue410_MalformedApplePng = "Png/issues/Issue_410.png"; - // Bad bit depth. public const string BitDepthZero = "Png/xd0n2c08.png"; public const string BitDepthThree = "Png/xd3n2c08.png"; @@ -232,6 +248,7 @@ public static class TestImages public const string SRgbGray = "Jpg/icc-profiles/sRGB_Gray.jpg"; public const string Perceptual = "Jpg/icc-profiles/Perceptual.jpg"; public const string PerceptualcLUTOnly = "Jpg/icc-profiles/Perceptual-cLUT-only.jpg"; + public const string Issue3064 = "Jpg/icc-profiles/issue-3064.jpg"; } public static class Progressive @@ -359,6 +376,7 @@ public static class TestImages public const string Issue2758 = "Jpg/issues/issue-2758.jpg"; public const string Issue2857 = "Jpg/issues/issue-2857-subsub-ifds.jpg"; public const string Issue2948 = "Jpg/issues/issue-2948-sos.jpg"; + public const string Issue3118 = "Jpg/issues/issue3118-multiple-sof.jpg"; public static class Fuzz { @@ -600,6 +618,7 @@ public static class TestImages public const string Issue2859_B = "Gif/issues/issue_2859_B.gif"; public const string Issue2953 = "Gif/issues/issue_2953.gif"; public const string Issue2980 = "Gif/issues/issue_2980.gif"; + public const string Issue3142 = "Gif/issues/issue_3142.gif"; } public static readonly string[] Animated = @@ -621,7 +640,8 @@ public static class TestImages Issues.BadDescriptorWidth, Issues.Issue1530, Bit18RGBCube, - Global256NoTrans + Global256NoTrans, + Issues.Issue3142 ]; } @@ -1386,4 +1406,23 @@ public static class TestImages public const string MultiFramesInEveryIconChunk = "Ani/aero_busy.ani"; public const string Help = "Ani/Help.ani"; } + + public static class Exr + { + public const string Benchmark = "Exr/Calliphora_benchmark.exr"; + public const string Uncompressed = "Exr/Calliphora_uncompressed.exr"; + public const string UncompressedRgba = "Exr/Calliphora_uncompressed_rgba.exr"; + public const string UncompressedFloatRgb = "Exr/Calliphora_float_uncompressed.exr"; + public const string UncompressedUintRgb = "Exr/Calliphora_uint32_uncompressed.exr"; + public const string UintRgba = "Exr/rgba_uint_uncompressed.exr"; + public const string Zip = "Exr/Calliphora_zip.exr"; + public const string Zips = "Exr/Calliphora_zips.exr"; + public const string Rle = "Exr/Calliphora_rle.exr"; + public const string B44 = "Exr/Calliphora_b44.exr"; + public const string Pxr24Half = "Exr/Calliphora_half_pxr24.exr"; + public const string Pxr24Float = "Exr/Calliphora_float_pxr24.exr"; + public const string Pxr24Uint = "Exr/Calliphora_uint_pxr24.exr"; + public const string Rgb = "Exr/Calliphora_rgb.exr"; + public const string Gray = "Exr/Calliphora_gray.exr"; + } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs index 21ac6966b..7c45dd047 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs @@ -15,7 +15,8 @@ internal readonly struct ApproximateFloatComparer : IEqualityComparer, IEqualityComparer, IEqualityComparer, - IEqualityComparer> + IEqualityComparer>, + IEqualityComparer> { private readonly float epsilon; @@ -78,4 +79,19 @@ internal readonly struct ApproximateFloatComparer : && this.Equals(x.GetElement(7), y.GetElement(7)); public int GetHashCode([DisallowNull] Vector256 obj) => obj.GetHashCode(); + + public bool Equals(Vector512 x, Vector512 y) + { + for (int i = 0; i < Vector512.Count; i++) + { + if (!this.Equals(x.GetElement(i), y.GetElement(i))) + { + return false; + } + } + + return true; + } + + public int GetHashCode([DisallowNull] Vector512 obj) => obj.GetHashCode(); } diff --git a/tests/ImageSharp.Tests/TestUtilities/CorruptedMetadataImageFactory.cs b/tests/ImageSharp.Tests/TestUtilities/CorruptedMetadataImageFactory.cs new file mode 100644 index 000000000..c9d18510d --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/CorruptedMetadataImageFactory.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestDataIcc; + +namespace SixLabors.ImageSharp.Tests.TestUtilities; + +/// +/// Creates encoded images with malformed metadata payloads while keeping the surrounding container valid. +/// +internal static class CorruptedMetadataImageFactory +{ + /// + /// Creates an encoded single-pixel image with a malformed ICC profile. + /// + /// The encoder used to produce the image bytes. + /// The encoded image bytes with a malformed ICC profile payload. + public static byte[] CreateImageWithMalformedIccProfile(IImageEncoder encoder) + { + using Image image = new(1, 1); + image[0, 0] = new Rgba32(255, 0, 0, 255); + image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.HeaderInvalidSizeSmallArray); + + using MemoryStream stream = new(); + image.Save(stream, encoder); + + return stream.ToArray(); + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs index d3671abd4..4cd30fc57 100644 --- a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs +++ b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs @@ -439,7 +439,7 @@ public enum HwIntrinsics : long DisableSSE42 = 1L << 1, DisableAVX = 1L << 2, DisableAVX2 = 1L << 3, - DisableAVX512 = 1L << 4, + DisableAVX512F = 1L << 4, DisableAVX512v2 = 1L << 5, DisableAVX512v3 = 1L << 6, DisableAVX10v1 = 1L << 7, @@ -455,6 +455,7 @@ public enum HwIntrinsics : long DisableVAES = 1L << 17, DisableWAITPKG = 1L << 18, DisableX86Serialize = 1 << 19, + // Arm64 DisableArm64Aes = 1L << 20, DisableArm64Atomics = 1L << 21, @@ -466,6 +467,7 @@ public enum HwIntrinsics : long DisableArm64Sha256 = 1L << 27, DisableArm64Sve = 1L << 28, DisableArm64Sve2 = 1L << 29, + // RISC-V64 DisableRiscV64Zba = 1L << 30, DisableRiscV64Zbb = 1L << 31, diff --git a/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs b/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs index 2a7b42f6b..649da425e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs @@ -6,13 +6,11 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities; public class GraphicsOptionsComparer : IEqualityComparer { public bool Equals(GraphicsOptions x, GraphicsOptions y) - { - return x.AlphaCompositionMode == y.AlphaCompositionMode - && x.Antialias == y.Antialias - && x.AntialiasSubpixelDepth == y.AntialiasSubpixelDepth - && x.BlendPercentage == y.BlendPercentage - && x.ColorBlendingMode == y.ColorBlendingMode; - } + => x.AlphaCompositionMode == y.AlphaCompositionMode + && x.Antialias == y.Antialias + && x.AntialiasThreshold == y.AntialiasThreshold + && x.BlendPercentage == y.BlendPercentage + && x.ColorBlendingMode == y.ColorBlendingMode; public int GetHashCode(GraphicsOptions obj) => obj.GetHashCode(); } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs index 813ed505d..ee1d27471 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs @@ -78,6 +78,6 @@ public abstract partial class TestImageProvider : IXunitSerializable return x < midX ? BottomLeftColor : BottomRightColor; } - private static TPixel GetBottomRightColor() => TPixel.FromScaledVector4(new Vector4(1f, 0f, 1f, 0.5f)); + private static TPixel GetBottomRightColor() => TPixel.FromUnassociatedScaledVector4(new Vector4(1f, 0f, 1f, 0.5f)); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs index 405d048fc..12a7ad4e2 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs @@ -158,15 +158,17 @@ public abstract partial class TestImageProvider : IXunitSerializable int bottom = pixels.Height; int height = (int)Math.Ceiling(pixels.Height / 6f); - Vector4 red = Color.Red.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern - Vector4 green = Color.Green.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern - Vector4 blue = Color.Blue.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern + // Round-trip the primaries through TPixel so this fixture still demonstrates the format's color quantization, while + // keeping the working vectors unassociated because the loop replaces alpha independently of RGB. + Vector4 red = Color.Red.ToPixel().ToUnassociatedScaledVector4(); + Vector4 green = Color.Green.ToPixel().ToUnassociatedScaledVector4(); + Vector4 blue = Color.Blue.ToPixel().ToUnassociatedScaledVector4(); for (int x = left; x < right; x++) { blue.W = red.W = green.W = x / (float)right; - TPixel c = TPixel.FromVector4(red); + TPixel c = TPixel.FromUnassociatedScaledVector4(red); int topBand = top; for (int y = topBand; y < top + height; y++) { @@ -174,14 +176,14 @@ public abstract partial class TestImageProvider : IXunitSerializable } topBand += height; - c = TPixel.FromVector4(green); + c = TPixel.FromUnassociatedScaledVector4(green); for (int y = topBand; y < topBand + height; y++) { pixels[x, y] = c; } topBand += height; - c = TPixel.FromVector4(blue); + c = TPixel.FromUnassociatedScaledVector4(blue); for (int y = topBand; y < bottom; y++) { pixels[x, y] = c; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index 73777ef60..c7883fac1 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -278,7 +278,7 @@ public class ImagingTestCaseUtility where TPixel : unmanaged, IPixel { TPixel pixel = img[x, y]; - Rgba64 rgbaPixel = Rgba64.FromScaledVector4(pixel.ToScaledVector4()); + Rgba64 rgbaPixel = Rgba64.FromUnassociatedScaledVector4(pixel.ToUnassociatedScaledVector4()); ushort change = (ushort)Math.Round((perChannelChange / 255F) * 65535F); if (rgbaPixel.R + perChannelChange <= 255) diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 862d4b64d..d72556b0e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -6,6 +6,7 @@ using ImageMagick; using ImageMagick.Formats; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tiff; @@ -42,6 +43,8 @@ public class MagickReferenceDecoder : ImageDecoder public static MagickReferenceDecoder WebP { get; } = new(WebpFormat.Instance); + public static MagickReferenceDecoder Exr { get; } = new(ExrFormat.Instance); + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { ImageMetadata metadata = new(); @@ -58,14 +61,14 @@ public class MagickReferenceDecoder : ImageDecoder MagickReadSettings settings = new() { - FrameCount = (int)options.MaxFrames + FrameCount = options.MaxFrames }; settings.SetDefines(bmpReadDefines); settings.SetDefines(pngReadDefines); using MagickImageCollection magickImageCollection = new(stream, settings); - int imageWidth = magickImageCollection.Max(x => x.Width); - int imageHeight = magickImageCollection.Max(x => x.Height); + int imageWidth = (int)magickImageCollection.Max(x => x.Width); + int imageHeight = (int)magickImageCollection.Max(x => x.Height); List> framesList = []; foreach (IMagickImage magicFrame in magickImageCollection) @@ -74,10 +77,10 @@ public class MagickReferenceDecoder : ImageDecoder framesList.Add(frame); Buffer2DRegion buffer = frame.PixelBuffer.GetRegion( - imageWidth - magicFrame.Width, - imageHeight - magicFrame.Height, - magicFrame.Width, - magicFrame.Height); + (int)(imageWidth - magicFrame.Width), + (int)(imageHeight - magicFrame.Height), + (int)magicFrame.Width, + (int)magicFrame.Height); using IUnsafePixelCollection pixels = magicFrame.GetPixelsUnsafe(); if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1) @@ -118,6 +121,7 @@ public class MagickReferenceDecoder : ImageDecoder PixelType = metadata.GetDecodedPixelTypeInfo() }; } + private static void FromRgba32Bytes( Configuration configuration, Span rgbaBytes, diff --git a/tests/ImageSharp.Tests/TestUtilities/ScaledRgbaVectorP.cs b/tests/ImageSharp.Tests/TestUtilities/ScaledRgbaVectorP.cs new file mode 100644 index 000000000..aed4ef243 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/ScaledRgbaVectorP.cs @@ -0,0 +1,271 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests; + +/// +/// A test pixel format that stores associated scaled components as 32-bit floating-point values. +/// +/// +/// Its native vector range differs from its scaled range so processor tests also detect missing +/// conversion boundaries without introducing packed-pixel quantization. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct ScaledRgbaVectorP : IPixel +{ + private Vector4 scaled; + + public ScaledRgbaVectorP(Vector4 scaled) => this.scaled = Numerics.Clamp(scaled, Vector4.Zero, Vector4.One); + + public readonly Rgba32 ToRgba32() + { + Vector4 unassociated = this.scaled; + Numerics.UnPremultiply(ref unassociated); + return Rgba32.FromScaledVector4(unassociated); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToScaledVector4() => this.scaled; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToVector4() => (this.scaled * 2F) - Vector4.One; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedScaledVector4() + { + Vector4 vector = this.scaled; + Numerics.UnPremultiply(ref vector); + return vector; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedScaledVector4() => this.ToScaledVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToUnassociatedVector4() + { + Vector4 vector = this.ToUnassociatedScaledVector4(); + + // The native vector is an affine view of the scaled data, so association changes occur before mapping the result. + return (vector * 2F) - Vector4.One; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Vector4 ToAssociatedVector4() => this.ToVector4(); + + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 32, 32, 32, 32), + PixelColorType.RGB | PixelColorType.Alpha, + PixelAlphaRepresentation.Associated); + + public static PixelOperations CreatePixelOperations() => new ScaledPixelOperations(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScaledRgbaVectorP FromScaledVector4(Vector4 source) => new(source); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScaledRgbaVectorP FromVector4(Vector4 source) => new((source + Vector4.One) * .5F); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScaledRgbaVectorP FromAssociatedScaledVector4(Vector4 source) + { + // Clamp logical color before restoring association so out-of-range alpha cannot permanently darken it. + Numerics.UnPremultiply(ref source); + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + Numerics.Premultiply(ref source); + return new(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScaledRgbaVectorP FromUnassociatedVector4(Vector4 source) + { + // The native vector is affine, so map it to scaled space before changing alpha representation. + source = (source + Vector4.One) * .5F; + return FromUnassociatedScaledVector4(source); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScaledRgbaVectorP FromAssociatedVector4(Vector4 source) + { + // The native vector is affine, so map it to scaled space before changing alpha representation. + source = (source + Vector4.One) * .5F; + return FromAssociatedScaledVector4(source); + } + + public static ScaledRgbaVectorP FromAbgr32(Abgr32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromArgb32(Argb32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromBgra5551(Bgra5551 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromBgr24(Bgr24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromBgra32(Bgra32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromL8(L8 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromL16(L16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromLa16(La16 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromLa32(La32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromRgb24(Rgb24 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromRgba32(Rgba32 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromRgb48(Rgb48 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public static ScaledRgbaVectorP FromRgba64(Rgba64 source) => FromUnassociatedScaledVector4(source.ToScaledVector4()); + + public override readonly bool Equals(object obj) => obj is ScaledRgbaVectorP other && this.Equals(other); + + public readonly bool Equals(ScaledRgbaVectorP other) => this.scaled.Equals(other.scaled); + + public override readonly int GetHashCode() => this.scaled.GetHashCode(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ScaledRgbaVectorP FromUnassociatedScaledVector4(Vector4 source) + { + source = Numerics.Clamp(source, Vector4.Zero, Vector4.One); + Numerics.Premultiply(ref source); + return new ScaledRgbaVectorP(source); + } + + /// + /// Provides representation-aware bulk operations for the floating-point test pixel. + /// + private sealed class ScaledPixelOperations : AssociatedAlphaPixelOperations + { + /// + protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Span vectors = destination[..source.Length]; + + // The test pixel contains one Vector4 field, so the cast copies its associated storage without changing representation. + MemoryMarshal.Cast(source).CopyTo(vectors); + Numerics.UnPremultiply(vectors); + + // Association is defined in scaled space, so map the unassociated result to the test format's native [-1, 1] range afterwards. + for (int i = 0; i < vectors.Length; i++) + { + vectors[i] = (vectors[i] * 2F) - Vector4.One; + } + } + + /// + protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Span vectors = destination[..source.Length]; + MemoryMarshal.Cast(source).CopyTo(vectors); + + for (int i = 0; i < vectors.Length; i++) + { + vectors[i] = (vectors[i] * 2F) - Vector4.One; + } + } + + /// + protected override void ToUnassociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Span vectors = destination[..source.Length]; + MemoryMarshal.Cast(source).CopyTo(vectors); + Numerics.UnPremultiply(vectors); + } + + /// + protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + protected override void FromUnassociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + MapNativeToScaled(source); + Numerics.Clamp(MemoryMarshal.Cast(source), 0F, 1F); + Numerics.Premultiply(source); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + protected override void FromAssociatedVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + MapNativeToScaled(source); + Numerics.UnPremultiply(source); + Numerics.Clamp(MemoryMarshal.Cast(source), 0F, 1F); + Numerics.Premultiply(source); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + protected override void FromUnassociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // This test format stores floating-point alpha without quantization, so clamping and premultiplication can use the shared SIMD span operations directly. + Numerics.Clamp(MemoryMarshal.Cast(source), 0F, 1F); + Numerics.Premultiply(source); + + // Source now matches the pixel's sole associated Vector4 field and can be copied without a scalar conversion loop. + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + protected override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + destination = destination[..source.Length]; + + // Recovering the logical color before clamping preserves it when alpha lies outside the scaled range; premultiplication then restores the associated representation. + Numerics.UnPremultiply(source); + Numerics.Clamp(MemoryMarshal.Cast(source), 0F, 1F); + Numerics.Premultiply(source); + MemoryMarshal.Cast(source).CopyTo(destination); + } + + /// + /// Maps vectors from the test format's native numeric range to its scaled range. + /// + /// The vectors to convert in place. + private static void MapNativeToScaled(Span vectors) + { + // Native values use [-1, 1], so the affine inverse maps them to [0, 1] before changing alpha representation. + for (int i = 0; i < vectors.Length; i++) + { + vectors[i] = (vectors[i] + Vector4.One) * .5F; + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index e4bee955b..8a40bd672 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; @@ -63,12 +64,13 @@ public static partial class TestEnvironment new TgaConfigurationModule(), new WebpConfigurationModule(), new TiffConfigurationModule(), + new ExrConfigurationModule(), new QoiConfigurationModule()); IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration(); IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); - // Magick codecs should work on all platforms + // Magick codecs should work on all platforms. cfg.ConfigureCodecs( PngFormat.Instance, MagickReferenceDecoder.Png, diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 4a34529dd..b51e4e2d4 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -505,7 +505,7 @@ public static class TestImageExtensions public static void CompareBuffers(Buffer2D expected, Buffer2D actual) where T : struct, IEquatable { - Assert.True(expected.Size() == actual.Size(), "Buffer sizes are not equal!"); + Assert.True(expected.Size == actual.Size, "Buffer sizes are not equal!"); for (int y = 0; y < expected.Height; y++) { @@ -826,14 +826,17 @@ public static class TestImageExtensions for (int y = rows.Min; y < rows.Max; y++) { Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.bounds.Left, this.bounds.Width); - PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + for (int i = 0; i < span.Length; i++) { ref Vector4 v = ref span[i]; v.W = 1F; } - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan, PixelConversionModifiers.Scale); + // Changing opacity must operate on logical RGB. The representation-aware bulk conversion prevents associated + // formats from retaining the darker RGB values that belonged to the old alpha. + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs index a0ff4a466..f9a4c0a6a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs @@ -37,7 +37,7 @@ internal class TestMemoryAllocator : MemoryAllocator this.returnLog = new List(); } - public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) { T[] array = this.AllocateArray(length, options); return new BasicArrayBuffer(array, length, this); @@ -110,7 +110,7 @@ internal class TestMemoryAllocator : MemoryAllocator /// /// Wraps an array as an instance. /// - private class BasicArrayBuffer : MemoryManager + private class BasicArrayBuffer : AllocationTrackedMemoryManager where T : struct { private readonly TestMemoryAllocator allocator; @@ -159,7 +159,7 @@ internal class TestMemoryAllocator : MemoryAllocator } /// - protected override void Dispose(bool disposing) + protected override void DisposeCore(bool disposing) { if (disposing) { diff --git a/tests/ImageSharp.Tests/TestUtilities/TestPixel.cs b/tests/ImageSharp.Tests/TestUtilities/TestPixel.cs index f0344e2b9..174766daf 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestPixel.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestPixel.cs @@ -35,7 +35,7 @@ public class TestPixel : IXunitSerializable public float Alpha { get; set; } - public TPixel AsPixel() => TPixel.FromScaledVector4(new Vector4(this.Red, this.Green, this.Blue, this.Alpha)); + public TPixel AsPixel() => TPixel.FromUnassociatedScaledVector4(new Vector4(this.Red, this.Green, this.Blue, this.Alpha)); internal Span AsSpan() => new([this.AsPixel()]); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs index 46fb7159e..714dbd223 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs @@ -31,7 +31,7 @@ public class TestUtilityExtensionsTests Vector4 v = new(i, j, 0, 1); v /= 10; - pixels[i, j] = TPixel.FromVector4(v); + pixels[i, j] = TPixel.FromUnassociatedVector4(v); } } diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png new file mode 100644 index 000000000..48281cebc --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15de93db72e2de0ad1a21b67931df57c1a129f062fe1ed3cb7f083761af4fe36 +size 61931 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png new file mode 100644 index 000000000..2e44b7ec5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a568f9f12adc5606e5b74d25a4bf083e2b565d73635704e205400d5a073fcd3 +size 9895 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png new file mode 100644 index 000000000..2e44b7ec5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a568f9f12adc5606e5b74d25a4bf083e2b565d73635704e205400d5a073fcd3 +size 9895 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png new file mode 100644 index 000000000..e91c987b0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c05e0b14a389d11f531d4238900106a81f25a741a55bbbcd160b8b67eb32adb6 +size 7019 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png new file mode 100644 index 000000000..71e2079d0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d577ac117b8260ad08e6f051c949d9187f04699eed0f063476aaf42eada2366 +size 20640 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png new file mode 100644 index 000000000..71e2079d0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d577ac117b8260ad08e6f051c949d9187f04699eed0f063476aaf42eada2366 +size 20640 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png new file mode 100644 index 000000000..7cf41fb5b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53a33da2c1a84c0e8426e1a2ba24e2fe2de17a7bc75760184fb10bf6dc96954a +size 7896 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png new file mode 100644 index 000000000..db7f30b41 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0c97446dec6af009423dcae431c44d23963179c3c3bfbeaf3f3b52022a33f68 +size 2414 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png new file mode 100644 index 000000000..c2959addd --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a651b81764696cd4fe42113d5ac26006f54820d2df8b2103793e82c66597b2b0 +size 2837 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png new file mode 100644 index 000000000..0e52f5332 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55672d405a3d085c72cfc9fb26bc3955bf858e525c54c6c4505f093f011f378d +size 3188 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png new file mode 100644 index 000000000..1c8828256 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e774cba4dda2fe9d3cdff141e7a8c1de7f3e9c8014093abf8697a34e6cc7144 +size 5379 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png new file mode 100644 index 000000000..86efdd207 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44d281c31cd264dcb812df8cb5dc9d5042e915d64e8013af5577dbfea6cbb1cf +size 3955 diff --git a/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp new file mode 100644 index 000000000..7f79c6f3a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fff91e78a87c2b2db661609dd70af2d7a74e6a0de18924ab2b6272e12c60977d +size 9270 diff --git a/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp deleted file mode 100644 index f4ae3b9b6..000000000 --- a/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a98b1ec707af066f77fad7d1a64b858d460986beb6d27682717dd5e221310fd4 -size 9270 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R16_C1_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R16_C1_G3.png index 564a76f90..3b4c12a24 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R16_C1_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R16_C1_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3a6e47d880b7276702e6bf4b1ba1b4781abfa2cd99ee9321a56169440fc318c -size 49844 +oid sha256:0c047a274060b1bf73af9c922bff4f2ea627bd0fad023b3c3a5852f49d026c6b +size 50025 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R16_C2_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R16_C2_G3.png index 4f2dc77b0..7dad93fba 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R16_C2_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R16_C2_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18b4837dceb9a065c5b27133b67ef257a0f050fa1342d02a7e06d3501e068f47 -size 44966 +oid sha256:aa1b0b58ddb6a5c29ccd78f1bc2e773a2005493b92efe5c0deb8056af6fc0208 +size 45605 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R8_C1_G1.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R8_C1_G1.png index 57ce8295e..a9b287c6b 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R8_C1_G1.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_BikeGrayscale_R8_C1_G1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e652706e1aec475c8eec08392d65be1bd888cc79eccc4019eb3826db71c6dac0 -size 54744 +oid sha256:ead63aaaf5b185342f83317c2400b7ada29a82223e35e9a7382f690101ae40f7 +size 54798 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R16_C1_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R16_C1_G3.png index a0e9c1b24..a4ac9718a 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R16_C1_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R16_C1_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffc82427285a2bfeb3226e3e87ac316e41a2a8f853bb406b88ba2ae7bfae099d -size 164923 +oid sha256:025751d22eeeac10878e79d2bf1987e0a055cf9f54903048673189a385036744 +size 157237 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R16_C2_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R16_C2_G3.png index caf152f9b..2c18a147d 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R16_C2_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R16_C2_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ea08065be7271e7ff75ed49e63544701bc14a03bd44d754fb056370559dd105 -size 149483 +oid sha256:77cae565842c0c8b0314d1c10c72f4b52483d002c614a33734b534f743d06b5d +size 146574 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R8_C1_G1.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R8_C1_G1.png index 5ad60a4bf..0e31e223b 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R8_C1_G1.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Bike_R8_C1_G1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a61bca9de7e0b6bf6f88c4d01b9e3f1611f6866db10e4a1168550ab84804f42 -size 178531 +oid sha256:223027dcd3a8e5ded5f98b3aa47b0e18bc40b569e47f6bfdab1126f4420e5977 +size 171283 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R16_C1_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R16_C1_G3.png index d1b15eb8e..be4b535c7 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R16_C1_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R16_C1_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e2f6c6c42ddd15c0730edcd402bf8fd05a1116cb72495e3156a545988ff0230 -size 85901 +oid sha256:db64cd4e0369fa2db33aa1f21a94466ca39a191b0e0adbbbfbcbca44284c61ab +size 86151 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R16_C2_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R16_C2_G3.png index 3acb240b3..5ddb7ce12 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R16_C2_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R16_C2_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8193ce8690dbb99517cd56e5a866268c0a5c971c992058afb818393658781b91 -size 77762 +oid sha256:afc3a020178a68715ab0dafa30ee1db81630b760fde3df3271048acc627f178c +size 77614 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R8_C1_G1.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R8_C1_G1.png index 335375205..db03e0fb4 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R8_C1_G1.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_CalliphoraPartial_R8_C1_G1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:412c302f92500d855658d466aa1a686386ed998f6a3d5fe7326cd148a6f829ae -size 116229 +oid sha256:5ea7e0c3e16acf40491354191278a09af2db5ac6e0e7b92ba377f8fc9d52e305 +size 117067 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R16_C1_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R16_C1_G3.png index ffa9624d0..4d9777bd8 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R16_C1_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R16_C1_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d753a7dc732d6f01f7bce8c6de50d70158639aa5e0eb18d168e417fe36492731 -size 135 +oid sha256:ba9b046556b04556632f4b651dc9d8ebc4d27699e53567179ef15a850567fee9 +size 85 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R16_C2_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R16_C2_G3.png index ffa9624d0..4d9777bd8 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R16_C2_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R16_C2_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d753a7dc732d6f01f7bce8c6de50d70158639aa5e0eb18d168e417fe36492731 -size 135 +oid sha256:ba9b046556b04556632f4b651dc9d8ebc4d27699e53567179ef15a850567fee9 +size 85 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R8_C1_G1.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R8_C1_G1.png index ffa9624d0..4d9777bd8 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R8_C1_G1.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_Solid50x50_(255,0,0,255)_R8_C1_G1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d753a7dc732d6f01f7bce8c6de50d70158639aa5e0eb18d168e417fe36492731 -size 135 +oid sha256:ba9b046556b04556632f4b651dc9d8ebc4d27699e53567179ef15a850567fee9 +size 85 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R16_C1_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R16_C1_G3.png index e155d618d..9edb930aa 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R16_C1_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R16_C1_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:570cb16b4026d38cbf586592c34d0fe303f2c1d99baeb531670c4ba25956f9c3 -size 15489 +oid sha256:20bb78a7539049ced92c936c2a78fdeb3809ffe0cb1dfae034e55e326de094a8 +size 14285 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R16_C2_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R16_C2_G3.png index 3f93014b6..ce093b38a 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R16_C2_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R16_C2_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27813f975640c7ac15e78af910efc796ada950cd7efcd9a4fe045d531de24ec8 -size 15197 +oid sha256:a55e413991f9817ce9cecaf9d3ed31f5cd7559f9d527ff2e03c8bea5be091887 +size 13943 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R8_C1_G1.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R8_C1_G1.png index 198baadf7..a8d549dc6 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R8_C1_G1.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern200x100_R8_C1_G1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e10a91175585b3843b25b710dc45f734ba706b60fc56a21748da355f27b8dcf5 -size 12776 +oid sha256:2afdab1f7f430c3f38fa7a6cdac60fbeb03ebcdda00a1bc79b289680a6811388 +size 12056 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R16_C1_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R16_C1_G3.png index 9140cdb0b..5104bad4b 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R16_C1_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R16_C1_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c0aca87ea94ec94c4d9e458dd33d366e0166533ee3e3c39066ee9d8be63a74e -size 1393 +oid sha256:028cd8201f9e7cd4988d63cc80d9afc96d0c739c65d807f7c755947e49e65111 +size 1326 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R16_C2_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R16_C2_G3.png index 32778c0a7..f96d77928 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R16_C2_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R16_C2_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d486512f51a57c90de6c3126791fbd3b7e7dd756932b5bd716660ee9b72f010 -size 1168 +oid sha256:213e3bd008292b807e2a2ec216ec017a31cd165f4d2a8bc86b3deea3daffc81c +size 1109 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R8_C1_G1.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R8_C1_G1.png index a60c7f0cb..6656f7564 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R8_C1_G1.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern23x31_R8_C1_G1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79f344abd9f1cfe6e8c2f0be52fd9498ee3ee6370cea94244d90fdaba7fa5e71 -size 1488 +oid sha256:2ceeb731111403f6354c460e403a30a2269ae1b2e2a9ef9b3819bdcadb50b859 +size 1424 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R16_C1_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R16_C1_G3.png index ae9b64afa..da9a0c2e3 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R16_C1_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R16_C1_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90a45e7212ce97754be0e1432fffdc62ef5d2bdc30fdff0477a1951754cc0929 -size 1113 +oid sha256:fa9d77a2f1b727a42323a8ac19cb4cabdef0e7f0257bc92bb0100cd280f87613 +size 1076 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R16_C2_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R16_C2_G3.png index 1e9dc61e6..e0ccdbb5d 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R16_C2_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R16_C2_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c61766f05dc48413c21baea8af1994895aa354732109fd5701a42f746f1f412 -size 1005 +oid sha256:fc9986cd6042d8cad709a5768dc464fc171a3cb5d3457406c11c46ef55388daf +size 956 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R8_C1_G1.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R8_C1_G1.png index 99a249b1d..4f1d40e2c 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R8_C1_G1.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_TestPattern30x20_R8_C1_G1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be002593c2874eab803d0dc9b5646fb51b94ea92e17158797609219f46c2f723 -size 1518 +oid sha256:7e92efee845abd30705bb9a9cf61997092fca89b48e20d8383a0c08ab159a0af +size 1491 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R16_C1_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R16_C1_G3.png index d4ff4e16b..404dc83b1 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R16_C1_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R16_C1_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11edba36c7f73271d9575b16cc7663a1e75c3b34560df51b7430cfde3ce1ea08 -size 6576 +oid sha256:21e8dce4256aecb250e0079c8bd60deb8b2ca55848fb0fd647ed3115afcf82af +size 8401 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R16_C2_G3.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R16_C2_G3.png index a02c2d670..525eedcd8 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R16_C2_G3.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R16_C2_G3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75320c25e8977e5d56ff32f054e15773bd797e4ba745cd7bf3a2fd4c7000e3df -size 6400 +oid sha256:c4b1b7f904b23b6d7bd7b0fc4c533d3c213f38bcab689e9dac1936b05b71e9cf +size 8268 diff --git a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R8_C1_G1.png b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R8_C1_G1.png index 5fbabed0a..2585ff479 100644 --- a/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R8_C1_G1.png +++ b/tests/Images/External/ReferenceOutput/BokehBlurTest/BokehBlurFilterProcessor_cross_R8_C1_G1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bb334d24245359fda8b64a74cbd30267cbdd1238e0aeae674b57be5371cd7db -size 6266 +oid sha256:33ca697815ebf4c74343e0abc7a418af8ffdd8fa0830285770249a1f20929eae +size 8444 diff --git a/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png new file mode 100644 index 000000000..8f31f0d8d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f66bf45b5d80e412314341567b8cf240d56b5872f01ce9f3b0d6034d85e8e942 +size 163327 diff --git a/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_Calliphora_uint32_uncompressed.png b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_Calliphora_uint32_uncompressed.png new file mode 100644 index 000000000..8f31f0d8d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_Calliphora_uint32_uncompressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f66bf45b5d80e412314341567b8cf240d56b5872f01ce9f3b0d6034d85e8e942 +size 163327 diff --git a/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Uint_Rgba32_rgba_uint_uncompressed.png b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Uint_Rgba32_rgba_uint_uncompressed.png new file mode 100644 index 000000000..96482b46e --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Uint_Rgba32_rgba_uint_uncompressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46ef9cd2f212651a5ddc78aa1707df87c18ce227466b7b6f00d215f11271e61a +size 24590 diff --git a/tests/Images/External/ReferenceOutput/Filters/BlackWhiteTest/ApplyBlackWhiteFilter_Rgba32_TestPattern48x48.png b/tests/Images/External/ReferenceOutput/Filters/BlackWhiteTest/ApplyBlackWhiteFilter_Rgba32_TestPattern48x48.png index 4f57775dc..aa561ace8 100644 --- a/tests/Images/External/ReferenceOutput/Filters/BlackWhiteTest/ApplyBlackWhiteFilter_Rgba32_TestPattern48x48.png +++ b/tests/Images/External/ReferenceOutput/Filters/BlackWhiteTest/ApplyBlackWhiteFilter_Rgba32_TestPattern48x48.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65d3a099dd24fcee2fc86c63b8664ebd81d7b3c530168da5f4e50ee0ca925496 -size 759 +oid sha256:d76ae5ac1a0aebb9d76b2fa5c6b1fc109e5fcbdc6c41d9681a76ade9c86fab79 +size 748 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/GifDecoder_Decode_Resize_giphy_150_150.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/GifDecoder_Decode_Resize_giphy_150_150.png index 87c1fb728..2449c3efd 100644 --- a/tests/Images/External/ReferenceOutput/GifDecoderTests/GifDecoder_Decode_Resize_giphy_150_150.png +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/GifDecoder_Decode_Resize_giphy_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfd3afda359646aa3d46e1cffbfa7060395fda4c36c419ed3a2d8865284d090d -size 4031 +oid sha256:43e15b35bd282ecbf5f3b0b08b0f6fdba2e9144b0c17875d27bb8366b213c999 +size 4020 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/00.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/00.gif new file mode 100644 index 000000000..4a2dcec8d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/00.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba81598dc214845f18064f987a55eebdab0e53149df8fd67904733b42c760ad6 +size 7389 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/08.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/08.gif new file mode 100644 index 000000000..4a2dcec8d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/08.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba81598dc214845f18064f987a55eebdab0e53149df8fd67904733b42c760ad6 +size 7389 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/104.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/104.gif new file mode 100644 index 000000000..a28b1dd60 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/104.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6bd47e66b07bab7edf53a07631016f8334ba337243d6b8d61b0c8a3cf7f3f0d +size 7974 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/112.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/112.gif new file mode 100644 index 000000000..a28b1dd60 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/112.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6bd47e66b07bab7edf53a07631016f8334ba337243d6b8d61b0c8a3cf7f3f0d +size 7974 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/16.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/16.gif new file mode 100644 index 000000000..016f9f9b6 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/16.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03d4e5cdaed2def942ca8480c9e45bb3d7914c6dbe3eac2b287afc8ff815b95d +size 8705 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/24.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/24.gif new file mode 100644 index 000000000..285514e23 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/24.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ad084d64942ea1dec0c1b22a88400b47f14145d761aa22a8e7deb42b5bd9fb0 +size 13011 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/32.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/32.gif new file mode 100644 index 000000000..34eff6c6c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/32.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:714cf4be4fc6c4164494ca1950796211577ab61cbd939f7cc53a36c3f7f742c5 +size 16457 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/40.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/40.gif new file mode 100644 index 000000000..7a732b4fe --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/40.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb04c45837b763abf2c326b3e831c5b772853d50c0bfc85fb7ac7ca62361292d +size 16872 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/48.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/48.gif new file mode 100644 index 000000000..7a732b4fe --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/48.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb04c45837b763abf2c326b3e831c5b772853d50c0bfc85fb7ac7ca62361292d +size 16872 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/56.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/56.gif new file mode 100644 index 000000000..6d60d4981 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/56.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c68bad5aebb65e86e4295956b9f0bc21e181a6d8dadd9dbfbd5febf0a5ba0d8 +size 17414 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/64.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/64.gif new file mode 100644 index 000000000..9af6451a6 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/64.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d0672248b0e45ea700aa0c484ed0e342284ce18cad1cdc233700614636bed11 +size 16556 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/72.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/72.gif new file mode 100644 index 000000000..282ef1f8a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/72.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b12d34adf8c531c0d4ab02f7b57507a585bb1d31fb82d828c8c52732cef9f18 +size 15590 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/80.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/80.gif new file mode 100644 index 000000000..282ef1f8a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/80.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b12d34adf8c531c0d4ab02f7b57507a585bb1d31fb82d828c8c52732cef9f18 +size 15590 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/88.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/88.gif new file mode 100644 index 000000000..108da2ce3 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/88.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a31b9aee61fbc7d5dd5f163f2c81cb36140a05d788a7ed2d15de550c0fe13232 +size 14561 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/96.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/96.gif new file mode 100644 index 000000000..161443e4f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/96.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bc49ba55ee33a3aff5069b7a6bf45cdb429ad691f8701093ad7fe04abe25ea1 +size 12156 diff --git a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png index de79ec729..84c7e64ee 100644 --- a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png +++ b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aada4a2ccf45de24f2a591a18d9bc0260ceb3829e104fee6982061013ed87282 -size 14107709 +oid sha256:4a4d2a3a1f320d3fcb121bd8edf716354b7e8ceb251a9eaa2087f443a3adbbc1 +size 10896218 diff --git a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png index ff5b35a5f..679615d17 100644 --- a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png +++ b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dca9b5b890d3a79b0002b7093d254d484ada4207e5010d1f0c6248d4dd6e22db -size 13909894 +oid sha256:0c3f30908a803ac5956dbd5959b225bceab1fc673a1b89d55d1096ba024c7bcc +size 12519351 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_Calliphora.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_Calliphora.png index 07c29c097..95ea3d472 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_Calliphora.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_Calliphora.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:362eddc5e06d672b4654bfe7a1ded995934a1c59719a3f909773b2e61931ffac -size 1332495 +oid sha256:9a6f484f06c0b466d16591db78fec3cd2c3f6ebc7578896075cd4af95d8ceaab +size 1411818 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_Issue394-MultiHuffmanBaseline-Speakers.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_Issue394-MultiHuffmanBaseline-Speakers.png index 1f38ec542..828880162 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_Issue394-MultiHuffmanBaseline-Speakers.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_Issue394-MultiHuffmanBaseline-Speakers.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7a1e89adcd70f792d678786a177bac927a15d6065001cd76aab0bf3cc1b7b4c -size 1034853 +oid sha256:88271ffea79e42914b8c37828da65afb691c7319b8e28fdd35942ecba4087baa +size 1060013 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_MultiScanBaselineCMYK.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_MultiScanBaselineCMYK.png index 349ff6485..e72b1a041 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_MultiScanBaselineCMYK.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_MultiScanBaselineCMYK.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9dbb00a4be909d4b7fd605f83f786ff85545da54272a256c100afd80d687e8f -size 93253 +oid sha256:3b160cc44490303c23c0abaa0dda827d68e11bfa67127e19da974073085abbe4 +size 96042 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_badeof.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_badeof.png index 830a95bd7..ccdb19e4d 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_badeof.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_badeof.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24f637742951c438da5acbae6a93545830ea4d0065031572a3bdd1c96be15cce -size 48990 +oid sha256:7ce1a5d71cefd6527daecb8c1d9089f63189b833f1186971ddb89c6257d076ef +size 46745 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_badrst.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_badrst.png index 0512251ab..4f2aae2ed 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_badrst.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_badrst.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0501e53f97ddb4252e15073dc75f4129428befe5594a07297c21ae9f89f075b2 -size 316278 +oid sha256:522b0a2f9a97689217af461ab2a63a41553c0ae6ca28cde046b01019c87e2393 +size 348228 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_cmyk.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_cmyk.png index 27c22ecc6..e24cc5969 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_cmyk.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_cmyk.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2253c83151238056caef2f3bca8800108a84360b2ff21979e4ff37fdddd2232 -size 419852 +oid sha256:5e81ac851f979c017082180a13ee4ddfe26c4151bfc082f7915252e57da44ec7 +size 416967 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png index 4032a32af..ae30676b3 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg420small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b5e1d91fb6dc1ddb696fbee63331ba9c6ef3548b619c005887e60c5b01f4981 -size 27303 +oid sha256:79291e1eddee66548156fc40bb5c8b1209c7b0048fb56f2bb7890c7d89ce655e +size 27264 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg422.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg422.png index 018ecda7a..8ddb74880 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg422.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg422.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:733cc46271c4402974db2536a55e6ecae3110856df73031ca48dad03745d852d -size 35375 +oid sha256:5196d5a20f639d833cf1ad979a290c144c97b41897fa5f0fa7454f5f4e8d3c11 +size 28697 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg444.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg444.png index e5ce7eb3d..acca4b44e 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg444.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_jpeg444.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6543f546fac9d05ebdac7a534b0cc422f31bfd81067212a19cb3a52ad24560a8 -size 3978 +oid sha256:9894983085f3d3d45b8c4ea9acc680932f236a4602230046386130aa2af916f1 +size 4936 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_testorig.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_testorig.png index 830a95bd7..ccdb19e4d 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_testorig.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_testorig.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24f637742951c438da5acbae6a93545830ea4d0065031572a3bdd1c96be15cce -size 48990 +oid sha256:7ce1a5d71cefd6527daecb8c1d9089f63189b833f1186971ddb89c6257d076ef +size 46745 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_testorig12.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_testorig12.png index 23f1941dc..dc41461e0 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_testorig12.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_testorig12.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad57cf87eade9ee6663f575b358eafaa869a16b3b02d0fb00ca8c683422b85a0 -size 47601 +oid sha256:26ef4244aa761c7b6c51534e414f79438753b9dbd1fbf45d2f1e2fd2dd4ec4c1 +size 46636 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_ycck-subsample-1222.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_ycck-subsample-1222.png index f35c40ed8..66337b5c9 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_ycck-subsample-1222.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_ycck-subsample-1222.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a17478333c6ef0943aeb96faba1c0ea560995d0612564fe033b72b2949f4869f -size 66748 +oid sha256:91a522b32485af3be304c2d98a60b9906112637a4627d69a072976474cef7485 +size 66932 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_ycck.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_ycck.png index 5f41fb523..76183356b 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_ycck.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_ycck.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63f7105e9e2d0794b3f7225af2187a5717c12b806b68b33b98e08e0a0b1ffa79 -size 71051 +oid sha256:f374012655af194ebece2e9329da416be830d1cadd24a2bd584480d45751ea78 +size 58272 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_BadEofProgressive.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_BadEofProgressive.png index 2386ae49a..ecf22f93c 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_BadEofProgressive.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_BadEofProgressive.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e5d84fa88ac8b552c21c042f155801924240d764da365440d28eb8133950925 -size 568177 +oid sha256:36b557cd6d55389b8f02b811ecd2b14f1f1c440c7e8dd5a5a3c55cea57547203 +size 564682 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_ExifUndefType.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_ExifUndefType.png index 3de8b1557..c144554ca 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_ExifUndefType.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_ExifUndefType.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb636a9f304cdef713b823adc7424108b4d44fada294c48a366278c9b9e7b4b5 -size 20163 +oid sha256:ff9554b1f421ca04e0580fbb505895f002b80298feb128a15cdc045a1625a490 +size 20111 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_fb.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_fb.png index 07a7c3573..30bfb538f 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_fb.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_fb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2935d3dc66eec334eec65c1ed9fbb04046404a9c28bed413ac635878e63ea6cf -size 114688 +oid sha256:c2eb2bd4db9b67466d96fe154cd771d6780a24a798a0c051f307425f1b0e3d15 +size 115708 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_progress.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_progress.png index 8241c36ac..e9a2a0bd8 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_progress.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeProgressiveJpeg_progress.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b48730a7a149e13dd87a24e62c0704895b08a5ed1f8ea48a6a6a7248450750d -size 301416 +oid sha256:c5c5b5711041f22e8ee1afb344137c00ee80ba51e777689513e8dfeea855befa +size 297941 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png index 77a9d0d9c..4e554a34d 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_CMYK_ICC_Jpeg_Rgba32_issue-129.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:215cba73dfb0e19f75f6dc0c3fefca474bd65f57684a207a11d896e1637bb643 -size 1240827 +oid sha256:9750b54f5220020e0c3f5ce22e4f108cb70290750c613c29beeb49473d5c80a6 +size 1349665 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png new file mode 100644 index 000000000..12692cb9e --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73497e1eaddaa86cc915fe59a177e9ab6423d725ab4e6af21c8414c9edc6ceaf +size 347 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png index 0963e90b7..37e1e37cd 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AdobeRGB-yes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c942b534baa51b8e46e88bd38d1ced319bccf1b55a5711ae5761697b7437fe4e -size 458321 +oid sha256:dcc32a36dcdfe3b9cfae17a81d3eb180248db4507ae93fdd4bb92e1e5e29950d +size 466215 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png index 1e5eaca4d..dbfa40378 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-AppleRGB-yes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:163711226bdfa2f102b314435baea9f69ad1be1b11ef5ad8348358cd09a029ae -size 482963 +oid sha256:ae9a32eba21fa0bd2039803b5af8ffdba0409c104746455d5028741f214eb1a1 +size 479922 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png index 06bee00d7..5427b5c95 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ColorMatch-yes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f7981f40bab5bffff3e7c9ea1676d224c173fabbaa6e7a920d7a9dabd58655f -size 464917 +oid sha256:e2bf66bb6d7eb3ff402b8564fb51bc951792ad47dc17fc9c7c886744638a59bb +size 469096 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png index 3ae12c657..787044068 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-ProPhoto-yes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11b02b982c024e295915e88f56a428ad73068217a7ae625f705127ab8c35a4bf -size 477061 +oid sha256:e9c015e599b1710fd50c4ad61104fb03f27f211c13686cb162baba492ec4a935 +size 482675 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png index 0a2eb91cc..2ee122b8d 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-WideRGB-yes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a49967c20cccf824df4de3f105f5ddb44d7a602c072ce22caa38939f21f62505 -size 469827 +oid sha256:e25ba3afb3346633db52e91062c4266474bc9d9c0b3fcf686f1040b3a4d70d0d +size 476050 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png index fa554484b..a1e876d2b 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Momiji-sRGB-yes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b33fc8fd03142aaaf8aabc39e084acd9e82e9222292e281953d92d65edcc1a7 -size 436111 +oid sha256:b80e88a7fdb27491687202c247eafbeb9d1c024affd5dc4633bcf15602fed990 +size 464921 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual-cLUT-only.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual-cLUT-only.png index a0b73d299..8a27087f5 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual-cLUT-only.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual-cLUT-only.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe06798b92c9b476c167407e752b4379d50f1b1ad6329eceb368c8c36097b401 -size 95103 +oid sha256:dbe96e449dfeadc71bbfb0b48d2f56e6357d5b64a225153eed60263ea3356e71 +size 119987 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual.png index 99ae53f93..586bc9647 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_Perceptual.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21f8d54d4b789b783f3020402d4c1b91bb541de6565e2960976b569f60694631 -size 99385 +oid sha256:6e9e4da17b7270565fd592f4ff710ff317e1054794d966d33f11748defc7c35b +size 124944 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_sRGB_Gray.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_sRGB_Gray.png index 759b26a60..5164defc1 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_sRGB_Gray.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Rgba32_sRGB_Gray.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18ad361f79b4ab26d452d5cc7ada4c121dfbf45d20da7c23a58f71a9497d17a2 -size 5341 +oid sha256:0eaf217570e283b6fb9a7d52ba5087d760d9a98827e507cbae299915871e1c31 +size 2595 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png index bf9556683..a73e5f31e 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_YCCK_ICC_Jpeg_Rgba32_issue_2723.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cef85199e8560d6669766c094d078831024e44ac7fc537f8696f802c8e06138b -size 420141 +oid sha256:de73fa7a2cdd79763e40c51381f9a0cf5c7d43db2dcd7a86dad0a5b68377665f +size 421249 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Resize_Bicubic_Calliphora_150_150.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Resize_Bicubic_Calliphora_150_150.png index e982d9034..16df99e04 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Resize_Bicubic_Calliphora_150_150.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Resize_Bicubic_Calliphora_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bb6ed717a2af582d60ccd6c1c9c1ac92df0f8662755530b7e9063724835b23b -size 27709 +oid sha256:9be82bf220095b5650c7cd0a0eba9ee2b0fbe39881ff2bbde465ca9d2f94490f +size 27803 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Resize_Calliphora_150_150.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Resize_Calliphora_150_150.png index 65aac22e9..98aac2c55 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Resize_Calliphora_150_150.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Resize_Calliphora_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4804948a2ba604e383dd2dcc4ca4cac91c75ac97a0ab10bd884478429fa50a5 -size 28178 +oid sha256:b7e4b589fdbad2688895317fc760a834624f76cc9a027a65ce3cd6b35563ec85 +size 28514 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_Combined_Resize_Calliphora_150_150.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_Combined_Resize_Calliphora_150_150.png index 65aac22e9..98aac2c55 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_Combined_Resize_Calliphora_150_150.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_Combined_Resize_Calliphora_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4804948a2ba604e383dd2dcc4ca4cac91c75ac97a0ab10bd884478429fa50a5 -size 28178 +oid sha256:b7e4b589fdbad2688895317fc760a834624f76cc9a027a65ce3cd6b35563ec85 +size 28514 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_IDCT_Resize_Calliphora_150_150.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_IDCT_Resize_Calliphora_150_150.png index abe31b2be..665967330 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_IDCT_Resize_Calliphora_150_150.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_IDCT_Resize_Calliphora_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc67170d70378ad8b8c0e1c1695b5c268341f0d26a6c788d1a8dffa8c90482a0 -size 102165 +oid sha256:d28335f6ad2421b6bc7e420c2e61ead73cd64f63ba7e1c9bd98ac1c140188ddb +size 104381 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_Scale_Resize_Calliphora_150_150.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_Scale_Resize_Calliphora_150_150.png index 87087adc5..9c01562f1 100644 --- a/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_Scale_Resize_Calliphora_150_150.png +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/JpegDecoder_Decode_Specialized_Scale_Resize_Calliphora_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae7f6ebfd9f2ddd85611827fda13eaf316d36d5187900458568f80b929effb9b -size 28291 +oid sha256:d0db0474f28d6302aae207b09f63e2de9e9ce6a9777a95dbac69ccb957d08daa +size 28381 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgb24_flecks_ImageThreshold-0_PerPixelManhattanThreshold-0.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgb24_flecks_ImageThreshold-0_PerPixelManhattanThreshold-0.png new file mode 100644 index 000000000..a180f0118 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgb24_flecks_ImageThreshold-0_PerPixelManhattanThreshold-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10ee142fc1d3638ebe53fdd21c0a4c53008801befcc5e986051b796561cae887 +size 237676 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_Issue_410_ImageThreshold-0_PerPixelManhattanThreshold-0.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_Issue_410_ImageThreshold-0_PerPixelManhattanThreshold-0.png new file mode 100644 index 000000000..020facf41 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_Issue_410_ImageThreshold-0_PerPixelManhattanThreshold-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:511cb90e72fcb837e4c9a31561a3c914f5201452d4ca63502cb5219cb4dc42be +size 619 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_clocks_ImageThreshold-0_PerPixelManhattanThreshold-0.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_clocks_ImageThreshold-0_PerPixelManhattanThreshold-0.png new file mode 100644 index 000000000..0f184812b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_clocks_ImageThreshold-0_PerPixelManhattanThreshold-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2dc73f1b4435a26125d910f005b5df7a540168a954f44c01a8e46df201adb1b6 +size 335851 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_colors_ImageThreshold-0_PerPixelManhattanThreshold-0.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_colors_ImageThreshold-0_PerPixelManhattanThreshold-0.png new file mode 100644 index 000000000..74647f04f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_colors_ImageThreshold-0_PerPixelManhattanThreshold-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cdcc80a8c662c50d2f72ad8e123d595c6e80394538c05666b8d3531d651e71a +size 11270 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_screen_ImageThreshold-0_PerPixelManhattanThreshold-0.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_screen_ImageThreshold-0_PerPixelManhattanThreshold-0.png new file mode 100644 index 000000000..c3a61bde8 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_screen_ImageThreshold-0_PerPixelManhattanThreshold-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00bb4c7b345389f5d95252c19d70fc2b654c4f0198e6a704b603da92b78e9a0a +size 102982 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/PngDecoder_Decode_Resize_splash_150_150.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/PngDecoder_Decode_Resize_splash_150_150.png index c697cd4c2..37323155f 100644 --- a/tests/Images/External/ReferenceOutput/PngDecoderTests/PngDecoder_Decode_Resize_splash_150_150.png +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/PngDecoder_Decode_Resize_splash_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c87a09b28b3f857e13a2bb420d16caa131a67c45b0fe31404756042f30de6d0 -size 28139 +oid sha256:a6a0e244af51f47f725dfcf1ef60705dd3223269db98685a1078b57eb2273c31 +size 23988 diff --git a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png index 4c7830375..c10227c6b 100644 --- a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png +++ b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1af50619f835b4470afac4553445176c121c3c9fa838dff937dcc56ae37941c3 -size 945821 +oid sha256:b33a960891c6b1e9cc6ac2ddc3cf49d8f49e0c749dfa7a67db49354988b129f6 +size 935007 diff --git a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png index b29213870..9c679acee 100644 --- a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png +++ b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df34f8f3640b145add4f24f8003c288fe7991373b079a87b4be90842e18c82ae -size 8236 +oid sha256:ad1630f3da7c2b997d04c75de2ac1465255c977454461f2fdc7026b3f87e11f1 +size 8232 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png new file mode 100644 index 000000000..d2b62e63a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27f6e8e195c4431dc7354a379152d3a8664582bc2bb1c8960ebf4088aa6505e2 +size 248709 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png new file mode 100644 index 000000000..ecbf328d3 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46b5751dc43e9ad5541913cf851ef1b061aa474a95283c712511531202d7015e +size 239326 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_OrderedDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_OrderedDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png deleted file mode 100644 index 327366f5b..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0086044f12a7c58e49733f203af29a8aff2826ea654730274720eada15669254 -size 249163 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png deleted file mode 100644 index 3e0be536e..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:85ee8479984aa52f837badbc49085c5448597fbfd987438fe25b58bad475e85f -size 239498 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png index 922c2bf9b..28db1b73a 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f1462733e02d499b0d8c61ab835a27c7fee560fdc7fc521d20ec09bb4ccc80f -size 216030 +oid sha256:af40e835e2f3cf0f406e15248169d058dc1ae69219f2bc5c3413ecea4eb4985f +size 215873 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png index 922c2bf9b..28db1b73a 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f1462733e02d499b0d8c61ab835a27c7fee560fdc7fc521d20ec09bb4ccc80f -size 216030 +oid sha256:af40e835e2f3cf0f406e15248169d058dc1ae69219f2bc5c3413ecea4eb4985f +size 215873 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png index 29c93d14e..078c75c45 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e6d91a3ec4f974af675dc360fd5fd623ec8773cdbc88c0a3a6506880838718a -size 226727 +oid sha256:5eb87f02c7924b764bbd2c951047b7204c56a0a1a0d6853a0fb3d30a56ed0184 +size 226633 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png index dbfab2b50..e80b9b8b1 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c68eba122814b5470e5f2e03e34190ff79e84e4b431ad8227355ce7ffcd4a6a7 -size 220192 +oid sha256:84b55eefd699cd74a1a7de958762b095f196275d2bbde2750936aed9a47f68f3 +size 220099 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png index dbfab2b50..e80b9b8b1 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c68eba122814b5470e5f2e03e34190ff79e84e4b431ad8227355ce7ffcd4a6a7 -size 220192 +oid sha256:84b55eefd699cd74a1a7de958762b095f196275d2bbde2750936aed9a47f68f3 +size 220099 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png index 86655af42..ad899553d 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dbd3189b559941f91dd6e0aa15b34a3e5081477400678c2396c6a66d398876f -size 230883 +oid sha256:c4548abed72e4f833b33eed14392206d7232112fc651becb2351fdee27da5bc1 +size 230687 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png index 82d5e5d59..a30d69d17 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4df5b1bc2c291ec1cf599580d198b447278412576ab998e099cc21110e82b3d -size 263152 +oid sha256:832173c8ca6bd7a8bf417d83b459ccddb541daed1c31539bf596cacea455441d +size 263018 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png index d8a1178ad..e5591852b 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df63a3d12e2998d5242b64169ac86e3df7ab4be585a80daddc3e3888dfcb7095 -size 262298 +oid sha256:15a6dc485f0c3fd4c9fbbdb6b50437d58d68210790e37f8aab32e66a864e2746 +size 261872 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png new file mode 100644 index 000000000..2e815d4d1 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6eeed563b407940e2a05f068c42b52738e6e1217a1500c9230f7068ca4e9f1e +size 304162 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png new file mode 100644 index 000000000..a8f30e5f5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dc7dc55af4ef0741a66c569876ad8a2df27164a653baa5bae536e6d121b2c11 +size 300528 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png new file mode 100644 index 000000000..3ece7ee28 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b65e7903fbfa1ed0682221fdd86c6f0448b3f6a886cae5379720cce881a1f1e +size 305962 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png deleted file mode 100644 index f29db004f..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce381c2d261b9b1ca61d8f6e2ff07b992283c327dc6b7cf53c7e5c9317abb7d3 -size 316443 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png deleted file mode 100644 index 284c3a270..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2bfc23a95df8a88ac6e2777d67f381e800d23647c162a9a97131a101bbb97143 -size 306703 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png deleted file mode 100644 index 5911faa72..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d3f58a108d933ec9ac0a5271af5b65d0a8ab9d521d54e48312b280cc42d71ac -size 322049 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png index 020562673..03b9a37f7 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a2aae04edebcaca9b95f30963201794887fa0eac954b64c68bfe529b14fa9be -size 269397 +oid sha256:97c277005703b029a9e791e4c9dc3adcbe06054885fdd31e361e8a0a0222a291 +size 268504 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png index 020562673..03b9a37f7 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a2aae04edebcaca9b95f30963201794887fa0eac954b64c68bfe529b14fa9be -size 269397 +oid sha256:97c277005703b029a9e791e4c9dc3adcbe06054885fdd31e361e8a0a0222a291 +size 268504 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png index 68d91fc43..a1d28a169 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f3e9a338a5ae37c88ce0c348e0b655429220da051db3352779c277bb2dcb441 -size 270622 +oid sha256:b5fa657236e12cbb2a8d2cd747029723a6b3829b475f28626d7647d7b2150918 +size 271579 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png index 324bd9253..eba58870f 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752760327cc1416c171a920f1e0e95e34eae6d78bd0c7393a3be427bf3c8e55c -size 284481 +oid sha256:532fa8044bb424b451343f89bf7cb954311641056bdbd5685cd7c4fa4ad8f3c8 +size 284056 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png index 324bd9253..eba58870f 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752760327cc1416c171a920f1e0e95e34eae6d78bd0c7393a3be427bf3c8e55c -size 284481 +oid sha256:532fa8044bb424b451343f89bf7cb954311641056bdbd5685cd7c4fa4ad8f3c8 +size 284056 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png index 52bf2a163..de30e7574 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:293459538454e07bc9ea1e9df1fa5b0eb986fde7de42f6c25b43e4c8859bd28a -size 285370 +oid sha256:61ed5f4d77428be46357609d80a66e884dedbb8c255fdcc71d49eeba0eed2bf2 +size 285037 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png index 05be1395a..c56a90ad2 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90a2b7b3872c6eb1f1f039558d9f6ace92891c86951c801da01ad55b055fd670 -size 316544 +oid sha256:1cc2ef3cb819b5a82e0af32c3ab44aff0206530e291b00bdade58da2ebe4494a +size 308246 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png index d94d57759..c3ab7996d 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff094e6bafe81e818bcbac69018dcfe29366389dfca0d63d8e05ef42896ffe1d -size 317309 +oid sha256:575c8d81152642fa0eec0ea9901d1941fea58b7686cfaac1d01e0bf59f393c4b +size 308330 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png index e016e3de6..47616cd31 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee0778aac671365dd0afae06cdcf8f36243bd9815f684b975f83e297bb694e63 -size 323979 +oid sha256:ba295a5ddb79bc61f0be9a28a636fdcc63055c26c46872d407fe20ff785f11ed +size 310415 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png new file mode 100644 index 000000000..2f939d957 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png new file mode 100644 index 000000000..9e8002ad1 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9a87ef109c08411ca61d91ddcf010c272303a17abd90b6ba2204eac021055e5 +size 13665 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png new file mode 100644 index 000000000..45e770bd9 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81496d88b42edf4b39ab723d0b5414b56140892f45d30fc2435904b630fa9af5 +size 13886 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png new file mode 100644 index 000000000..2f939d957 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png new file mode 100644 index 000000000..c84edd138 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:997e5281abd8cf3a587984ec1b7e31487ec5ddf16326d025124833d536e4ac27 +size 13910 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.25.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.25.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.5.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.5.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.75.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.75.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png new file mode 100644 index 000000000..2f939d957 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_1.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_1.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png deleted file mode 100644 index a2fb2a676..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89 -size 13158 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png deleted file mode 100644 index 8d99eb49b..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:abfdd1e40c2c1d7fde419bda1da6e534ed989598e790b8ae4de35152a83f77a0 -size 13686 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png deleted file mode 100644 index bf93c39ff..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:60c28eb1dc3c0416b20cec230917c0e4a70dd2929467bbab796ecbb04fe5a178 -size 13886 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png deleted file mode 100644 index a2fb2a676..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89 -size 13158 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png deleted file mode 100644 index 457298b54..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a523f097bf3b155f3823c5e400190b5d5e0d4470db7136576472c3257db76600 -size 13909 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png deleted file mode 100644 index a2fb2a676..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89 -size 13158 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png index 878a36a47..f288b3c8b 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2bd11fa19fab712b5cd6c2b36d673c7dce904b5032b860d257b00e095e4aadf -size 13432 +oid sha256:dd31b6fc59e1f9f88230d57b39362b76cedd0bd94e15904f69071ba3f465e48d +size 13656 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png index eaf7e8241..62b1fb055 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png index 02879b7a3..d2d2e3e4b 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4ac8b88b317281738d833fc71f52348d9f4f45ea5a1303dd91fdb8b42be4267 -size 13186 +oid sha256:dd738ee2a397bb1ee305f03c70e185dea6f67827dc15b9df1966cfe8c0f28040 +size 13177 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png index ba0509480..c444923a2 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1305d54f2139d4577490317051d6ce94a7fc8dd45b902d87a30fb04098dd4594 -size 13407 +oid sha256:2a2df64f89df17428415932c2ef0028d8ad408b5276264d99e6038b70473ebde +size 13417 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png index eaf7e8241..62b1fb055 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png index b16a5a5c7..74cbbd758 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3fc3a7ace123c330ea06072eb36dd5d65ed9154d4d0f55a828fc542c8a422c1 -size 13472 +oid sha256:234854be2a3f774a58baf79f20e68c7331b6caff486ab4b1e509a96e2a3d70b9 +size 13455 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png index 6adac16cf..a9bb2c316 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35757f2e0831cae2fbd3cc11ffaaae855e853ebaa9a1a5564b6568a5e1c442e9 -size 16031 +oid sha256:ef65ce360293ca5659730747087c15735c15df1143204acb60120a5b68cd7cd4 +size 15905 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png index 5d1030e6b..ceb918800 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6679d6d6f7c8b44461956b54654cea71180a2b0d43712d3775e60cbedd90cc82 -size 17520 +oid sha256:6618f169cf4b585979f8e9261af88fe4a61c3c40b453a159cb643cc062a6a9dc +size 17517 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png index eaf7e8241..62b1fb055 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png index 567e5d6a3..f6cb17367 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5af5d16f875172d73f8426928fc8edaa4a6cab321a968b6c29fca32d0fba0df5 -size 18182 +oid sha256:4acf21f23978c83c9872bb2575ab45e4f0bbc86c8610c99479b1469fc12df5f2 +size 18112 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png new file mode 100644 index 000000000..b17bce9e3 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:822f4ae1d8676e2231a0c938296931be42a74c0a51b4ec63fdf6dcd17d64dc7e +size 83924 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_NoDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_NoDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_OrderedDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_OrderedDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png deleted file mode 100644 index e9782bc07..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b380eda5646fe97ee217ef711103001e54ee023fb8d95f7f3bbad19d886130da -size 83702 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png index 10daff76b..fd565c0a1 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8ba00e2948337f77d935d98349958c6a520958671e9ec714ff1bfadfb130e72 -size 44622 +oid sha256:4ded8db323023a7c7620bba3b2259a549571442fe0a37883c7755ac69ae9d6d5 +size 44646 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png index 37e5035d8..c342e3a23 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3802cfe67638a24869d6cc9ace1d94460b4c0c26f2c91b12b95fa8f979de64bb -size 101579 +oid sha256:83c8403f5d0e5457721d992c1e6980134e8a65a1f646163a4f091cf34583ca02 +size 101417 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png index e72ea4b24..d07231c18 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf2021eba9edbb2295924f8394472ac0bb237f0c462c39aa32a2074ef15f9acc -size 81771 +oid sha256:e5412b892143bb433804c662750a64a1660b2072520db53d76ec6897c636ec50 +size 81742 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png index 0997945e5..7d2070820 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d11b18946d373b995ecbb449c8c4cfcc7078aad1c8705997bcbf83131acde03 -size 102439 +oid sha256:a88a48586502de786aca0b36341cf6033fb3ec3ce7924ce1e2819fd14791ffe4 +size 102235 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png new file mode 100644 index 000000000..79711e2eb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22920fb2379dee7d12fee52f6a39b8e46e1e99f77b91f879c51bb33a981dfdcb +size 98851 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png new file mode 100644 index 000000000..fa6d4cb43 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c7137e1b87d317d7e139cde8499deafa89f27bddba146cc5736f9c0566778c5 +size 81609 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png deleted file mode 100644 index 314a05606..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2236e81d33fcfb50afb9d5fd1a38c5ddf5d33fbb52de1c3204a4a9892fd334ce -size 99084 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png deleted file mode 100644 index 529304672..000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4b59097d1507236af2556ae5f2638360b223b7752cd4c8f760bc14673d811d0 -size 81709 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png index b51076bd1..0fe3d30bf 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a8d9c0d81525d9f37d2f36946939040aea30edfc2b7ec0bf329fb49f6c7d73f -size 69896 +oid sha256:aee197677c3276d4abb8fc027358b38be26462374e364841781626f0aa67e1a4 +size 69769 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_ErrorDither.png index 7204abff4..ef86e7c48 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4474b94e2d563938e10ec0526e7d94ba06b440db51b910604e752f7f9e814d66 -size 110757 +oid sha256:0b3c8dc7e653ef1846c7359e9a0f719bee91549846f160abb547cd0aab6a8a59 +size 110711 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png index 691623fc8..c65381c05 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58a61c1d9a1d05acd484948c3e5c0496dbc74c0060f5de71741de39eae04ffa8 -size 103875 +oid sha256:4b95721a963def9e82dd32e277ed4594213920d7808ad26696d01e4f8fda842e +size 103855 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_OrderedDither.png index e80e6c6e8..eb2c2a0c9 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6649918c0394ead13c016a57b6a08561290651bccac88f7f15ba0e29dc5faa4 -size 110422 +oid sha256:cb174c104cdcf35433c98522a1d9d52ccf42e8927e0b59fec3556aeee8b15a47 +size 110505 diff --git a/tests/Images/External/ReferenceOutput/TgaDecoderTests/TgaDecoder_Decode_Resize_rgb_a_rle_UL_150_150.png b/tests/Images/External/ReferenceOutput/TgaDecoderTests/TgaDecoder_Decode_Resize_rgb_a_rle_UL_150_150.png index f3b7b0e80..8f63e3277 100644 --- a/tests/Images/External/ReferenceOutput/TgaDecoderTests/TgaDecoder_Decode_Resize_rgb_a_rle_UL_150_150.png +++ b/tests/Images/External/ReferenceOutput/TgaDecoderTests/TgaDecoder_Decode_Resize_rgb_a_rle_UL_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb67006d156cff0fa8d4d1b131ac0eaa98279ae6dcc477818b2fc65f2dfb77aa -size 23396 +oid sha256:d0ce3abf8024859d1375721c18049513106ade2f9529ec20864621e310ca0b1b +size 18786 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_msb.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_msb.png index 5b72c3732..a3317a6b4 100644 --- a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_msb.png +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_msb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3333503012aa29e23f0d0e52993ad3499514251208fb3318e2bb1560d54650fa -size 117956 +oid sha256:e50dfa103459d21642df5e1ca760081fbdfe3b7244624d9d87d8a20f45b51bbe +size 117953 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_Cmyk_Rgba32_Cmyk-jpeg.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_Cmyk_Rgba32_Cmyk-jpeg.png index 06d60e030..6f35ecb55 100644 --- a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_Cmyk_Rgba32_Cmyk-jpeg.png +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_Cmyk_Rgba32_Cmyk-jpeg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f68db78d765a7f36570cd7b57a1f06cfca24c3b4916d0692a4aa051209ec327 -size 616 +oid sha256:b1ff0ce13bf0521b8a6f0d77806473c837c45428391786f751186a53fd951c0c +size 394 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png index 6150aacb3..aded240a1 100644 --- a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cd36c7e07a08e22cceecd28a056c80e5553a8c092bfc091e902d13bd5c46f4d -size 120054 +oid sha256:a94178ba1f0bd2edecdfd14c93196d1e195912f98875d875e1d79c8632676d03 +size 46525 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_ICC_Rgba32_Issue2454_A.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_ICC_Rgba32_Issue2454_A.png index 97118c15b..addaa27bf 100644 --- a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_ICC_Rgba32_Issue2454_A.png +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_ICC_Rgba32_Issue2454_A.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4f77673028643af0ac02a8f6a1e2db14052177e3401c369391a8ff7e943770c -size 7679254 +oid sha256:13f51f69b061303d06b73fa7e626d7252a9fd14aa84c961ea894f6e7cd7218cc +size 7603818 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_ICC_Rgba32_Issue2454_B.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_ICC_Rgba32_Issue2454_B.png index 52accc22d..445cad99c 100644 --- a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_ICC_Rgba32_Issue2454_B.png +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_ICC_Rgba32_Issue2454_B.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e616895c21fd8b19a216e8a3ef4968bd413589b5875efdac29860f019a710527 -size 7517284 +oid sha256:b6a35853e44d06a6f74e3aaf558e6a4b9c3209c6c3fa2c8f3ee650bf049ac0d3 +size 7624359 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_Rgba32_Issue2454_A.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_Rgba32_Issue2454_A.png index 350d1af68..e88a98fa4 100644 --- a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_Rgba32_Issue2454_A.png +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_Rgba32_Issue2454_A.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7911e059049c427229136479740fd62e2e09907549ec3e1421a6a60da6167cc -size 7840892 +oid sha256:9e91f6ae534b95a9e052158043e929e1da22e0851b4d6dfdb8b802605396758f +size 7888373 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_Rgba32_Issue2454_B.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_Rgba32_Issue2454_B.png index 3dc99e604..29134afca 100644 --- a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_Rgba32_Issue2454_B.png +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_YccK_Rgba32_Issue2454_B.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:291f2033a7b4cfc10fb3301283c167b3fbc288bc173c95b21bc726bf076865af -size 7649213 +oid sha256:81bb1f05a6f72aebee41941b2390ca0a35c4368758af2d27b3e81bbee8901c56 +size 7956102 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_Decode_Resize_RgbaUnassociatedAlpha3bit_150_150.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_Decode_Resize_RgbaUnassociatedAlpha3bit_150_150.png index 412af0ac7..2f546df5b 100644 --- a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_Decode_Resize_RgbaUnassociatedAlpha3bit_150_150.png +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_Decode_Resize_RgbaUnassociatedAlpha3bit_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53b9dabffaae6a9250bf16f201ef8c9e933327874e8be91785c4fb6cd7e787a8 -size 10767 +oid sha256:10b471070b26a6aedfc0fce762dd71090409494ce0728790278d8584636672a2 +size 9940 diff --git a/tests/Images/External/ReferenceOutput/Transforms/EntropyCropTest/EntropyCrop_MultiScanBaselineCMYK_0.25.png b/tests/Images/External/ReferenceOutput/Transforms/EntropyCropTest/EntropyCrop_MultiScanBaselineCMYK_0.25.png index 331b8b30a..1a052e444 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/EntropyCropTest/EntropyCrop_MultiScanBaselineCMYK_0.25.png +++ b/tests/Images/External/ReferenceOutput/Transforms/EntropyCropTest/EntropyCrop_MultiScanBaselineCMYK_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a68aa183691be3240e881057cb2f5785e50228c9c5dd98163ba766b5cafa8b55 -size 88601 +oid sha256:1f312445263030ea79e2d1a05c62d19ade816c9cda9f0d3494ab76fde1e03ace +size 92534 diff --git a/tests/Images/External/ReferenceOutput/Transforms/EntropyCropTest/EntropyCrop_MultiScanBaselineCMYK_0.75.png b/tests/Images/External/ReferenceOutput/Transforms/EntropyCropTest/EntropyCrop_MultiScanBaselineCMYK_0.75.png index c375fff78..769e172b7 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/EntropyCropTest/EntropyCrop_MultiScanBaselineCMYK_0.75.png +++ b/tests/Images/External/ReferenceOutput/Transforms/EntropyCropTest/EntropyCrop_MultiScanBaselineCMYK_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53a7bc1aa7279cce0b73568b6ed9b141377f7654b73dcf926c43c22dd908039a -size 88502 +oid sha256:672f167d514f01c625145701ea0d45ced503a6e84cae75fbdb67e6b0db179aee +size 92318 diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/WebpDecoder_Decode_Resize_bike_lossless_150_150.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/WebpDecoder_Decode_Resize_bike_lossless_150_150.png index 35b99df98..4592d77da 100644 --- a/tests/Images/External/ReferenceOutput/WebpDecoderTests/WebpDecoder_Decode_Resize_bike_lossless_150_150.png +++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/WebpDecoder_Decode_Resize_bike_lossless_150_150.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d73ae8bfad75c8b169fb050653eb4f8351edf173d1cc121ff1e23e3f1e594a97 -size 36342 +oid sha256:c372b6317bc4806ffdbddee45ded7b3a8c9f8ee7b7a462d8ff770b60ce00e176 +size 28629 diff --git a/tests/Images/Input/Exr/Calliphora_b44.exr b/tests/Images/Input/Exr/Calliphora_b44.exr new file mode 100644 index 000000000..ea008e070 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_b44.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a6ac7fd879fc2dae646adaa14382aa10e5b8d02634af4668dd04bce09148151 +size 157973 diff --git a/tests/Images/Input/Exr/Calliphora_benchmark.exr b/tests/Images/Input/Exr/Calliphora_benchmark.exr new file mode 100644 index 000000000..da416b3c8 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_benchmark.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3368860692927e709365f2d37b92411068e77a0f23624ff57af6089ec69f357 +size 2592888 diff --git a/tests/Images/Input/Exr/Calliphora_float_pxr24.exr b/tests/Images/Input/Exr/Calliphora_float_pxr24.exr new file mode 100644 index 000000000..3c5d61e53 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_float_pxr24.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca6c29eb36395b923298d2bf4ba1f73fd7f081f7b1af2e72b94b92ad2acb638b +size 226997 diff --git a/tests/Images/Input/Exr/Calliphora_float_uncompressed.exr b/tests/Images/Input/Exr/Calliphora_float_uncompressed.exr new file mode 100644 index 000000000..04a5c035d --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_float_uncompressed.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:926996c647c1c54921db0a623e58e5edeb92b447cb4eceb53b8c0e86fd24f11e +size 720281 diff --git a/tests/Images/Input/Exr/Calliphora_gray.exr b/tests/Images/Input/Exr/Calliphora_gray.exr new file mode 100644 index 000000000..2aaeafbbe --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_gray.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a5daadcfd4ff0e45282d39d6c54f9a13651da3fd8841abda580e76661555470 +size 124245 diff --git a/tests/Images/Input/Exr/Calliphora_half_pxr24.exr b/tests/Images/Input/Exr/Calliphora_half_pxr24.exr new file mode 100644 index 000000000..17a90699b --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_half_pxr24.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:927778a73832b5ad68473e46df6be52076d98294271f2cd0ead66691db41b7bf +size 195063 diff --git a/tests/Images/Input/Exr/Calliphora_rgb.exr b/tests/Images/Input/Exr/Calliphora_rgb.exr new file mode 100644 index 000000000..5dc90343d --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_rgb.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f54e5e57df1b8cdf1b26418d5696dd9cefbd7ed3bf31cdcde06fd9a7bf5e3724 +size 362681 diff --git a/tests/Images/Input/Exr/Calliphora_rle.exr b/tests/Images/Input/Exr/Calliphora_rle.exr new file mode 100644 index 000000000..a956c9c40 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_rle.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccd25a952240a75d9c8b646be5001a2b006c053314e30cdf12e35ac865a31ae2 +size 292545 diff --git a/tests/Images/Input/Exr/Calliphora_uint32_uncompressed.exr b/tests/Images/Input/Exr/Calliphora_uint32_uncompressed.exr new file mode 100644 index 000000000..867cffaa1 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_uint32_uncompressed.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bed68d2e491cad0bdd0fa668ce57328951927d722f130ba7c5c5475f6a3e0d96 +size 289183 diff --git a/tests/Images/Input/Exr/Calliphora_uint_pxr24.exr b/tests/Images/Input/Exr/Calliphora_uint_pxr24.exr new file mode 100644 index 000000000..fd858abb0 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_uint_pxr24.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc72685224ba818ac77d914f5e4c91162503f88f775e16b5c745baef6bc7b790 +size 187914 diff --git a/tests/Images/Input/Exr/Calliphora_uncompressed.exr b/tests/Images/Input/Exr/Calliphora_uncompressed.exr new file mode 100644 index 000000000..5dc90343d --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_uncompressed.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f54e5e57df1b8cdf1b26418d5696dd9cefbd7ed3bf31cdcde06fd9a7bf5e3724 +size 362681 diff --git a/tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr b/tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr new file mode 100644 index 000000000..86bc1d354 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e9f6b5afa3c10c895ba67b51568295be40c2b1057224437600028487c581291 +size 481899 diff --git a/tests/Images/Input/Exr/Calliphora_zip.exr b/tests/Images/Input/Exr/Calliphora_zip.exr new file mode 100644 index 000000000..82dc0dc39 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_zip.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8b8e3b9a74179cb51a7a85031b3d177e109c20a56392804b10873273ff2163e +size 181871 diff --git a/tests/Images/Input/Exr/Calliphora_zips.exr b/tests/Images/Input/Exr/Calliphora_zips.exr new file mode 100644 index 000000000..6556adf00 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_zips.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c6962a5b06c677648d5e22ff30ba8a28e7801fffb32259984e23bbaa97061b1 +size 220159 diff --git a/tests/Images/Input/Exr/rgba_uint_uncompressed.exr b/tests/Images/Input/Exr/rgba_uint_uncompressed.exr new file mode 100644 index 000000000..2738783b2 --- /dev/null +++ b/tests/Images/Input/Exr/rgba_uint_uncompressed.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b45345cb1e4539e5298ccce3ccfc0f27b7a469836bfd7aa13c4d9f6c52baa7a +size 482731 diff --git a/tests/Images/Input/Gif/issues/issue_3142.gif b/tests/Images/Input/Gif/issues/issue_3142.gif new file mode 100644 index 000000000..6053eebcf --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_3142.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:982c394dbee297bb3bc5cc532827742260acb6e5aa07e1dca3f0b17e57bc3bbe +size 386565 diff --git a/tests/Images/Input/Jpg/icc-profiles/issue-3064.jpg b/tests/Images/Input/Jpg/icc-profiles/issue-3064.jpg new file mode 100644 index 000000000..477372c03 --- /dev/null +++ b/tests/Images/Input/Jpg/icc-profiles/issue-3064.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e02fff450519423fd5746e610d65bd7296553252567e93de9c051250139e8adc +size 27537 diff --git a/tests/Images/Input/Jpg/issues/issue3118-multiple-sof.jpg b/tests/Images/Input/Jpg/issues/issue3118-multiple-sof.jpg new file mode 100644 index 000000000..e8897a511 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/issue3118-multiple-sof.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29d9073bc60cb8f5965993654ec5a949cdbacebe6365db9831ece860095ca85f +size 11541050 diff --git a/tests/Images/Input/Png/animated/issue-animated-frame-count.png b/tests/Images/Input/Png/animated/issue-animated-frame-count.png new file mode 100644 index 000000000..88427f487 --- /dev/null +++ b/tests/Images/Input/Png/animated/issue-animated-frame-count.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af4e320f586ab26c55612a7ccfc98a8c99cd6a0efe8a70d379503751d06fe8bd +size 51542 diff --git a/tests/Images/Input/Png/cgbi/clocks.png b/tests/Images/Input/Png/cgbi/clocks.png new file mode 100644 index 000000000..d7cf18367 --- /dev/null +++ b/tests/Images/Input/Png/cgbi/clocks.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc462d8c2697060cde9a2e975ffb828d822ee3b0d4d12e3c5f081114176c036b +size 389981 diff --git a/tests/Images/Input/Png/cgbi/colors-cgbi-bitdepth16.png b/tests/Images/Input/Png/cgbi/colors-cgbi-bitdepth16.png new file mode 100644 index 000000000..18cfa9246 --- /dev/null +++ b/tests/Images/Input/Png/cgbi/colors-cgbi-bitdepth16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59610bc03f6ca867e5f71c574b3a0d1942c9e3a230c8a32bf3007cb82f286866 +size 12853 diff --git a/tests/Images/Input/Png/cgbi/colors-cgbi-palette.png b/tests/Images/Input/Png/cgbi/colors-cgbi-palette.png new file mode 100644 index 000000000..f6406559b --- /dev/null +++ b/tests/Images/Input/Png/cgbi/colors-cgbi-palette.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3a2f20c69ae423523a8f41887e3f37257a338f2220c2ea44d35c87daf8c3aa3 +size 12853 diff --git a/tests/Images/Input/Png/cgbi/colors.png b/tests/Images/Input/Png/cgbi/colors.png new file mode 100644 index 000000000..9b3b371bd --- /dev/null +++ b/tests/Images/Input/Png/cgbi/colors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f34436f755e3c6d15341f29c992331045ae16ad413144ca798ede5c085c8e6a +size 12853 diff --git a/tests/Images/Input/Png/cgbi/flecks.png b/tests/Images/Input/Png/cgbi/flecks.png new file mode 100644 index 000000000..640044229 --- /dev/null +++ b/tests/Images/Input/Png/cgbi/flecks.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6be7b478594ba5e4d37bc135c881c0a16cf1c804fece5440bab997c7b69182f1 +size 187703 diff --git a/tests/Images/Input/Png/cgbi/screen.png b/tests/Images/Input/Png/cgbi/screen.png new file mode 100644 index 000000000..57eca3d46 --- /dev/null +++ b/tests/Images/Input/Png/cgbi/screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e9bfbac37a57b71fa27b38b21314bd49dbe1cb19a2eb9d0f272ec7be3b72a33 +size 94834 diff --git a/tests/Images/Input/Png/issues/Issue_396_dragon.png b/tests/Images/Input/Png/issues/Issue_396_dragon.png new file mode 100644 index 000000000..4e8b2d891 --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_396_dragon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:203aca58e0aa19cbb7ab0820ef55005af830bf4c9bfd1dd4d9967c7e1bc1d51a +size 494160 diff --git a/tests/Images/Input/Png/issues/Issue_396_hand_1.png b/tests/Images/Input/Png/issues/Issue_396_hand_1.png new file mode 100644 index 000000000..1624bce04 --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_396_hand_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ab791784ac46f9e50a63bd255f39ff8af236a922aab3510a29b473bd93bffd5 +size 16295 diff --git a/tests/Images/Input/Png/issues/Issue_396_hand_2.png b/tests/Images/Input/Png/issues/Issue_396_hand_2.png new file mode 100644 index 000000000..d12fa097b --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_396_hand_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbf54bcef6730b68b18770105a15448f984aaca6910693d4e65106a526af58d8 +size 17756 diff --git a/tests/coverlet.runsettings b/tests/coverlet.runsettings index cffce3540..f327b47a2 100644 --- a/tests/coverlet.runsettings +++ b/tests/coverlet.runsettings @@ -9,7 +9,7 @@ lcov - [SixLabors.*]* + [SixLabors.ImageSharp*]* true