Browse Source

Super-optimized GenericBlock8x8<T> to replace PixelArea<T> in JpegEncoder

af/merge-core
Anton Firszov 8 years ago
parent
commit
b484dd401e
  1. 26
      src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.cs
  2. 43
      src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.tt
  3. 128
      src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.cs
  4. 4
      src/ImageSharp/Formats/Jpeg/GolangPort/Components/Encoder/RgbToYCbCrTables.cs
  5. 2
      src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs
  6. 2
      src/ImageSharp/Formats/Jpeg/GolangPort/Utils/OrigJpegUtils.cs
  7. 9
      src/ImageSharp/ImageSharp.csproj
  8. 126
      tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs
  9. 12
      tests/ImageSharp.Tests/Formats/Jpg/JpegUtilsTests.cs

26
src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using System.Runtime.CompilerServices;
// <auto-generated />
namespace SixLabors.ImageSharp.Formats.Jpeg.Common
{
internal unsafe partial struct GenericBlock8x8<T>
{
#pragma warning disable 169
// It's not allowed use fix-sized buffers with generics, need to place all the fields manually:
private T _y0_x0, _y0_x1, _y0_x2, _y0_x3, _y0_x4, _y0_x5, _y0_x6, _y0_x7;
private T _y1_x0, _y1_x1, _y1_x2, _y1_x3, _y1_x4, _y1_x5, _y1_x6, _y1_x7;
private T _y2_x0, _y2_x1, _y2_x2, _y2_x3, _y2_x4, _y2_x5, _y2_x6, _y2_x7;
private T _y3_x0, _y3_x1, _y3_x2, _y3_x3, _y3_x4, _y3_x5, _y3_x6, _y3_x7;
private T _y4_x0, _y4_x1, _y4_x2, _y4_x3, _y4_x4, _y4_x5, _y4_x6, _y4_x7;
private T _y5_x0, _y5_x1, _y5_x2, _y5_x3, _y5_x4, _y5_x5, _y5_x6, _y5_x7;
private T _y6_x0, _y6_x1, _y6_x2, _y6_x3, _y6_x4, _y6_x5, _y6_x6, _y6_x7;
private T _y7_x0, _y7_x1, _y7_x2, _y7_x3, _y7_x4, _y7_x5, _y7_x6, _y7_x7;
#pragma warning restore 169
}
}

43
src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.tt

@ -0,0 +1,43 @@
<#
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
#>
<#@ 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 and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using System.Runtime.CompilerServices;
// <auto-generated />
namespace SixLabors.ImageSharp.Formats.Jpeg.Common
{
internal unsafe partial struct GenericBlock8x8<T>
{
#pragma warning disable 169
// It's not allowed use fix-sized buffers with generics, need to place all the fields manually:
<#
PushIndent(" ");
Write(" ");
for (int y = 0; y < 8; y++)
{
Write("private T ");
for (int x = 0; x < 8; x++)
{
Write($"_y{y}_x{x}");
if (x < 7) Write(", ");
}
WriteLine(";");
}
PopIndent();
#>
#pragma warning restore 169
}
}

128
src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.cs

@ -0,0 +1,128 @@
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Formats.Jpeg.Common
{
/// <summary>
/// A generic 8x8 block implementation, useful for manipulating custom 8x8 pixel data.
/// </summary>
// ReSharper disable once InconsistentNaming
internal unsafe partial struct GenericBlock8x8<T>
where T : struct
{
public const int Size = 64;
public const int SizeInBytes = Size * 3;
public void LoadAndStretchEdges<TPixel>(IPixelSource<TPixel> source, int sourceX, int sourceY)
where TPixel : struct, IPixel<TPixel>
{
var buffer = source.PixelBuffer as Buffer2D<T>;
if (buffer == null)
{
throw new InvalidOperationException("LoadAndStretchEdges<TPixels>() is only valid for TPixel == T !");
}
this.LoadAndStretchEdges(buffer, sourceX, sourceY);
}
/// <summary>
/// Load a 8x8 region of an image into the block.
/// The "outlying" area of the block will be stretched out with pixels on the right and bottom edge of the image.
/// </summary>
public void LoadAndStretchEdges(Buffer2D<T> source, int sourceX, int sourceY)
{
int width = Math.Min(8, source.Width - sourceX);
int height = Math.Min(8, source.Height - sourceY);
if (width <= 0 || height <= 0)
{
return;
}
uint byteWidth = (uint)width * (uint)Unsafe.SizeOf<T>();
int remainderXCount = 8 - width;
ref byte blockStart = ref Unsafe.As<GenericBlock8x8<T>, byte>(ref this);
ref byte imageStart = ref Unsafe.As<T, byte>(
ref Unsafe.Add(ref source.GetRowSpan(sourceY).DangerousGetPinnableReference(), sourceX)
);
int blockRowSizeInBytes = 8 * Unsafe.SizeOf<T>();
int imageRowSizeInBytes = source.Width * Unsafe.SizeOf<T>();
for (int y = 0; y < height; y++)
{
ref byte s = ref Unsafe.Add(ref imageStart, y * imageRowSizeInBytes);
ref byte d = ref Unsafe.Add(ref blockStart, y * blockRowSizeInBytes);
Unsafe.CopyBlock(ref d, ref s, byteWidth);
ref T last = ref Unsafe.Add(ref Unsafe.As<byte, T>(ref d), width - 1);
for (int x = 1; x <= remainderXCount; x++)
{
Unsafe.Add(ref last, x) = last;
}
}
int remainderYCount = 8 - height;
if (remainderYCount == 0)
{
return;
}
ref byte lastRowStart = ref Unsafe.Add(ref blockStart, (height - 1) * blockRowSizeInBytes);
for (int y = 1; y <= remainderYCount; y++)
{
ref byte remStart = ref Unsafe.Add(ref lastRowStart, blockRowSizeInBytes * y);
Unsafe.CopyBlock(ref remStart, ref lastRowStart, (uint)blockRowSizeInBytes);
}
}
/// <summary>
/// ONLY FOR GenericBlock instances living on the stack!
/// </summary>
public Span<T> AsSpanUnsafe() => new Span<T>(Unsafe.AsPointer(ref this), Size);
/// <summary>
/// FOR TESTING ONLY!
/// Gets or sets a <see cref="Rgb24"/> value at the given index
/// </summary>
/// <param name="idx">The index</param>
/// <returns>The value</returns>
public T this[int idx]
{
get
{
ref T selfRef = ref Unsafe.As<GenericBlock8x8<T>, T>(ref this);
return Unsafe.Add(ref selfRef, idx);
}
set
{
ref T selfRef = ref Unsafe.As<GenericBlock8x8<T>, T>(ref this);
Unsafe.Add(ref selfRef, idx) = value;
}
}
/// <summary>
/// FOR TESTING ONLY!
/// Gets or sets a value in a row+coulumn of the 8x8 block
/// </summary>
/// <param name="x">The x position index in the row</param>
/// <param name="y">The column index</param>
/// <returns>The value</returns>
public T this[int x, int y]
{
get => this[(y * 8) + x];
set => this[(y * 8) + x] = value;
}
}
}

4
src/ImageSharp/Formats/Jpeg/GolangPort/Components/Encoder/RgbToYCbCrTables.cs

@ -8,6 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Encoder
/// <summary>
/// Provides 8-bit lookup tables for converting from Rgb to YCbCr colorspace.
/// Methods to build the tables are based on libjpeg implementation.
/// TODO: Replace this logic with SIMD conversion (similar to the one in the decoder)!
/// </summary>
internal unsafe struct RgbToYCbCrTables
{
@ -91,6 +92,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Encoder
}
/// <summary>
/// TODO: Replace this logic with SIMD conversion (similar to the one in the decoder)!
/// Optimized method to allocates the correct y, cb, and cr values to the DCT blocks from the given r, g, b values.
/// </summary>
/// <param name="yBlockRaw">The The luminance block.</param>
@ -102,7 +104,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Encoder
/// <param name="g">The green value.</param>
/// <param name="b">The blue value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Allocate(ref float* yBlockRaw, ref float* cbBlockRaw, ref float* crBlockRaw, ref RgbToYCbCrTables* tables, int index, int r, int g, int b)
public static void Rgb2YCbCr(float* yBlockRaw, float* cbBlockRaw, float* crBlockRaw, ref RgbToYCbCrTables* tables, int index, int r, int g, int b)
{
// float y = (0.299F * r) + (0.587F * g) + (0.114F * b);
yBlockRaw[index] = (tables->YRTable[r] + tables->YGTable[g] + tables->YBTable[b]) >> ScaleBits;

2
src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs

@ -331,7 +331,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort
int index = j8 + i;
RgbToYCbCrTables.Allocate(ref yBlockRaw, ref cbBlockRaw, ref crBlockRaw, ref tables, index, r, g, b);
RgbToYCbCrTables.Rgb2YCbCr(yBlockRaw, cbBlockRaw, crBlockRaw, ref tables, index, r, g, b);
dataIdx += 3;
}

2
src/ImageSharp/Formats/Jpeg/GolangPort/Utils/OrigJpegUtils.cs

@ -8,7 +8,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils
{
/// <summary>
/// Jpeg specific utilities and extension methods
/// Jpeg specific utilities and extension methods
/// </summary>
internal static class OrigJpegUtils
{

9
src/ImageSharp/ImageSharp.csproj

@ -62,6 +62,10 @@
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>Block8x8F.Generated.cs</LastGenOutput>
</None>
<None Update="Formats\Jpeg\Common\GenericBlock8x8.Generated.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>GenericBlock8x8.Generated.cs</LastGenOutput>
</None>
<None Update="Formats\Jpeg\Components\Block8x8F.Generated.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>Block8x8F.Generated.cs</LastGenOutput>
@ -92,6 +96,11 @@
<AutoGen>True</AutoGen>
<DependentUpon>Block8x8F.Generated.tt</DependentUpon>
</Compile>
<Compile Update="Formats\Jpeg\Common\GenericBlock8x8.Generated.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>GenericBlock8x8.Generated.tt</DependentUpon>
</Compile>
<Compile Update="Formats\Jpeg\Components\Block8x8F.Generated.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>

126
tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs

@ -0,0 +1,126 @@
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
using System;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Jpeg.Common;
using SixLabors.ImageSharp.Helpers;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
using Xunit;
public class GenericBlock8x8Tests
{
public static Image<TPixel> CreateTestImage<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
var image = new Image<TPixel>(10, 10);
using (PixelAccessor<TPixel> pixels = image.Lock())
{
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
var rgba = new Rgba32((byte)(i+1), (byte)(j+1), (byte)200, (byte)255);
var color = default(TPixel);
color.PackFromRgba32(rgba);
pixels[i, j] = color;
}
}
}
return image;
}
[Theory]
[WithMemberFactory(nameof(CreateTestImage), PixelTypes.Rgb24 | PixelTypes.Rgba32 /* | PixelTypes.Rgba32 | PixelTypes.Argb32*/)]
public void LoadAndStretchCorners_FromOrigo<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> s = provider.GetImage())
{
var d = default(GenericBlock8x8<TPixel>);
d.LoadAndStretchEdges(s.Frames.RootFrame, 0, 0);
TPixel a = s.Frames.RootFrame[0, 0];
TPixel b = d[0, 0];
Assert.Equal(s[0, 0], d[0, 0]);
Assert.Equal(s[1, 0], d[1, 0]);
Assert.Equal(s[7, 0], d[7, 0]);
Assert.Equal(s[0, 1], d[0, 1]);
Assert.Equal(s[1, 1], d[1, 1]);
Assert.Equal(s[7, 0], d[7, 0]);
Assert.Equal(s[0, 7], d[0, 7]);
Assert.Equal(s[7, 7], d[7, 7]);
}
}
[Theory]
[WithMemberFactory(nameof(CreateTestImage), PixelTypes.Rgb24 | PixelTypes.Rgba32)]
public unsafe void LoadAndStretchCorners_WithOffset<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> s = provider.GetImage())
{
var d = default(GenericBlock8x8<TPixel>);
d.LoadAndStretchEdges(s.Frames.RootFrame, 6, 7);
Assert.Equal(s[6, 7], d[0, 0]);
Assert.Equal(s[6, 8], d[0, 1]);
Assert.Equal(s[7, 8], d[1, 1]);
Assert.Equal(s[6, 9], d[0, 2]);
Assert.Equal(s[6, 9], d[0, 3]);
Assert.Equal(s[6, 9], d[0, 7]);
Assert.Equal(s[7, 9], d[1, 2]);
Assert.Equal(s[7, 9], d[1, 3]);
Assert.Equal(s[7, 9], d[1, 7]);
Assert.Equal(s[9, 9], d[3, 2]);
Assert.Equal(s[9, 9], d[3, 3]);
Assert.Equal(s[9, 9], d[3, 7]);
Assert.Equal(s[9, 7], d[3, 0]);
Assert.Equal(s[9, 7], d[4, 0]);
Assert.Equal(s[9, 7], d[7, 0]);
Assert.Equal(s[9, 9], d[3, 2]);
Assert.Equal(s[9, 9], d[4, 2]);
Assert.Equal(s[9, 9], d[7, 2]);
Assert.Equal(s[9, 9], d[4, 3]);
Assert.Equal(s[9, 9], d[7, 7]);
}
}
[Fact]
public void Indexer()
{
var block = default(GenericBlock8x8<Rgb24>);
Span<Rgb24> span = block.AsSpanUnsafe();
Assert.Equal(64, span.Length);
for (int i = 0; i < 64; i++)
{
span[i] = new Rgb24((byte)i, (byte)(2 * i), (byte)(3 * i));
}
Rgb24 expected00 = new Rgb24(0, 0, 0);
Rgb24 expected07 = new Rgb24(7, 14, 21);
Rgb24 expected11 = new Rgb24(9, 18, 27);
Rgb24 expected77 = new Rgb24(63, 126, 189);
Rgb24 expected67 = new Rgb24(62, 124, 186);
Assert.Equal(expected00, block[0, 0]);
Assert.Equal(expected07, block[7, 0]);
Assert.Equal(expected11, block[1, 1]);
Assert.Equal(expected67, block[6, 7]);
Assert.Equal(expected77, block[7, 7]);
}
}
}

12
tests/ImageSharp.Tests/Formats/Jpg/JpegUtilsTests.cs

@ -1,18 +1,14 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
using System.Numerics;
using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
public class JpegUtilsTests
{
public static Image<TPixel> CreateTestImage<TPixel>()

Loading…
Cancel
Save