-
-
Notifications
You must be signed in to change notification settings - Fork 890
Description
- I have written a descriptive issue title
- I have verified that I am running the latest version of ImageSharp
- I have verified if the problem exist in both
DEBUGandRELEASEmode - I have searched open and closed issues to ensure it has not already been reported
Description
Custom debug logs from MemoryGroup<T>.Allocate(...) call during steps stated below:
// Loading & decoding initial image from disk, 10x10=100 pixels - ok
// Minimal contiguous memory line 40 bytes - ok
Requesting Rgba32[100] (400 bytes) with stride of 40/1000 bytes
Allocation of 1 buffers 100 elements/400 bytes each
// Image cloning with bigger size for resizing, 15x15=225 pixels - ok
// Minimal contiguous memory line for 60 bytes - ok
Requesting Rgba32[225] (900 bytes) with stride of 60/1000 bytes
Allocation of 1 buffers 225 elements/900 bytes each
// Something related to resizing I guess - ok
Requesting Single[35] (140 bytes) with stride of 20/1000 bytes
Allocation of 1 buffers 35 elements/140 bytes each
// Something related to resizing I guess - ok
Requesting Single[35] (140 bytes) with stride of 20/1000 bytes
Allocation of 1 buffers 35 elements/140 bytes each
// That's where everything breaks due to non-contiguous memory allocation
Requesting Vector4[150] (2400 bytes) with stride of 160/1000 bytes
Allocation of 3 buffers 60 elements/960 bytes each
<-- After this call exception occurs -->
Last allocation from log allocates 150 Vector4 elements with a stride of 10 elements which is done by allocating 3 separate buffers of 60 elements each which fullfils requested "contiguous chunks of 10 elements each", so far so good.
Real problem lies inside ResizeWorker class:
ImageSharp/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
Lines 117 to 118 in 993f96e
| // When creating transposedFirstPassBuffer, we made sure it's contiguous: | |
| Span<Vector4> transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.GetSingleSpan(); |
Although comment states that buffer is ensured to be contiguous it's not:
ImageSharp/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
Lines 88 to 91 in 993f96e
| this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D<Vector4>( | |
| this.workerHeight, | |
| destWidth, | |
| AllocationOptions.Clean); |
Allocate2D only ensures that requested memory is aligned by at least chunks of width parameter:
ImageSharp/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
Lines 25 to 35 in 993f96e
| public static Buffer2D<T> Allocate2D<T>( | |
| this MemoryAllocator memoryAllocator, | |
| int width, | |
| int height, | |
| AllocationOptions options = AllocationOptions.None) | |
| where T : struct | |
| { | |
| long groupLength = (long)width * height; | |
| MemoryGroup<T> memoryGroup = memoryAllocator.AllocateGroup<T>(groupLength, width, options); | |
| return new Buffer2D<T>(memoryGroup, width, height); | |
| } |
Which is what happened in logs, ResizeWorker requested 10 strides of 15 elements with alignment of 15.
Atm given Buffer2D API doesn't provide any way to ensure that requested memory occupies single contiguous buffer but I suppose it should be something like this:
public static Buffer2D<T> AllocateContiguous2D<T>(
this MemoryAllocator memoryAllocator,
int width,
int height,
AllocationOptions options = AllocationOptions.None)
where T : struct
{
long groupLength = (long)width * height;
MemoryGroup<T> memoryGroup = memoryAllocator.AllocateGroup<T>(
groupLength, // passing total size as buffer size
groupLength, // passing total size as alignment
options);
return new Buffer2D<T>(memoryGroup, width, height);
}Steps to Reproduce
Use this code snippet:
Configuration.Default.MemoryAllocator =
new ImageSharp.Memory.ArrayPoolMemoryAllocator(
maxPoolSizeInBytes: 1000,
poolSelectorThresholdInBytes: 1, // [not relevant]
maxArraysPerBucketLargePool: 1000,
maxArraysPerBucketNormalPool: 1, // [not relevant]
bufferCapacityInBytes: 1000
);
var image = Image.Load("IMAGE_PATH");
image.Mutate(op => op.Resize(15, 15));With this 10x10 pixels plain black png image:
https://user-images.githubusercontent.com/20967409/117883361-fdb01e80-b2b3-11eb-850f-10c66309430d.png
Yes, it can be broken even with current default config allocator but with a bit more extreme example:
var image = Image.Load("IMAGE_PATH");
image.Mutate(op => op.Resize(10000, 1));With this 1x4096 pixels plain black png image:
https://user-images.githubusercontent.com/20967409/117890174-56d08000-b2bd-11eb-93f3-ad625787f0be.png