Skip to content

Commit 3d95d9d

Browse files
committed
Make IBuffer.ToArray robust to out-of-range arguments
This change makes it possible to retrieve all elements from a BufferView without knowing the count beforehand.
1 parent 6f6c9a9 commit 3d95d9d

5 files changed

Lines changed: 98 additions & 50 deletions

File tree

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
- Fixed support for 64-bit attributes and uniforms
22
- [Sg] Fixed broken Ag rule for `FaceVertexCount`
3+
- Made `IBuffer.ToArray` and `BufferView.download` robust to out-of-range arguments
34

45
### 5.6.4
56
- [GL] Dispose MultimediaTimer in LodRenderer to avoid resource exhaustion

src/Aardvark.Rendering/Resources/Adaptive/BufferView.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,15 @@ module BufferView =
153153
int ((sizeInBytes - uint64 view.Offset) / uint64 view.ElementType.CLRSize)
154154
)
155155

156+
/// Retrieves up to count elements from the given buffer view as an array starting at startIndex.
157+
/// If count < 0, all available elements are retrieved.
156158
let download (startIndex : int) (count : int) (view : BufferView) : aval<Array> =
159+
let startIndex = max startIndex 0
160+
157161
match view.SingleValue with
158162
| Some value ->
163+
let count = if count < 0 then 1 else count
164+
159165
value.Accept {
160166
new IAdaptiveValueVisitor<_> with
161167
member _.Visit(value) = value |> AVal.map (fun value -> Array.replicate count value :> Array)

src/Aardvark.Rendering/Resources/Buffers/BufferExtensions.fs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,10 @@ type IBufferRangeExtensions private() =
342342
[<AbstractClass; Sealed; Extension>]
343343
type IBufferExtensions private() =
344344

345+
static let getAvailableCount (elementSize: uint64) (stride: uint64) (offset: uint64) (totalSize: uint64) =
346+
let availableSize = if offset <= totalSize then totalSize - offset else 0UL
347+
if availableSize >= elementSize then 1UL + (availableSize - elementSize) / stride else 0UL
348+
345349
static let copyToArray (elementType: Type) (elementSize: uint64) (stride: uint64) (offset: uint64) (count: uint64) (src: nativeint) =
346350
let array = Array.CreateInstance(elementType, int64 count)
347351
let sizeInBytes = count * elementSize
@@ -369,30 +373,27 @@ type IBufferExtensions private() =
369373
///<summary>Returns elements of a buffer as an array.</summary>
370374
///<param name="buffer">The buffer from which to retrieve elements.</param>
371375
///<param name="elementType">The element type of the buffer and resulting array.</param>
372-
///<param name="count">The number of elements to retrieve.</param>
376+
///<param name="count">The maximum number of elements to retrieve. Default is UInt64.MaxValue.</param>
373377
///<param name="offset">Offset (in bytes) into the buffer. Default is 0.</param>
374378
///<param name="stride">Number of bytes between elements or 0 for tightly packed data. Default is 0.</param>
375379
///<returns>An array containing the buffer elements.</returns>
376380
[<Extension>]
377-
static member ToArray(buffer: IBuffer, elementType: Type, count: uint64,
381+
static member ToArray(buffer: IBuffer, elementType: Type,
382+
[<Optional; DefaultParameterValue(UInt64.MaxValue)>] count: uint64,
378383
[<Optional; DefaultParameterValue(0UL)>] offset: uint64,
379384
[<Optional; DefaultParameterValue(0UL)>] stride: uint64) : Array =
380385
let elementSize = uint64 elementType.CLRSize
381386
let stride = if stride = 0UL then elementSize else stride
382-
let copySize = if count = 0UL then 0UL else stride * (count - 1UL) + elementSize
383387

384388
match buffer with
385-
| :? ArrayBuffer as buffer ->
386-
if buffer.ElementType <> elementType then
387-
raise <| InvalidCastException($"Expected ArrayBuffer with element type {elementType} but got {buffer.ElementType}.")
389+
| :? ArrayBuffer as buffer when buffer.ElementType = elementType ->
390+
let totalSize = uint64 buffer.Data.Length * elementSize
391+
let available = getAvailableCount elementSize stride offset totalSize
392+
let count = min available count
388393

389394
if count = uint64 buffer.Data.Length && offset = 0UL && stride = elementSize then
390395
buffer.Data
391396
else
392-
let totalSize = uint64 buffer.Data.Length * elementSize
393-
if copySize > totalSize - offset then
394-
raise <| ArgumentOutOfRangeException($"Cannot copy {copySize} bytes from ArrayBuffer with size {totalSize} starting at offset {offset}.")
395-
396397
if stride = elementSize && offset % elementSize = 0UL then
397398
let result = Array.CreateInstance(elementType, int64 count)
398399
Array.Copy(buffer.Data, int64 (offset / elementSize), result, 0L, int64 count)
@@ -401,14 +402,13 @@ type IBufferExtensions private() =
401402
buffer.Data |> NativeInt.pin (copyToArray elementType elementSize stride offset count)
402403

403404
| :? INativeBuffer as buffer ->
404-
if copySize > buffer.SizeInBytes - offset then
405-
raise <| ArgumentOutOfRangeException($"Cannot copy {copySize} bytes from INativeBuffer with size {buffer.SizeInBytes} starting at offset {offset}.")
406-
405+
let available = getAvailableCount elementSize stride offset buffer.SizeInBytes
406+
let count = min available count
407407
buffer.Use(copyToArray elementType elementSize stride offset count)
408408

409409
| :? IBackendBuffer as buffer ->
410-
if copySize > buffer.SizeInBytes - offset then
411-
raise <| ArgumentOutOfRangeException($"Cannot copy {copySize} bytes from IBackendBuffer with size {buffer.SizeInBytes} starting at offset {offset}.")
410+
let available = getAvailableCount elementSize stride offset buffer.SizeInBytes
411+
let count = min available count
412412

413413
if stride = elementSize then
414414
let result = Array.CreateInstance(elementType, int64 count)
@@ -417,6 +417,7 @@ type IBufferExtensions private() =
417417
)
418418
result
419419
else
420+
let copySize = if count = 0UL then 0UL else stride * (count - 1UL) + elementSize
420421
let tmp = Marshal.AllocHGlobal(nativeint copySize)
421422
try
422423
buffer.Download(offset, tmp, copySize)
@@ -429,12 +430,13 @@ type IBufferExtensions private() =
429430

430431
///<summary>Returns elements of a buffer as an array.</summary>
431432
///<param name="buffer">The buffer from which to retrieve elements.</param>
432-
///<param name="count">The number of elements to retrieve.</param>
433+
///<param name="count">The maximum number of elements to retrieve. Default is UInt64.MaxValue.</param>
433434
///<param name="offset">Offset (in bytes) into the buffer. Default is 0.</param>
434435
///<param name="stride">Number of bytes between elements or 0 for tightly packed data. Default is 0.</param>
435436
///<returns>An array containing the buffer elements.</returns>
436437
[<Extension>]
437-
static member inline ToArray<'T when 'T : unmanaged>(buffer: IBuffer, count: uint64,
438+
static member inline ToArray<'T when 'T : unmanaged>(buffer: IBuffer,
439+
[<Optional; DefaultParameterValue(UInt64.MaxValue)>] count: uint64,
438440
[<Optional; DefaultParameterValue(0UL)>] offset: uint64,
439441
[<Optional; DefaultParameterValue(0UL)>] stride: uint64) : 'T[] =
440442
buffer.ToArray(typeof<'T>, count, offset, stride) :?> 'T[]

src/Tests/Aardvark.Rendering.Tests/Tests/Buffer/ToArray.fs

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,71 +28,110 @@ module BufferToArray =
2828
Expect.throwsT<ArgumentOutOfRangeException> (fun _ -> buffer.ToArray(buffer.ElementType, 4UL, offset = 1UL * elementSize) |> ignore) "Expected out-of-range"
2929
Expect.throwsT<ArgumentOutOfRangeException> (fun _ -> buffer.ToArray(buffer.ElementType, 2UL, offset = 1UL * elementSize, stride = 3UL * elementSize) |> ignore) "Expected out-of-range"
3030

31-
let testToArray (referenceEqual: bool) (expected: 'T[]) (offset: uint64) (stride: uint64) (buffer: IBuffer) =
32-
let result = buffer.ToArray<'T>(uint64 expected.Length, offset = offset, stride = stride)
31+
let testToArray (referenceEqual: bool) (expected: 'T[]) (offset: uint64) (stride: uint64) (count: uint64) (buffer: IBuffer) =
32+
let result = buffer.ToArray<'T>(count = count, offset = offset, stride = stride)
3333
Expect.equal result expected "Result not equal"
3434
Expect.equal (obj.ReferenceEquals(result, expected)) referenceEqual "Unexpected reference equality"
3535

3636
let arrayBufferFull (_: IRuntime) =
3737
let array = [| 0; 2; 4; 8 |]
38-
ArrayBuffer array |> testToArray true array 0UL 0UL
38+
ArrayBuffer array |> testToArray true array 0UL 0UL 42UL
3939

4040
let arrayBufferRange (_: IRuntime) =
4141
let buffer = ArrayBuffer [| 0; 2; 4; 8 |]
4242
let elementSize = uint64 buffer.ElementType.CLRSize
43-
buffer |> testToArray false [| 2; 4 |] (1UL * elementSize) 0UL
43+
buffer |> testToArray false [| 2; 4 |] (1UL * elementSize) 0UL 2UL
4444

4545
let arrayBufferStrided (_: IRuntime) =
4646
let buffer = ArrayBuffer [| 0; 2; 4; 8 |]
4747
let elementSize = uint64 buffer.ElementType.CLRSize
48-
buffer |> testToArray false [| 2; 8 |] (1UL * elementSize) (2UL * elementSize)
48+
buffer |> testToArray false [| 2; 8 |] (1UL * elementSize) (2UL * elementSize) 2UL
49+
50+
let arrayBufferOutOfRange (_: IRuntime) =
51+
let buffer = ArrayBuffer [| 0; 2; 4; 8 |]
52+
let elementSize = uint64 buffer.ElementType.CLRSize
53+
buffer |> testToArray false Array.empty<int> (4UL * elementSize) elementSize 1UL
54+
buffer |> testToArray false [| 4 |] (2UL * elementSize) (2UL * elementSize) 2UL
55+
56+
let arrayBufferReinterpreted (_: IRuntime) =
57+
let input = [| 0; 2; 4; 8 |]
58+
let output = input |> Array.map Fun.FloatFromBits
59+
ArrayBuffer input |> testToArray false output 0UL 0UL 42UL
4960

5061
let nativeBufferFull (_: IRuntime) =
5162
let array = [| 0; 2; 4; 8 |]
52-
NativeArrayBuffer array |> testToArray false array 0UL 0UL
63+
NativeArrayBuffer array |> testToArray false array 0UL 0UL 42UL
5364

5465
let nativeBufferRange (_: IRuntime) =
5566
let buffer = NativeArrayBuffer [| 0; 2; 4; 8 |]
5667
let elementSize = uint64 buffer.ElementType.CLRSize
57-
buffer |> testToArray false [| 2; 4 |] (1UL * elementSize) 0UL
68+
buffer |> testToArray false [| 2; 4 |] (1UL * elementSize) 0UL 2UL
5869

5970
let nativeBufferStrided (_: IRuntime) =
6071
let buffer = NativeArrayBuffer [| 0; 2; 4; 8 |]
6172
let elementSize = uint64 buffer.ElementType.CLRSize
62-
buffer |> testToArray false [| 2; 8 |] (1UL * elementSize) (2UL * elementSize)
73+
buffer |> testToArray false [| 2; 8 |] (1UL * elementSize) (2UL * elementSize) 2UL
74+
75+
let nativeBufferOutOfRange (_: IRuntime) =
76+
let buffer = NativeArrayBuffer [| 0; 2; 4; 8 |]
77+
let elementSize = uint64 buffer.ElementType.CLRSize
78+
buffer |> testToArray false Array.empty<int> (4UL * elementSize) elementSize 1UL
79+
buffer |> testToArray false [| 4 |] (2UL * elementSize) (2UL * elementSize) 2UL
80+
81+
let nativeBufferReinterpreted (_: IRuntime) =
82+
let input = [| 0; 2; 4; 8 |]
83+
let output = input |> Array.map Fun.FloatFromBits
84+
NativeArrayBuffer input |> testToArray false output 0UL 0UL 42UL
6385

6486
let backendBufferFull (runtime: IRuntime) =
6587
let array = [| 0; 2; 4; 8; 16; 32 |]
6688
use buffer = runtime.PrepareBuffer(ArrayBuffer array)
67-
buffer |> testToArray false array 0UL 0UL
89+
buffer |> testToArray false array 0UL 0UL 42UL
6890

6991
let backendBufferRange (runtime: IRuntime) =
7092
let array = ArrayBuffer [| 0; 2; 4; 8; 16; 32 |]
7193
let elementSize = uint64 array.ElementType.CLRSize
7294
use buffer = runtime.PrepareBuffer(array)
73-
buffer |> testToArray false [| 8; 16 |] (3UL * elementSize) 0UL
95+
buffer |> testToArray false [| 8; 16 |] (3UL * elementSize) 0UL 2UL
7496

7597
let backendBufferStrided (runtime: IRuntime) =
7698
let array = ArrayBuffer [| 0; 2; 4; 8; 16; 32 |]
7799
let elementSize = uint64 array.ElementType.CLRSize
78100
use buffer = runtime.PrepareBuffer(array)
79-
buffer |> testToArray false [| 2; 16 |] (1UL * elementSize) (3UL * elementSize)
80-
81-
let tests (backend : Backend) =
82-
[
83-
"Invalid arguments", Cases.invalidArgs
101+
buffer |> testToArray false [| 2; 16 |] (1UL * elementSize) (3UL * elementSize) 2UL
84102

85-
"ArrayBuffer full", Cases.arrayBufferFull
86-
"ArrayBuffer range", Cases.arrayBufferRange
87-
"ArrayBuffer strided", Cases.arrayBufferStrided
103+
let backendBufferOutOfRange (runtime: IRuntime) =
104+
let array = ArrayBuffer [| 0; 2; 4; 8; 16; 32 |]
105+
let elementSize = uint64 array.ElementType.CLRSize
106+
use buffer = runtime.PrepareBuffer(array)
107+
buffer |> testToArray false Array.empty<int> (6UL * elementSize) elementSize 1UL
108+
buffer |> testToArray false [| 4 |] (2UL * elementSize) (4UL * elementSize) 2UL
88109

89-
"INativeBuffer full", Cases.nativeBufferFull
90-
"INativeBuffer range", Cases.nativeBufferRange
91-
"INativeBuffer strided", Cases.nativeBufferStrided
110+
let backendBufferReinterpreted (runtime: IRuntime) =
111+
let array = ArrayBuffer [| 0; 2; 4; 8; 16; 32 |]
112+
use buffer = runtime.PrepareBuffer(array)
113+
let output = array.Data |> unbox<int[]> |> Array.map Fun.FloatFromBits
114+
buffer |> testToArray false output 0UL 0UL 42UL
92115

93-
"IBackendBuffer full", Cases.backendBufferFull
94-
"IBackendBuffer range", Cases.backendBufferRange
95-
"IBackendBuffer strided", Cases.backendBufferStrided
116+
let tests (backend : Backend) =
117+
[
118+
"ArrayBuffer full", Cases.arrayBufferFull
119+
"ArrayBuffer range", Cases.arrayBufferRange
120+
"ArrayBuffer strided", Cases.arrayBufferStrided
121+
"ArrayBuffer out-of-range", Cases.arrayBufferOutOfRange
122+
"ArrayBuffer reinterpreted", Cases.arrayBufferReinterpreted
123+
124+
"INativeBuffer full", Cases.nativeBufferFull
125+
"INativeBuffer range", Cases.nativeBufferRange
126+
"INativeBuffer strided", Cases.nativeBufferStrided
127+
"INativeBuffer out-of-range", Cases.nativeBufferOutOfRange
128+
"INativeBuffer reinterpreted", Cases.nativeBufferReinterpreted
129+
130+
"IBackendBuffer full", Cases.backendBufferFull
131+
"IBackendBuffer range", Cases.backendBufferRange
132+
"IBackendBuffer strided", Cases.backendBufferStrided
133+
"IBackendBuffer out-of-range", Cases.backendBufferOutOfRange
134+
"IBackendBuffer reinterpreted", Cases.backendBufferReinterpreted
96135

97136
]
98137
|> prepareCases backend "ToArray"

src/Tests/Aardvark.Rendering.Tests/Tests/Utilities.fs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -334,18 +334,18 @@ module ``Expecto Extensions`` =
334334
let inline approxEqualAux< ^a, ^b when (^a or ^b) : (static member ApproximateEquals : ^a * ^a * float -> bool)> (_ : ^ b) (a : 'a) (b : 'a) (eps : float) =
335335
(((^a or ^b) : (static member ApproximateEquals : ^a * ^a * float -> bool) (a, b, eps)))
336336

337-
let inline approxEquals a b eps msg =
338-
if not (approxEqualAux Unchecked.defaultof<Fun> a b eps) then
339-
Expect.equal a b msg
337+
let inline approxEquals actual expected eps msg =
338+
if not (approxEqualAux Unchecked.defaultof<Fun> actual expected eps) then
339+
Expect.equal actual expected msg
340340

341-
let inline relativeApproxEquals (a : float) (b : float) eps msg =
342-
let scale = max (abs a) (abs b)
341+
let inline relativeApproxEquals (actual: float) (expected: float) eps msg =
342+
let scale = max (abs actual) (abs expected)
343343
if not (Fun.IsTiny(scale, eps)) then
344-
let ra = a / scale
345-
let rb = b / scale
344+
let ra = actual / scale
345+
let re = expected / scale
346346

347-
if not (Fun.ApproximateEquals(ra, rb, eps)) then
348-
Expect.equal a b msg
347+
if not (Fun.ApproximateEquals(ra, re, eps)) then
348+
Expect.equal actual expected msg
349349

350350
module ExpectoOverrides =
351351

0 commit comments

Comments
 (0)