Skip to content

Commit 6bc51a5

Browse files
authored
Fix WriteableBitmap's Write/CopyPixels (Clone) to support up to 4GB backbuffers (#9470)
* Fix WritePixels/CopyPixels (Clone) in WriteableBitmap to work with Bitmaps up to 4GB * Fix WritePixels to work with source buffers up to 4GB * Probe CI with high memory usage * Skip tests with large working set for now
1 parent 23723b9 commit 6bc51a5

File tree

3 files changed

+228
-33
lines changed

3 files changed

+228
-33
lines changed

src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Imaging/BitmapSource.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ public virtual void CopyPixels(Int32Rect sourceRect, IntPtr buffer, int bufferSi
389389
// Demand Site Of origin on the URI if it passes then this information is ok to expose
390390
CheckIfSiteOfOrigin();
391391

392-
CriticalCopyPixels(sourceRect, buffer, bufferSize, stride);
392+
CriticalCopyPixels(sourceRect, buffer, (uint)bufferSize, stride);
393393
}
394394

395395
/// <summary>
@@ -661,7 +661,7 @@ internal unsafe void CriticalCopyPixels(Int32Rect sourceRect, Array pixels, int
661661
if (elementSize == -1)
662662
throw new ArgumentException(SR.Image_InvalidArrayForPixel);
663663

664-
int destBufferSize = checked(elementSize * (pixels.Length - offset));
664+
uint destBufferSize = checked((uint)elementSize * (uint)(pixels.Length - offset));
665665

666666
// Check whether offset is out of bounds manually
667667
if (offset >= pixels.Length)
@@ -678,7 +678,7 @@ internal unsafe void CriticalCopyPixels(Int32Rect sourceRect, Array pixels, int
678678
/// <param name="buffer"></param>
679679
/// <param name="bufferSize"></param>
680680
/// <param name="stride"></param>
681-
internal void CriticalCopyPixels(Int32Rect sourceRect, IntPtr buffer, int bufferSize, int stride)
681+
internal void CriticalCopyPixels(Int32Rect sourceRect, IntPtr buffer, uint bufferSize, int stride)
682682
{
683683
if (buffer == IntPtr.Zero)
684684
throw new ArgumentNullException(nameof(buffer));
@@ -697,7 +697,7 @@ internal void CriticalCopyPixels(Int32Rect sourceRect, IntPtr buffer, int buffer
697697
int minStride = checked(((sourceRect.Width * Format.BitsPerPixel) + 7) / 8);
698698
ArgumentOutOfRangeException.ThrowIfLessThan(stride, minStride);
699699

700-
int minRequiredDestSize = checked((stride * (sourceRect.Height - 1)) + minStride);
700+
uint minRequiredDestSize = checked(((uint)stride * (uint)(sourceRect.Height - 1)) + (uint)minStride);
701701
ArgumentOutOfRangeException.ThrowIfLessThan(bufferSize, minRequiredDestSize);
702702

703703
lock (_syncObject)
@@ -706,7 +706,7 @@ internal void CriticalCopyPixels(Int32Rect sourceRect, IntPtr buffer, int buffer
706706
WicSourceHandle,
707707
ref sourceRect,
708708
(uint)stride,
709-
(uint)bufferSize,
709+
bufferSize,
710710
buffer
711711
));
712712
}

src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Imaging/WriteableBitmap.cs

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ int destinationY
352352

353353
WritePixelsImpl(sourceRect,
354354
sourceBuffer,
355-
sourceBufferSize,
355+
(uint)sourceBufferSize,
356356
sourceBufferStride,
357357
destinationX,
358358
destinationY,
@@ -380,14 +380,11 @@ int destinationY
380380
{
381381
WritePreamble();
382382

383-
int elementSize;
384-
int sourceBufferSize;
385-
Type elementType;
386383
ValidateArrayAndGetInfo(sourceBuffer,
387384
backwardsCompat: false,
388-
out elementSize,
389-
out sourceBufferSize,
390-
out elementType);
385+
out _,
386+
out uint sourceBufferSize,
387+
out Type elementType);
391388

392389
// We accept arrays of arbitrary value types - but not reference types.
393390
if (elementType == null || !elementType.IsValueType)
@@ -425,7 +422,7 @@ int stride
425422
{
426423
WritePreamble();
427424

428-
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
425+
ArgumentOutOfRangeException.ThrowIfZero(bufferSize);
429426
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(stride);
430427

431428
if (sourceRect.IsEmpty || sourceRect.Width <= 0 || sourceRect.Height <= 0)
@@ -448,7 +445,7 @@ int stride
448445

449446
WritePixelsImpl(sourceRect,
450447
buffer,
451-
bufferSize,
448+
(uint)bufferSize,
452449
stride,
453450
destinationX,
454451
destinationY,
@@ -476,14 +473,11 @@ int offset
476473
return;
477474
}
478475

479-
int elementSize;
480-
int sourceBufferSize;
481-
Type elementType;
482476
ValidateArrayAndGetInfo(pixels,
483477
backwardsCompat: true,
484-
out elementSize,
485-
out sourceBufferSize,
486-
out elementType);
478+
out int elementSize,
479+
out uint sourceBufferSize,
480+
out Type elementType);
487481

488482
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(stride);
489483
ArgumentOutOfRangeException.ThrowIfNegative(offset);
@@ -496,7 +490,7 @@ int offset
496490

497491
checked
498492
{
499-
int offsetInBytes = checked(offset * elementSize);
493+
uint offsetInBytes = (uint)offset * (uint)elementSize;
500494
if (offsetInBytes >= sourceBufferSize)
501495
{
502496
// Backwards compat:
@@ -751,7 +745,7 @@ out _pDoubleBufferedBitmap
751745
try
752746
{
753747
Int32Rect rcFull = new Int32Rect(0, 0, _pixelWidth, _pixelHeight);
754-
int bufferSize = checked(_backBufferStride * source.PixelHeight);
748+
uint bufferSize = checked((uint)_backBufferStride * (uint)source.PixelHeight);
755749
source.CriticalCopyPixels(rcFull, _backBuffer, bufferSize, _backBufferStride);
756750
AddDirtyRect(rcFull);
757751
}
@@ -792,7 +786,7 @@ out _pDoubleBufferedBitmap
792786
private void WritePixelsImpl(
793787
Int32Rect sourceRect,
794788
IntPtr sourceBuffer,
795-
int sourceBufferSize,
789+
uint sourceBufferSize,
796790
int sourceBufferStride,
797791
int destinationX,
798792
int destinationY,
@@ -854,7 +848,7 @@ bool backwardsCompat
854848
{
855849
uint finalRowWidthInBits = (uint)((sourceRect.X + sourceRect.Width) * _format.InternalBitsPerPixel);
856850
uint finalRowWidthInBytes = ((finalRowWidthInBits + 7) / 8);
857-
uint requiredBufferSize = (uint)((sourceRect.Y + sourceRect.Height - 1) * sourceBufferStride) + finalRowWidthInBytes;
851+
uint requiredBufferSize = ((uint)(sourceRect.Y + sourceRect.Height - 1) * (uint)sourceBufferStride) + finalRowWidthInBytes;
858852
if (sourceBufferSize < requiredBufferSize)
859853
{
860854
if (backwardsCompat)
@@ -885,14 +879,14 @@ bool backwardsCompat
885879
//
886880
unsafe
887881
{
888-
uint destOffset = (uint)(destinationY * _backBufferStride) + destXbyteOffset;
882+
uint destOffset = ((uint)destinationY * (uint)_backBufferStride) + destXbyteOffset;
889883
byte* pDest = (byte*)_backBuffer.ToPointer();
890884
pDest += destOffset;
891885
uint outputBufferSize = _backBufferSize - destOffset;
892886

893887
byte* pSource = (byte*)sourceBuffer.ToPointer();
894888
pSource += firstPixelByteOffet;
895-
uint inputBufferSize = (uint)sourceBufferSize - firstPixelByteOffet;
889+
uint inputBufferSize = sourceBufferSize - firstPixelByteOffet;
896890

897891
Lock();
898892

@@ -1040,11 +1034,11 @@ internal override void FinalizeCreation()
10401034
/// <param name="sourceBufferSize">
10411035
/// On output, will contain the size of the array.
10421036
/// </param>
1043-
private void ValidateArrayAndGetInfo(Array sourceBuffer,
1044-
bool backwardsCompat,
1045-
out int elementSize,
1046-
out int sourceBufferSize,
1047-
out Type elementType)
1037+
private static void ValidateArrayAndGetInfo(Array sourceBuffer,
1038+
bool backwardsCompat,
1039+
out int elementSize,
1040+
out uint sourceBufferSize,
1041+
out Type elementType)
10481042
{
10491043
//
10501044
// Assure that a valid pixels Array was provided.
@@ -1076,7 +1070,7 @@ private void ValidateArrayAndGetInfo(Array sourceBuffer,
10761070
{
10771071
object exemplar = sourceBuffer.GetValue(0);
10781072
elementSize = Marshal.SizeOf(exemplar);
1079-
sourceBufferSize = firstDimLength * elementSize;
1073+
sourceBufferSize = (uint)firstDimLength * (uint)elementSize;
10801074
elementType = exemplar.GetType();
10811075
}
10821076
}
@@ -1104,7 +1098,7 @@ private void ValidateArrayAndGetInfo(Array sourceBuffer,
11041098
{
11051099
object exemplar = sourceBuffer.GetValue(0, 0);
11061100
elementSize = Marshal.SizeOf(exemplar);
1107-
sourceBufferSize = (firstDimLength * secondDimLength) * elementSize;
1101+
sourceBufferSize = ((uint)firstDimLength * (uint)secondDimLength) * (uint)elementSize;
11081102
elementType = exemplar.GetType();
11091103
}
11101104
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.InteropServices;
6+
7+
namespace System.Windows.Media.Imaging;
8+
9+
[Collection("WriteableBitmapTests")]
10+
public sealed class WriteableBitmapTests
11+
{
12+
// Under 2GB back-buffer (4 channels)
13+
[InlineData(128, 128, 96.0, 96.0)]
14+
[InlineData(256, 512, 96.0, 96.0)]
15+
[InlineData(256, 256, 120.0, 120.0)]
16+
[InlineData(512, 256, 120.0, 120.0)]
17+
[InlineData(10_000, 10_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
18+
[InlineData(20_000, 20_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
19+
// Over 2GB back-buffer (4 channels) -- NOTE: These tests shall not be run on x86 without PAE
20+
[InlineData(25_000, 25_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
21+
[InlineData(30_000, 30_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
22+
[InlineData(32_000, 32_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
23+
[Theory]
24+
public void Constructor_CreationSucceeds_HasCorrectParameters(int width, int height, double dpiX, double dpiY)
25+
{
26+
WriteableBitmap writeableBitmap = new(width, height, dpiX, dpiY, PixelFormats.Pbgra32, null);
27+
28+
// Assert
29+
Assert.Equal(width, writeableBitmap.PixelWidth);
30+
Assert.Equal(height, writeableBitmap.PixelHeight);
31+
32+
Assert.Equal(dpiX, writeableBitmap.DpiX);
33+
Assert.Equal(dpiY, writeableBitmap.DpiY);
34+
35+
Assert.Equal(PixelFormats.Pbgra32, writeableBitmap.Format);
36+
}
37+
38+
// Under 2GB back-buffer (4 channels)
39+
[InlineData(2_000, 2_000, 96.0, 96.0)]
40+
[InlineData(4_000, 4_000, 120, 120)]
41+
[InlineData(10_000, 10_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
42+
[InlineData(20_000, 20_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
43+
// Over 2GB back-buffer (4 channels) -- NOTE: These tests shall not be run on x86 without PAE
44+
[InlineData(25_000, 25_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
45+
[InlineData(32_000, 32_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
46+
[Theory]
47+
public void WritePixels_SmallRect_Safe_Succeeds(int width, int height, double dpiX, double dpiY)
48+
{
49+
const int tileSize = 500;
50+
const int channels = 4;
51+
52+
// Create 1000x1000 rectangle with 4 channels, fill the rectangle with teal color
53+
byte[] smallRect = GC.AllocateUninitializedArray<byte>(tileSize * tileSize * channels);
54+
MemoryMarshal.Cast<byte, uint>(smallRect.AsSpan()).Fill(0xFF00E6FF);
55+
56+
WriteableBitmap writeableBitmap = new(width, height, dpiX, dpiY, PixelFormats.Pbgra32, null);
57+
58+
// Top-Left
59+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize),
60+
smallRect, tileSize * channels, 0, 0);
61+
62+
// Top-Right
63+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize),
64+
smallRect, tileSize * channels, width - tileSize, 0);
65+
66+
// Middle Rect
67+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize),
68+
smallRect, tileSize * channels, (width - tileSize) / 2, (height - tileSize) / 2);
69+
70+
// Bottom-Left
71+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize),
72+
smallRect, tileSize * channels, 0, height - tileSize);
73+
74+
// Bottom-Right
75+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize),
76+
smallRect, tileSize * channels, width - tileSize, height - tileSize);
77+
}
78+
79+
// Under 2GB back-buffer (4 channels)
80+
[InlineData(2_000, 2_000, 96.0, 96.0)]
81+
[InlineData(4_000, 4_000, 120, 120)]
82+
[InlineData(10_000, 10_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
83+
[InlineData(20_000, 20_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
84+
// Over 2GB back-buffer (4 channels) -- NOTE: These tests shall not be run on x86 without PAE
85+
[InlineData(25_000, 25_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
86+
[InlineData(32_000, 32_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
87+
[Theory]
88+
public unsafe void WritePixels_SmallRect_Unsafe_Succeeds(int width, int height, double dpiX, double dpiY)
89+
{
90+
const int tileSize = 500;
91+
const int channels = 4;
92+
93+
// Create 1000x1000 rectangle with 4 channels, fill the rectangle with teal color
94+
Span<byte> smallRect = GC.AllocateUninitializedArray<byte>(tileSize * tileSize * channels, pinned: true);
95+
MemoryMarshal.Cast<byte, uint>(smallRect).Fill(0xFF00E6FF);
96+
97+
WriteableBitmap writeableBitmap = new(width, height, dpiX, dpiY, PixelFormats.Pbgra32, null);
98+
99+
// Top-Left
100+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize), smallRect.AsNativePointer(),
101+
smallRect.Length, tileSize * channels, 0, 0);
102+
103+
// Top-Right
104+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize), smallRect.AsNativePointer(),
105+
smallRect.Length, tileSize * channels, width - tileSize, 0);
106+
107+
// Middle Rect
108+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize), smallRect.AsNativePointer(),
109+
smallRect.Length, tileSize * channels, (width - tileSize) / 2, (height - tileSize) / 2);
110+
111+
// Bottom-Left
112+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize), smallRect.AsNativePointer(),
113+
smallRect.Length, tileSize * channels, 0, height - tileSize);
114+
115+
// Bottom-Right
116+
writeableBitmap.WritePixels(new Int32Rect(0, 0, tileSize, tileSize), smallRect.AsNativePointer(),
117+
smallRect.Length, tileSize * channels, width - tileSize, height - tileSize);
118+
}
119+
120+
// Under 2GB back-buffer (4 channels)
121+
[InlineData(512, 512, 96.0, 96.0)]
122+
[InlineData(4_000, 4_000, 120, 120)]
123+
[InlineData(10_000, 10_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
124+
[InlineData(20_000, 20_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
125+
// Over 2GB back-buffer (4 channels) -- NOTE: These tests shall not be run on x86 without PAE
126+
[InlineData(25_000, 25_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
127+
[InlineData(32_000, 32_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
128+
[Theory]
129+
public void WritePixels_FullRect_Safe_Succeeds(int width, int height, double dpiX, double dpiY)
130+
{
131+
const int channels = 4;
132+
133+
// Create same-sized rectangle with 4 channels, fill the rectangle with teal color
134+
// NOTE: We use uint[] over byte[] to avoid Array.MaxLength limit for single-dims on 2GB+ bitmaps
135+
uint[] bigRect = GC.AllocateUninitializedArray<uint>(width * height);
136+
Array.Fill(bigRect, 0xFF00E6FF);
137+
138+
WriteableBitmap writeableBitmap = new(width, height, dpiX, dpiY, PixelFormats.Pbgra32, null);
139+
140+
// Paint the full rect teal
141+
writeableBitmap.WritePixels(new Int32Rect(0, 0, width, height), bigRect, width * channels, 0, 0);
142+
}
143+
144+
// Under 2GB back-buffer (4 channels)
145+
[InlineData(512, 512, 96.0, 96.0)]
146+
[InlineData(4_000, 4_000, 120, 120)]
147+
[InlineData(10_000, 10_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
148+
[InlineData(20_000, 20_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
149+
// Over 2GB back-buffer (4 channels) -- NOTE: These tests shall not be run on x86 without PAE
150+
[InlineData(25_000, 25_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
151+
[InlineData(32_000, 32_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
152+
[Theory]
153+
public unsafe void WritePixels_FullRect_Unsafe_Succeeds(int width, int height, double dpiX, double dpiY)
154+
{
155+
const int channels = 4;
156+
157+
// Create same-sized rectangle with 4 channels, fill the rectangle with teal color
158+
// NOTE: We use uint[] over byte[] to avoid Array.MaxLength limit for single-dims on 2GB+ bitmaps
159+
Span<uint> bigRect = GC.AllocateUninitializedArray<uint>(width * height, pinned: true);
160+
bigRect.Fill(0xFF00E6FF);
161+
162+
WriteableBitmap writeableBitmap = new(width, height, dpiX, dpiY, PixelFormats.Pbgra32, null);
163+
164+
// Paint the full rect teal
165+
writeableBitmap.WritePixels(new Int32Rect(0, 0, width, height), bigRect.AsNativePointer(),
166+
bigRect.Length * channels, width * channels, 0, 0);
167+
}
168+
169+
// Under 2GB back-buffer (4 channels)
170+
[InlineData(128, 128, 96.0, 96.0)]
171+
[InlineData(256, 512, 96.0, 96.0)]
172+
[InlineData(256, 256, 120.0, 120.0)]
173+
[InlineData(512, 256, 120.0, 120.0)]
174+
[InlineData(10_000, 10_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
175+
[InlineData(20_000, 20_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
176+
// Over 2GB back-buffer (4 channels) -- NOTE: These tests shall not be run on x86 without PAE
177+
[InlineData(25_000, 25_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
178+
[InlineData(32_000, 32_000, 96.0, 96.0, Skip = "Disabled to reduce working set")]
179+
[Theory]
180+
public void Clone_CopyPixels_Succeeds(int width, int height, double dpiX, double dpiY)
181+
{
182+
WriteableBitmap writeableBitmap = new(width, height, dpiX, dpiY, PixelFormats.Pbgra32, null);
183+
184+
// Invoke bitmap copy
185+
BitmapSource bitmapSource = writeableBitmap.Clone();
186+
187+
// Must succeed
188+
Assert.NotNull(bitmapSource);
189+
}
190+
}
191+
192+
public static unsafe class SpanExtensions
193+
{
194+
/// <summary>Retrieves the data pointer of the underlying <see cref="Span{T}"/> data reference.</summary>
195+
/// <param name="span">The <see cref="Span{T}"/> to retrieve a pointer to.</param>
196+
/// <returns>A <see cref="nuint"/> pointer of the underlying data reference.</returns>
197+
/// <remarks>The pointer reference is not pinned, use only on <see langword="fixed"/> buffers or <see langword="stackalloc"/> pointers.</remarks>
198+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
199+
public static nint AsNativePointer<T>(this Span<T> span) => (nint)Unsafe.AsPointer(ref MemoryMarshal.GetReference(span));
200+
201+
}

0 commit comments

Comments
 (0)