1 /// A memory allocator for Gfx-d
2 module gfx.memalloc;
3
4 import gfx.core.log : LogTag;
5 import gfx.core.rc : AtomicRefCounted, IAtomicRefCounted;
6 import gfx.graal.device : Device;
7 import gfx.graal.memory : DeviceMemory, MemoryProperties, MemoryRequirements,
8 MemoryType, MemProps;
9
10 enum gfxMemallocLogMask = 0x1000_0000;
11 package immutable gfxMemallocLog = LogTag("GFX-MEMALLOC", gfxMemallocLogMask);
12
13 /// Option flags for creating an Allocator
14 enum AllocatorFlags
15 {
16 /// Default behavior, no flag set
17 none = 0,
18 /// If set, each allocation will have a dedicated DeviceMemory
19 /// Even if not set, some backends (i.e. OpenGL) require a dedicated DeviceMemory
20 /// per allocation and will do so regardless of this flag.
21 dedicatedOnly = 1,
22 }
23
24 /// Option to define allocation behavior for each heap of the device
25 struct HeapOptions
26 {
27 /// How many bytes may be use on the heap.
28 /// set to 0 to forbid use of a specific heap and to size_t.max to allow entire use
29 size_t maxUsage = size_t.max;
30 /// Size of a single DeviceMemory on this heap.
31 /// set to 0 to use default behavior for this heap
32 size_t blockSize = 0;
33 }
34
35 /// Options for the creation of an Allocator
36 struct AllocatorOptions
37 {
38 /// option flags
39 AllocatorFlags flags;
40 /// One HeapOption per heap in the device, or empty to use default behavior.
41 /// Default behavior is to allow use of entire heap. Default block size is
42 /// 256Mb for heaps > 1Gb, and heapSize/8 for smaller ones.
43 HeapOptions[] heapOptions;
44 }
45
46 /// Flags controlling an allocation of memory
47 enum AllocFlags {
48 /// default behavior, no flags.
49 none = 0,
50 /// Set to force the creation of a new DeviceMemory, that will be dedicated for the allocation.
51 dedicated = 1,
52 /// Set to prohib the creation of a new DeviceMemory. This forces the use of an existing chunk, and fails if it cannot find one.
53 neverAllocate = 2,
54 }
55
56 /// Describes the usage of a memory allocation
57 enum MemoryUsage {
58 /// No intended usage. The type of memory will not be influenced by the usage.
59 unknown,
60 /// Memory will be used on device only (MemProps.deviceLocal) and having it mappable
61 /// on host is not requested (although it is possible on some devices).
62 /// Usage:
63 /// $(UL
64 /// $(LI Resources written and read by device, e.g. images used as attachments. )
65 /// $(LI Resources transferred from host once or infrequently and read by device multiple times,
66 /// e.g. textures, vertex bufers, uniforms etc. )
67 /// )
68 gpuOnly,
69 /// Memory will be mappable on host. It usually means CPU (system) memory.
70 /// Resources created for this usage may still be accessible to the device,
71 /// but access to them can be slower. Guarantees to be MemProps.hostVisible and MemProps.hostCoherent.
72 /// Usage:
73 /// $(UL $(LI Staging copy of resources used as transfer source.))
74 cpuOnly,
75 /// Memory that is both mappable on host (guarantees to be MemProps.hostVisible)
76 /// and preferably fast to access by GPU. CPU reads may be uncached and very slow.
77 /// Usage:
78 /// $(UL $(LI Resources written frequently by host (dynamic), read by device.
79 /// E.g. textures, vertex buffers, uniform buffers updated every frame or every draw call.))
80 cpuToGpu,
81 /// Memory mappable on host (guarantees to be MemProps.hostVisible) and cached.
82 /// Usage:
83 /// $(UL
84 /// $(LI Resources written by device, read by host - results of some computations,
85 /// e.g. screen capture, average scene luminance for HDR tone mapping.)
86 /// $(LI Any resources read or accessed randomly on host, e.g. CPU-side copy of
87 /// vertex buffer used as source of transfer, but also used for collision detection.)
88 /// )
89 gpuToCpu,
90 }
91
92 /// Structure controlling an allocation of memory
93 struct AllocOptions {
94 /// Control flags
95 AllocFlags flags;
96 /// Intended usage. Will affect preferredBits and requiredBits;
97 MemoryUsage usage;
98 /// MemProps bits that are optional but are preferred to be present.
99 /// Allocation will favor memory types with these bits if available, but may
100 /// fallback to other memory types.
101 MemProps preferredProps;
102 /// MemProps bits that must be set.
103 /// Allocation will fail if it can't allocate a memory type satisfies all bits.
104 MemProps requiredProps;
105 /// mask of memory type indices (0b0101 means indices 0 and 2) that, if not
106 /// zero, will constrain MemoryRequirement.memTypeMask
107 uint memTypeIndexMask;
108
109 /// Initializes an AllocOptions with usage
110 static @property AllocOptions forUsage(MemoryUsage usage) {
111 AllocOptions options;
112 options.usage = usage;
113 return options;
114 }
115 /// set flags to options
116 AllocOptions withFlags(AllocFlags flags) {
117 this.flags = flags;
118 return this;
119 }
120 /// set preferredProps to options
121 AllocOptions withPreferredProps(MemProps props) {
122 this.preferredProps = props;
123 return this;
124 }
125 /// set requiredProps to options
126 AllocOptions withRequiredBits(MemProps props) {
127 this.requiredProps = props;
128 return this;
129 }
130 /// set type index mask to options
131 AllocOptions withTypeIndexMask(uint indexMask) {
132 this.memTypeIndexMask = indexMask;
133 return this;
134 }
135 }
136
137
138
139 /// Create an Allocator for device and options
140 Allocator createAllocator(Device device, AllocatorOptions options)
141 {
142 import gfx.graal : Backend;
143 if ((options.flags & AllocatorFlags.dedicatedOnly) || device.instance.backend == Backend.gl3) {
144 import gfx.memalloc.dedicated : DedicatedAllocator;
145 return new DedicatedAllocator(device, options);
146 }
147 else {
148 import gfx.memalloc.pool : PoolAllocator;
149 return new PoolAllocator(device, options);
150 }
151 }
152
153 /// Memory allocator for a device
154 abstract class Allocator : AtomicRefCounted
155 {
156 import gfx.core.rc : Rc;
157 import gfx.graal.buffer : BufferUsage;
158 import gfx.graal.image : ImageInfo;
159
160 package Device _device;
161 package AllocatorOptions _options;
162 package MemoryProperties _memProps;
163 package size_t _linearOptimalGranularity;
164
165 package this(Device device, AllocatorOptions options)
166 {
167 import gfx.core.rc : retainObj;
168
169 _device = retainObj(device);
170 _options = options;
171 _memProps = device.physicalDevice.memoryProperties;
172 _linearOptimalGranularity = device.physicalDevice.limits.linearOptimalGranularity;
173
174 import std.algorithm : all;
175 import std.exception : enforce;
176 enforce(_memProps.types.all!(mt => mt.heapIndex < _memProps.heaps.length));
177 }
178
179 override void dispose()
180 {
181 import gfx.core.rc : releaseObj;
182
183 releaseObj(_device);
184 }
185
186 /// Device this allocator is bound to.
187 final @property Device device() {
188 return _device;
189 }
190
191 /// Allocate memory for the given requirements
192 /// Returns: A MemAlloc object
193 /// Throws: An Exception if memory could not be allocated
194 final MemAlloc allocate (in MemoryRequirements requirements,
195 in AllocOptions options=AllocOptions.init)
196 {
197 AllocResult res;
198 if (allocateRaw(requirements, options, ResourceLayout.unknown, res)) {
199 return new MemAlloc(
200 res.mem, res.offset, requirements.size, res.block, res.blockData
201 );
202 }
203 else {
204 import std.format : format;
205 throw new Exception(format(
206 "Could not allocate memory for requirements: %s", requirements
207 ));
208 }
209 }
210
211 /// Create a buffer, then allocate and bind memory for its requirements
212 final BufferAlloc allocateBuffer (in BufferUsage usage, in size_t size,
213 in AllocOptions options=AllocOptions.init)
214 {
215 auto buf = _device.createBuffer(usage, size);
216 const requirements = buf.memoryRequirements;
217 AllocResult res;
218 if (allocateRaw(requirements, options, ResourceLayout.linear, res)) {
219 buf.bindMemory(res.mem, res.offset);
220 return new BufferAlloc(
221 buf, res.offset, requirements.size, res.block, res.blockData
222 );
223 }
224 else {
225 import std.format : format;
226 throw new Exception(format(
227 "Could not allocate memory for buffer with usage %s and size %s",
228 usage, size
229 ));
230 }
231 }
232
233 /// Create an image, then allocate and bind memory for its requirements
234 final ImageAlloc allocateImage (in ImageInfo info,
235 in AllocOptions options=AllocOptions.init)
236 {
237 import gfx.graal.image : ImageTiling;
238
239 auto img = _device.createImage(info);
240 const requirements = img.memoryRequirements;
241 const layout = info.tiling == ImageTiling.optimal ? ResourceLayout.optimal : ResourceLayout.linear;
242 AllocResult res;
243 if (allocateRaw(requirements, options, layout, res)) {
244 img.bindMemory(res.mem, res.offset);
245 return new ImageAlloc(
246 img, res.offset, requirements.size, res.block, res.blockData
247 );
248 }
249 else {
250 import std.format : format;
251 throw new Exception(format(
252 "Could not allocate memory for image with info %s", info
253 ));
254 }
255 }
256
257 AllocStats collectStats() {
258 return AllocStats.init;
259 }
260
261 /// Attempt to allocate memory for the given index and for given requirements.
262 /// If successful, result is filled with necessary data.
263 /// Returns: true if successful, false otherwise.
264 abstract protected bool tryAllocate (in MemoryRequirements requirements,
265 in uint memoryTypeIndex,
266 in AllocOptions options,
267 in ResourceLayout layout,
268 ref AllocResult result)
269 in {
270 assert(memoryTypeIndex < _memProps.types.length);
271 assert(
272 ((1 << memoryTypeIndex) & requirements.memTypeMask) != 0,
273 "memoryTypeIndex is not compatible with requirements"
274 );
275 }
276
277 private final bool allocateRaw (in MemoryRequirements requirements,
278 in AllocOptions options,
279 in ResourceLayout layout,
280 ref AllocResult result)
281 {
282 uint allowedMask = requirements.memTypeMask;
283 uint index = findMemoryTypeIndex(_memProps.types, allowedMask, options);
284 if (index != uint.max) {
285 if (tryAllocate(requirements, index, options, layout, result)) return true;
286
287 while (allowedMask != 0) {
288 // retrieve former index from possible choices
289 allowedMask &= ~(1 << index);
290 index = findMemoryTypeIndex(_memProps.types, allowedMask, options);
291 if (index == uint.max) continue;
292 if (tryAllocate(requirements, index, options, layout, result)) return true;
293 }
294 }
295
296 return false;
297 }
298 }
299
300
301 /// Represent a single allocation within a DeviceMemory
302 class MemAlloc : AtomicRefCounted
303 {
304 import gfx.core.rc : Rc;
305 import gfx.graal.memory : MemoryMap;
306
307 private DeviceMemory _mem;
308 private size_t _offset;
309 private size_t _size;
310 private MemBlock _block;
311 private Object _blockData;
312 private size_t _mapCount;
313 private void* _mapPtr;
314 private bool _dedicated;
315
316 package this(DeviceMemory mem, size_t offset, size_t size,
317 MemBlock block, Object blockData)
318 {
319 import gfx.core.rc : retainObj;
320
321 _mem = retainObj(mem);
322 _offset = offset;
323 _size = size;
324 _block = retainObj(block);
325 _blockData = blockData;
326 _dedicated = mem.size == size;
327 }
328
329 override void dispose()
330 {
331 import gfx.core.rc : releaseObj;
332
333 _block.free(_blockData);
334 releaseObj(_mem);
335 releaseObj(_block);
336 }
337
338 final @property size_t offset() const {
339 return _offset;
340 }
341
342 final @property size_t size() const {
343 return _size;
344 }
345
346 final @property DeviceMemory mem() {
347 return _mem;
348 }
349
350 /// Artificially increment the mapping reference count in order
351 /// to keep the memory mapped even if no MemoryMap is alive
352 final void retainMap() {
353 if (_dedicated) {
354 dedicatedMap();
355 }
356 else {
357 _block.map();
358 }
359 }
360
361 final void releaseMap() {
362 if (_dedicated) {
363 dedicatedUnmap();
364 }
365 else {
366 _block.unmap();
367 }
368 }
369
370 final MemoryMap map(in size_t offset=0, in size_t size=size_t.max)
371 {
372 import std.algorithm : min;
373
374 const off = this.offset + offset;
375 const sz = min(this.size-offset, size);
376 void* ptr;
377 void delegate() unmap;
378
379 if (_dedicated) {
380 dedicatedMap();
381 ptr = _mapPtr;
382 unmap = &dedicatedUnmap;
383 }
384 else {
385 ptr = _block.map();
386 unmap = &_block.unmap;
387 }
388
389 auto data = ptr[off .. off+sz];
390 return MemoryMap (_mem, off, data, unmap);
391 }
392
393 private void dedicatedMap() {
394 if (!_mapCount) _mapPtr = _mem.mapRaw(0, _mem.size);
395 ++_mapCount;
396 }
397
398 private void dedicatedUnmap() {
399 --_mapCount;
400 if (!_mapCount) {
401 _mem.unmapRaw();
402 _mapPtr = null;
403 }
404 }
405 }
406
407 final class BufferAlloc : MemAlloc
408 {
409 import gfx.graal.buffer : Buffer;
410
411 private Buffer _buffer;
412
413 package this (Buffer buffer, size_t offset, size_t size, MemBlock block, Object blockData)
414 {
415 import gfx.core.rc : retainObj;
416
417 super(buffer.boundMemory, offset, size, block, blockData);
418 _buffer = retainObj(buffer);
419 }
420
421 override void dispose()
422 {
423 import gfx.core.rc : releaseObj;
424
425 releaseObj(_buffer);
426 super.dispose();
427 }
428
429 final @property Buffer buffer() {
430 return _buffer;
431 }
432 }
433
434 final class ImageAlloc : MemAlloc
435 {
436 import gfx.graal.image : Image;
437
438 private Image _image;
439
440 package this (Image image, size_t offset, size_t size, MemBlock block, Object blockData)
441 {
442 import gfx.core.rc : retainObj;
443
444 super(image.boundMemory, offset, size, block, blockData);
445 _image = retainObj(image);
446 }
447
448 override void dispose()
449 {
450 import gfx.core.rc : releaseObj;
451
452 releaseObj(_image);
453 super.dispose();
454 }
455
456 final @property Image image() {
457 return _image;
458 }
459 }
460
461
462 /// Find a memory type index suitable for the given allowedIndexMask and info.
463 /// Params:
464 /// types = the memory types obtained from a device
465 /// allowedIndexMask = the mask obtained from MemoryRequirements.memTypeMask
466 /// options = an optional AllocOptions that will constraint the
467 /// choice
468 /// Returns: the found index of memory type, or uint.max if none could satisfy requirements
469 uint findMemoryTypeIndex(in MemoryType[] types,
470 in uint allowedIndexMask,
471 in AllocOptions options=AllocOptions.init)
472 {
473 const allowedMask = options.memTypeIndexMask != 0 ?
474 allowedIndexMask & options.memTypeIndexMask :
475 allowedIndexMask;
476
477 MemProps preferred = options.preferredProps;
478 MemProps required = options.requiredProps;
479
480 switch (options.usage) {
481 case MemoryUsage.gpuOnly:
482 preferred |= MemProps.deviceLocal;
483 break;
484 case MemoryUsage.cpuOnly:
485 required |= MemProps.hostVisible | MemProps.hostCoherent;
486 break;
487 case MemoryUsage.cpuToGpu:
488 required |= MemProps.hostVisible;
489 preferred |= MemProps.deviceLocal;
490 break;
491 case MemoryUsage.gpuToCpu:
492 required |= MemProps.hostVisible;
493 preferred |= MemProps.hostCoherent | MemProps.hostCached;
494 break;
495 case MemoryUsage.unknown:
496 default:
497 break;
498 }
499
500 uint maxValue = uint.max;
501 uint index = uint.max;
502
503 foreach (i; 0 .. cast(uint)types.length) {
504 const mask = 1 << i;
505 // is this type allowed?
506 if ((allowedMask & mask) != 0) {
507 const props = types[i].props;
508 // does it have the required properties?
509 if ((props & required) == required) {
510 // it is a valid candidate. calcaulate its value as number of
511 // preferred flags present
512 import core.bitop : popcnt;
513 const value = popcnt(cast(uint)(props & preferred));
514 if (maxValue == uint.max || value > maxValue) {
515 index = i;
516 maxValue = value;
517 }
518 }
519 }
520 }
521 return index;
522 }
523
524 /// Layout of a resource
525 /// This is important to determined whether a page alignment or simple alignemnt
526 /// is necessary between two consecutive resources
527 enum ResourceLayout {
528 /// layout is unknown
529 unknown,
530 /// layout of buffers and linear images
531 linear,
532 /// layout of optimal images
533 optimal,
534 }
535
536 /// Some stats of an allocator that can be collected with Allocator.collectStats
537 struct AllocStats
538 {
539 /// A chunk is a suballocation from a block
540 static struct Chunk
541 {
542 size_t start;
543 size_t end;
544 bool occupied;
545 ResourceLayout layout;
546 }
547
548 /// A block is a one to one mapping on a DeviceMemory
549 static struct Block
550 {
551 size_t size;
552 Chunk[] chunks;
553 }
554
555 size_t totalReserved;
556 size_t totalUsed;
557 size_t totalFrag;
558 size_t linearOptimalGranularity;
559 Block[] blocks;
560
561 string toString()
562 {
563 import std.format : format;
564 string res = "AllocStats (\n";
565
566 res ~= format(" total reserved: %s\n", totalReserved);
567 res ~= format(" total used : %s\n", totalUsed);
568 res ~= format(" total frag : %s\n", totalFrag);
569 res ~= format(" granularity : %s\n", linearOptimalGranularity);
570
571 foreach (b; blocks) {
572 res ~= " DeviceMemory (\n";
573 res ~= format(" size: %s\n", b.size);
574 foreach (c; b.chunks) {
575 res ~= " Resource (\n";
576
577 res ~= format(" start : %s\n", c.start);
578 res ~= format(" end : %s\n", c.end);
579 res ~= format(" occupied: %s\n", c.occupied);
580 res ~= format(" layout : %s\n", c.layout);
581
582 res ~= " )\n";
583 }
584
585 res ~= " )\n";
586 }
587
588 res ~= ")\n";
589 return res;
590 }
591 }
592
593
594 package:
595
596 /// A block of memory associated to one DeviceMemory
597 interface MemBlock : IAtomicRefCounted
598 {
599 /// increase map count and return cached pointer
600 /// if map count was zero, it maps the memory to the cached pointer before
601 void* map();
602 /// decrease map count and unmap memory if it reaches zero
603 void unmap();
604 /// called by MemAlloc when it is disposed to notify its memory block
605 void free(Object blockData);
606 }
607
608 /// The result of allocation request
609 struct AllocResult
610 {
611 DeviceMemory mem;
612 size_t offset;
613 MemBlock block;
614 Object blockData;
615 }
616
617 /// whether two adjacent block should check for granularity alignment
618 bool granularityMatters(in ResourceLayout l1, in ResourceLayout l2) pure
619 {
620 if (l1 == ResourceLayout.unknown || l2 == ResourceLayout.unknown) return true;
621 return l1 != l2;
622 }
623
624 unittest {
625 assert(!granularityMatters(ResourceLayout.linear, ResourceLayout.linear));
626 assert(!granularityMatters(ResourceLayout.optimal, ResourceLayout.optimal));
627 assert( granularityMatters(ResourceLayout.linear, ResourceLayout.optimal));
628 assert( granularityMatters(ResourceLayout.optimal, ResourceLayout.linear));
629 }