Skip to content

Commit

Permalink
Implement CachedBitmap
Browse files Browse the repository at this point in the history
Implements GDI+ `CachedBitmap` wrapper which allows caching a device dependent copy of `Bitmap`. Rendering is signficantly faster (5x+), although it has a few caveats:

- It needs to be regenerated if color depth changes
- It cannot be rendered to a Graphics object that has a tranform other than translation (no rotation, scaling)

Fixes dotnet#8822
  • Loading branch information
JeremyKuhne committed Apr 13, 2023
1 parent 4ab9606 commit d3a2594
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/System.Drawing.Common/src/System.Drawing.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ Since .NET 7, non-Windows platforms are not supported, even with the runtime con
<PackageReference Include="Microsoft.Win32.SystemEvents" Version="$(MicrosoftWin32SystemEventsPackageVersion)" />
</ItemGroup>

<ItemGroup>
<Compile Include="System\Drawing\Imaging\CachedBitmap.cs" />
</ItemGroup>

<Import Project="resources.targets" />
<Import Project="packaging.targets" />
</Project>
19 changes: 19 additions & 0 deletions src/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4455,6 +4455,25 @@ internal static partial int GdipComment(

[LibraryImport(LibraryName)]
internal static partial int GdipCreateBitmapFromStreamICM(IntPtr stream, IntPtr* bitmap);

#if NET8_0_OR_GREATER
[LibraryImport(LibraryName)]
internal static partial int GdipCreateCachedBitmap(
[MarshalUsing(typeof(HandleRefMarshaller))] HandleRef bitmap,
[MarshalUsing(typeof(HandleRefMarshaller))] HandleRef graphics,
out nint cachedBitmap);

[LibraryImport(LibraryName)]
internal static partial int GdipDeleteCachedBitmap(
nint cachedBitmap);

[LibraryImport(LibraryName)]
internal static partial int GdipDrawCachedBitmap(
[MarshalUsing(typeof(HandleRefMarshaller))] HandleRef graphics,
[MarshalUsing(typeof(HandleRefMarshaller))] HandleRef cachedBitmap,
int x,
int y);
#endif
}
}
}
31 changes: 31 additions & 0 deletions src/System.Drawing.Common/src/System/Drawing/Graphics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3851,5 +3851,36 @@ private static void CheckErrorStatus(int status)
// Legitimate error, throw our status exception.
throw Gdip.StatusException(status);
}

#if NET8_0_OR_GREATER

/// <summary>
/// Draws the given <paramref name="cachedBitmap"/>.
/// </summary>
/// <param name="cachedBitmap">The <see cref="CachedBitmap"/> that contains the image to be drawn.</param>
/// <param name="x">The x-coordinate of the upper-left corner of the drawn image.</param>
/// <param name="y">The y-coordinate of the upper-left corner of the drawn image.</param>
/// <exception cref="ArgumentNullException"><paramref name="cachedBitmap"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">
/// <para>
/// The <paramref name="cachedBitmap"/> is not compatible with the <see cref="Graphics"/> device state.
/// </para>
/// <para>
/// - or -
/// </para>
/// <para>
/// The <see cref="Graphics"/> object has a transform applied other than a translation.
/// </para>
/// </exception>
public void DrawCachedBitmap(CachedBitmap cachedBitmap, int x, int y)
{
ArgumentNullException.ThrowIfNull(cachedBitmap);

Gdip.CheckStatus(Gdip.GdipDrawCachedBitmap(
new(this, NativeGraphics),
new(cachedBitmap, cachedBitmap.Handle),
x, y));
}
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading;

#if NET8_0_OR_GREATER


using static System.Drawing.SafeNativeMethods;

namespace System.Drawing.Imaging;

/// <summary>
/// A device dependent copy of a <see cref="Bitmap"/> matching a specified <see cref="Graphics"/> object's current
/// device (display) settings. Avoids reformatting step when rendering, which can significantly improve performance.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="CachedBitmap"/> matches the current bit depth of the <see cref="Graphics"/>'s device. If the device bit
/// depth changes, the <see cref="CachedBitmap"/> will no longer be usable and a new instance will need to be created
/// that matches. If the <see cref="CachedBitmap"/> was created against <see cref="PixelFormat.Format32bppRgb"/> it
/// will always work.
/// </para>
/// <para>
/// <see cref="CachedBitmap"/> will not work with any transformations other than translation.
/// </para>
/// <para>
/// <see cref="CachedBitmap"/> cannot be used to draw to a printer or metafile.
/// </para>
/// </remarks>
public sealed class CachedBitmap : IDisposable
{
private nint _handle;

/// <summary>
/// Create a device dependent copy of the given <paramref name="bitmap"/> for the device settings of the given
/// <paramref name="graphics"/>
/// </summary>
/// <param name="bitmap">The <see cref="Bitmap"/> to convert.</param>
/// <param name="graphics">The <see cref="Graphics"/> object to use to format the cached copy of the <paramref name="bitmap"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="bitmap"/> or <paramref name="graphics"/> is <see langword="null"/>.</exception>
public CachedBitmap(Bitmap bitmap, Graphics graphics)
{
ArgumentNullException.ThrowIfNull(bitmap);
ArgumentNullException.ThrowIfNull(graphics);

Gdip.CheckStatus(Gdip.GdipCreateCachedBitmap(
new(bitmap, bitmap.nativeImage),
new(graphics, graphics.NativeGraphics),
out _handle));
}

internal nint Handle => _handle;

private void Dispose(bool disposing)
{
nint handle = Interlocked.Exchange(ref _handle, 0);
if (handle == 0)
{
return;
}

int status = Gdip.GdipDeleteCachedBitmap(handle);
if (disposing)
{
// Don't want to throw on the finalizer thread.
Gdip.CheckStatus(status);
}
}

~CachedBitmap() => Dispose(disposing: false);

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
#endif
80 changes: 80 additions & 0 deletions src/System.Drawing.Common/tests/GraphicsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3197,5 +3197,85 @@ private static void VerifyGraphics(Graphics graphics, RectangleF expectedVisible
Assert.Equal(new Matrix(), graphics.Transform);
Assert.Equal(expectedVisibleClipBounds, graphics.VisibleClipBounds);
}

#if NET8_0_OR_GREATER
[Fact]
public void DrawCachedBitmap_ThrowsArgumentNullException()
{
using Bitmap bitmap = new(10, 10);
using Graphics graphics = Graphics.FromImage(bitmap);

Assert.Throws<ArgumentNullException>(() => graphics.DrawCachedBitmap(null!, 0, 0));
}

[Fact]
public void DrawCachedBitmap_DisposedBitmap_ThrowsArgumentException()
{
using Bitmap bitmap = new(10, 10);
using Graphics graphics = Graphics.FromImage(bitmap);
CachedBitmap cachedBitmap = new(bitmap, graphics);
cachedBitmap.Dispose();

Assert.Throws<ArgumentException>(() => graphics.DrawCachedBitmap(cachedBitmap, 0, 0));
}

[Fact]
public void DrawCachedBitmap_Simple()
{
using Bitmap bitmap = new(10, 10);
using Graphics graphics = Graphics.FromImage(bitmap);
using CachedBitmap cachedBitmap = new(bitmap, graphics);

graphics.DrawCachedBitmap(cachedBitmap, 0, 0);
}

[Fact]
public void DrawCachedBitmap_Translate()
{
using Bitmap bitmap = new(10, 10);
using Graphics graphics = Graphics.FromImage(bitmap);
graphics.TranslateTransform(1.0f, 8.0f);
using CachedBitmap cachedBitmap = new(bitmap, graphics);

graphics.DrawCachedBitmap(cachedBitmap, 0, 0);
}

[Fact]
public void DrawCachedBitmap_Rotation_ThrowsInvalidOperation()
{
using Bitmap bitmap = new(10, 10);
using Graphics graphics = Graphics.FromImage(bitmap);
graphics.RotateTransform(36.0f);
using CachedBitmap cachedBitmap = new(bitmap, graphics);

Assert.Throws<InvalidOperationException>(() => graphics.DrawCachedBitmap(cachedBitmap, 0, 0));
}

[Theory]
[InlineData(PixelFormat.Format16bppRgb555, PixelFormat.Format32bppRgb, false)]
[InlineData(PixelFormat.Format32bppRgb, PixelFormat.Format16bppRgb555, true)]
[InlineData(PixelFormat.Format32bppArgb, PixelFormat.Format16bppRgb555, false)]
public void DrawCachedBitmap_ColorDepthChange_ThrowsInvalidOperation(
PixelFormat sourceFormat,
PixelFormat destinationFormat,
bool shouldSucceed)
{
using Bitmap bitmap = new(10, 10, sourceFormat);
using Graphics graphics = Graphics.FromImage(bitmap);
using CachedBitmap cachedBitmap = new(bitmap, graphics);

using Bitmap bitmap2 = new(10, 10, destinationFormat);
using Graphics graphics2 = Graphics.FromImage(bitmap2);

if (shouldSucceed)
{
graphics2.DrawCachedBitmap(cachedBitmap, 0, 0);
}
else
{
Assert.Throws<InvalidOperationException>(() => graphics2.DrawCachedBitmap(cachedBitmap, 0, 0));
}
}
#endif
}
}
2 changes: 1 addition & 1 deletion src/System.Drawing.Common/tests/Imaging/BitmapDataTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
Expand Down
21 changes: 21 additions & 0 deletions src/System.Drawing.Common/tests/Imaging/CachedBitmapTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace System.Drawing.Imaging.Tests;

#if NET8_0_OR_GREATER
public class CachedBitmapTests
{
[Fact]
public void Ctor_Throws_ArgumentNullException()
{
using var bitmap = new Bitmap(10, 10);
using var graphics = Graphics.FromImage(bitmap);

Assert.Throws<ArgumentNullException>(() => new CachedBitmap(bitmap, null));
Assert.Throws<ArgumentNullException>(() => new CachedBitmap(null, graphics));
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<Compile Include="Graphics_GetContextTests.cs" />
<Compile Include="IconTests.cs" />
<Compile Include="ImageTests.cs" />
<Compile Include="Imaging\CachedBitmapTests.cs" />
<Compile Include="Imaging\ImageAttributesTests.cs" />
<Compile Include="Imaging\MetafileTests.cs" />
<Compile Include="Imaging\PropertyItemTests.cs" />
Expand Down

0 comments on commit d3a2594

Please sign in to comment.