diff --git a/src/gpu/GrBlockAllocator.cpp b/src/gpu/GrBlockAllocator.cpp index 18c854da03..11dea680e8 100644 --- a/src/gpu/GrBlockAllocator.cpp +++ b/src/gpu/GrBlockAllocator.cpp @@ -46,7 +46,7 @@ GrBlockAllocator::Block::~Block() { size_t GrBlockAllocator::totalSize() const { // Use size_t since the sum across all blocks could exceed 'int', even though each block won't - size_t size = offsetof(GrBlockAllocator, fHead); + size_t size = offsetof(GrBlockAllocator, fHead) + this->scratchBlockSize(); for (const Block* b : this->blocks()) { size += b->fSize; } @@ -55,7 +55,10 @@ size_t GrBlockAllocator::totalSize() const { } size_t GrBlockAllocator::totalUsableSpace() const { - size_t size = 0; + size_t size = this->scratchBlockSize(); + if (size > 0) { + size -= kDataStart; // scratchBlockSize reports total block size, not usable size + } for (const Block* b : this->blocks()) { size += (b->fSize - kDataStart); } @@ -87,9 +90,14 @@ GrBlockAllocator::Block* GrBlockAllocator::findOwningBlock(const void* p) { } void GrBlockAllocator::releaseBlock(Block* block) { - if (block->fPrev) { - // Unlink block from the double-linked list of blocks - SkASSERT(block != &fHead); + if (block == &fHead) { + // Reset the cursor of the head block so that it can be reused if it becomes the new tail + block->fCursor = kDataStart; + block->fMetadata = 0; + // Unlike in reset(), we don't set the head's next block to null because there are + // potentially heap-allocated blocks that are still connected to it. + } else { + SkASSERT(block->fPrev); block->fPrev->fNext = block->fNext; if (block->fNext) { SkASSERT(fTail != block); @@ -99,14 +107,17 @@ void GrBlockAllocator::releaseBlock(Block* block) { fTail = block->fPrev; } - delete block; - } else { - // Reset the cursor of the head block so that it can be reused - SkASSERT(block == &fHead); - block->fCursor = kDataStart; - block->fMetadata = 0; - // Unlike in reset(), we don't set the head's next block to null because there are - // potentially heap-allocated blocks that are still connected to it. + // The released block becomes the new scratch block (if it's bigger), or delete it + if (this->scratchBlockSize() < block->fSize) { + SkASSERT(block != fHead.fPrev); // sanity check, shouldn't already be the scratch block + if (fHead.fPrev) { + delete fHead.fPrev; + } + block->markAsScratch(); + fHead.fPrev = block; + } else { + delete block; + } } // Decrement growth policy (opposite of addBlock()'s increment operations) @@ -139,14 +150,15 @@ void GrBlockAllocator::reset() { b->fNext = nullptr; b->fCursor = kDataStart; b->fMetadata = 0; - - // For reset(), but NOT releaseBlock(), the head allocatorMetadata resets too + // For reset(), but NOT releaseBlock(), the head allocatorMetadata and scratch block + // are reset/destroyed. b->fAllocatorMetadata = 0; + this->resetScratchSpace(); } else { delete b; } } - SkASSERT(fTail == &fHead && fHead.fNext == nullptr && + SkASSERT(fTail == &fHead && fHead.fNext == nullptr && fHead.fPrev == nullptr && fHead.metadata() == 0 && fHead.fCursor == kDataStart); GrowthPolicy gp = static_cast(fGrowthPolicy); @@ -154,6 +166,13 @@ void GrBlockAllocator::reset() { fN1 = 1; } +void GrBlockAllocator::resetScratchSpace() { + if (fHead.fPrev) { + delete fHead.fPrev; + fHead.fPrev = nullptr; + } +} + void GrBlockAllocator::addBlock(int minimumSize, int maxSize) { SkASSERT(minimumSize > (int) sizeof(Block) && minimumSize <= maxSize); @@ -161,40 +180,59 @@ void GrBlockAllocator::addBlock(int minimumSize, int maxSize) { static constexpr int kMaxN = (1 << 23) - 1; static_assert(2 * kMaxN <= std::numeric_limits::max()); // Growth policy won't overflow - // Calculate the 'next' size per growth policy sequence - GrowthPolicy gp = static_cast(fGrowthPolicy); - int nextN1 = fN0 + fN1; - int nextN0; - if (gp == GrowthPolicy::kFixed || gp == GrowthPolicy::kLinear) { - nextN0 = fN0; - } else if (gp == GrowthPolicy::kFibonacci) { - nextN0 = fN1; - } else { - SkASSERT(gp == GrowthPolicy::kExponential); - nextN0 = nextN1; - } - fN0 = std::min(kMaxN, nextN0); - fN1 = std::min(kMaxN, nextN1); - - // However, must guard against overflow here, since all the size-based asserts prevented - // alignment/addition overflows, while multiplication requires 2x bits instead of x+1. - int sizeIncrement = fBlockIncrement * kAddressAlign; - int allocSize; - if (maxSize / sizeIncrement < nextN1) { - // The growth policy would overflow, so use the max. We've already confirmed that maxSize - // will be sufficient for the requested minimumSize - allocSize = maxSize; - } else { - allocSize = std::max(minimumSize, sizeIncrement * nextN1); - // Then round to a nice boundary since the block isn't maxing out: + auto alignAllocSize = [](int size) { + // Round to a nice boundary since the block isn't maxing out: // if allocSize > 32K, aligns on 4K boundary otherwise aligns on max_align_t, to play // nicely with jeMalloc (from SkArenaAlloc). - int mask = allocSize > (1 << 15) ? ((1 << 12) - 1) : (kAddressAlign - 1); - allocSize = std::min((allocSize + mask) & ~mask, maxSize); + int mask = size > (1 << 15) ? ((1 << 12) - 1) : (kAddressAlign - 1); + return (size + mask) & ~mask; + }; + + int allocSize; + void* mem = nullptr; + if (this->scratchBlockSize() >= minimumSize) { + // Activate the scratch block instead of making a new block + SkASSERT(fHead.fPrev->isScratch()); + allocSize = fHead.fPrev->fSize; + mem = fHead.fPrev; + fHead.fPrev = nullptr; + } else if (minimumSize < maxSize) { + // Calculate the 'next' size per growth policy sequence + GrowthPolicy gp = static_cast(fGrowthPolicy); + int nextN1 = fN0 + fN1; + int nextN0; + if (gp == GrowthPolicy::kFixed || gp == GrowthPolicy::kLinear) { + nextN0 = fN0; + } else if (gp == GrowthPolicy::kFibonacci) { + nextN0 = fN1; + } else { + SkASSERT(gp == GrowthPolicy::kExponential); + nextN0 = nextN1; + } + fN0 = std::min(kMaxN, nextN0); + fN1 = std::min(kMaxN, nextN1); + + // However, must guard against overflow here, since all the size-based asserts prevented + // alignment/addition overflows, while multiplication requires 2x bits instead of x+1. + int sizeIncrement = fBlockIncrement * kAddressAlign; + if (maxSize / sizeIncrement < nextN1) { + // The growth policy would overflow, so use the max. We've already confirmed that + // maxSize will be sufficient for the requested minimumSize + allocSize = maxSize; + } else { + allocSize = std::min(alignAllocSize(std::max(minimumSize, sizeIncrement * nextN1)), + maxSize); + } + } else { + SkASSERT(minimumSize == maxSize); + // Still align on a nice boundary, no max clamping since that would just undo the alignment + allocSize = alignAllocSize(minimumSize); } // Create new block and append to the linked list of blocks in this allocator - void* mem = operator new(allocSize); + if (!mem) { + mem = operator new(allocSize); + } fTail->fNext = new (mem) Block(fTail, allocSize); fTail = fTail->fNext; } @@ -207,7 +245,13 @@ void GrBlockAllocator::validate() const { blocks.push_back(block); SkASSERT(kAssignedMarker == block->fSentinel); - SkASSERT(prev == block->fPrev); + if (block == &fHead) { + // The head blocks' fPrev may be non-null if it holds a scratch block, but that's not + // considered part of the linked list + SkASSERT(!prev && (!fHead.fPrev || fHead.fPrev->isScratch())); + } else { + SkASSERT(prev == block->fPrev); + } if (prev) { SkASSERT(prev->fNext == block); } diff --git a/src/gpu/GrBlockAllocator.h b/src/gpu/GrBlockAllocator.h index 9bc21065d8..befd48483b 100644 --- a/src/gpu/GrBlockAllocator.h +++ b/src/gpu/GrBlockAllocator.h @@ -132,6 +132,9 @@ public: template int alignedOffset(int offset) const; + bool isScratch() const { return fCursor < 0; } + void markAsScratch() { fCursor = -1; } + SkDEBUGCODE(int fSentinel;) // known value to check for bad back pointers to blocks Block* fNext; // doubly-linked list of blocks @@ -259,6 +262,33 @@ public: template ByteRange allocate(size_t size); + enum ReserveFlags : unsigned { + // If provided to reserve(), the input 'size' will be rounded up to the next size determined + // by the growth policy of the GrBlockAllocator. If not, 'size' will be aligned to max_align + kIgnoreGrowthPolicy_Flag = 0b01, + // If provided to reserve(), the number of available bytes of the current block will not + // be used to satisfy the reservation (assuming the contiguous range was long enough to + // begin with). + kIgnoreExistingBytes_Flag = 0b10, + + kNo_ReserveFlags = 0b00 + }; + + /** + * Ensure the block allocator has 'size' contiguous available bytes. After calling this + * function, currentBlock()->avail() may still report less than 'size' if the + * reserved space was added as a scratch block. This is done so that anything remaining in + * the current block can still be used if a smaller-than-size allocation is requested. If 'size' + * is requested by a subsequent allocation, the scratch block will automatically be activated + * and the request will not itself trigger any malloc. + * + * The optional 'flags' controls how the input size is allocated; by default it will attempt + * to use available contiguous bytes in the current block and will respect the growth policy + * of the allocator. + */ + template + void reserve(size_t size, ReserveFlags flags = kNo_ReserveFlags); + /** * Return a pointer to the start of the current block. This will never be null. */ @@ -305,6 +335,10 @@ public: * * If 'block' represents the inline-allocated head block, its cursor and metadata are instead * reset to their defaults. + * + * If the block is not the head block, it may be kept as a scratch block to be reused for + * subsequent allocation requests, instead of making an entirely new block. A scratch block is + * not visible when iterating over blocks but is reported in the total size of the allocator. */ void releaseBlock(Block* block); @@ -314,6 +348,11 @@ public: */ void reset(); + /** + * Remove any reserved scratch space, either from calling reserve() or releaseBlock(). + */ + void resetScratchSpace(); + template class BlockIter; /** @@ -338,6 +377,10 @@ public: void validate() const; #endif +#if GR_TEST_UTILS + int testingOnly_scratchBlockSize() const { return this->scratchBlockSize(); } +#endif + private: static constexpr int kDataStart = sizeof(Block); #ifdef SK_FORCE_8_BYTE_ALIGNMENT @@ -369,6 +412,8 @@ private: // that will preserve the static guarantees GrBlockAllocator makes. void addBlock(int minSize, int maxSize); + int scratchBlockSize() const { return fHead.fPrev ? fHead.fPrev->fSize : 0; } + Block* fTail; // All non-head blocks are heap allocated; tail will never be null. // All remaining state is packed into 64 bits to keep GrBlockAllocator at 16 bytes + head block @@ -390,6 +435,9 @@ private: // Inline head block, must be at the end so that it can utilize any additional reserved space // from the initial allocation. + // The head block's prev pointer may be non-null, which signifies a scratch block that may be + // reused instead of allocating an entirely new block (this helps when allocate+release calls + // bounce back and forth across the capacity of a block). alignas(kAddressAlign) Block fHead; static_assert(kGrowthPolicyCount <= 4); @@ -435,6 +483,8 @@ private: /////////////////////////////////////////////////////////////////////////////////////////////////// // Template and inline implementations +GR_MAKE_BITFIELD_OPS(GrBlockAllocator::ReserveFlags) + template constexpr size_t GrBlockAllocator::BlockOverhead() { static_assert(GrAlignTo(kDataStart + Padding, Align) >= sizeof(Block)); @@ -457,6 +507,29 @@ constexpr size_t GrBlockAllocator::MaxBlockSize() { return BlockOverhead() + kMaxAllocationSize; } +template +void GrBlockAllocator::reserve(size_t size, ReserveFlags flags) { + if (size > kMaxAllocationSize) { + SK_ABORT("Allocation too large (%zu bytes requested)", size); + } + int iSize = (int) size; + if ((flags & kIgnoreExistingBytes_Flag) || + this->currentBlock()->avail() < iSize) { + + int blockSize = BlockOverhead() + iSize; + int maxSize = (flags & kIgnoreGrowthPolicy_Flag) ? blockSize + : MaxBlockSize(); + SkASSERT((size_t) maxSize <= (MaxBlockSize())); + + SkDEBUGCODE(auto oldTail = fTail;) + this->addBlock(blockSize, maxSize); + SkASSERT(fTail != oldTail); + // Releasing the just added block will move it into scratch space, allowing the original + // tail's bytes to be used first before the scratch block is activated. + this->releaseBlock(fTail); + } +} + template GrBlockAllocator::ByteRange GrBlockAllocator::allocate(size_t size) { // Amount of extra space for a new block to make sure the allocation can succeed. @@ -599,6 +672,12 @@ public: void advance(BlockT* block) { fBlock = block; fNext = block ? (Forward ? block->fNext : block->fPrev) : nullptr; + if (!Forward && fNext && fNext->isScratch()) { + // For reverse-iteration only, we need to stop at the head, not the scratch block + // possibly stashed in head->prev. + fNext = nullptr; + } + SkASSERT(!fNext || !fNext->isScratch()); } BlockT* fBlock; diff --git a/src/gpu/GrMemoryPool.h b/src/gpu/GrMemoryPool.h index 87771f8a2a..8650a74109 100644 --- a/src/gpu/GrMemoryPool.h +++ b/src/gpu/GrMemoryPool.h @@ -67,7 +67,8 @@ public: bool isEmpty() const { // If size is the same as preallocSize, there aren't any heap blocks, so currentBlock() // is the inline head block. - return 0 == this->size() && 0 == fAllocator.currentBlock()->metadata(); + return fAllocator.currentBlock() == fAllocator.headBlock() && + fAllocator.currentBlock()->metadata() == 0; } /** diff --git a/src/gpu/GrTAllocator.h b/src/gpu/GrTAllocator.h index 961afa6c49..94c3828298 100644 --- a/src/gpu/GrTAllocator.h +++ b/src/gpu/GrTAllocator.h @@ -65,6 +65,19 @@ public: return *new (this->pushItem()) T(std::forward(args)...); } + /** + * Allocate, if needed, space to hold N more Ts before another malloc will occur. + */ + void reserve(int n) { + int avail = fAllocator->currentBlock()->template avail() / sizeof(T); + if (n > avail) { + int reserved = n - avail; + // Don't consider existing bytes since we've already determined how to split the N items + fAllocator->template reserve( + reserved * sizeof(T), GrBlockAllocator::kIgnoreExistingBytes_Flag); + } + } + /** * Remove the last item, only call if count() != 0 */ @@ -156,7 +169,7 @@ public: * Use for-range loops by calling items() or ritems() instead to access all added items in order */ T& item(int i) { - SkASSERT(i >= 0 && i < fAllocator->metadata()); + SkASSERT(i >= 0 && i < this->count()); // Iterate over blocks until we find the one that contains i. for (auto* b : fAllocator->blocks()) { @@ -236,6 +249,11 @@ public: // Iterate from newest to oldest using a for-range loop. RIter ritems() { return RIter(fAllocator.allocator()); } CRIter ritems() const { return CRIter(fAllocator.allocator()); } + +#if GR_TEST_UTILS + // For introspection + const GrBlockAllocator* allocator() const { return fAllocator.allocator(); } +#endif }; /** diff --git a/tests/GrBlockAllocatorTest.cpp b/tests/GrBlockAllocatorTest.cpp index b58b701e38..c500ec908c 100644 --- a/tests/GrBlockAllocatorTest.cpp +++ b/tests/GrBlockAllocatorTest.cpp @@ -38,13 +38,22 @@ static Block* get_block(GrSBlockAllocator& pool, int blockIndex) { return found; } +// GrBlockAllocator holds on to the largest last-released block to reuse for new allocations, +// and this is still counted in its totalSize(). However, it's easier to reason about size - scratch +// in many of these tests. +template +static size_t total_size(GrSBlockAllocator& pool) { + return pool->totalSize() - pool->testingOnly_scratchBlockSize(); +} + template static size_t add_block(GrSBlockAllocator& pool) { - size_t currentSize = pool->totalSize(); - while(pool->totalSize() == currentSize) { + size_t currentSize = total_size(pool); + GrBlockAllocator::Block* current = pool->currentBlock(); + while(pool->currentBlock() == current) { pool->template allocate<4>(pool->preallocSize() / 2); } - return pool->totalSize() - currentSize; + return total_size(pool) - currentSize; } template @@ -124,7 +133,7 @@ DEF_TEST(GrBlockAllocatorAlloc, r) { validate_ptr(32, 96, p32, &p16); // All of these allocations should be in the head block - REPORTER_ASSERT(r, pool->totalSize() == pool->preallocSize()); + REPORTER_ASSERT(r, total_size(pool) == pool->preallocSize()); SkDEBUGCODE(pool->validate();) // Requesting an allocation of avail() should not make a new block @@ -137,21 +146,21 @@ DEF_TEST(GrBlockAllocatorAlloc, r) { REPORTER_ASSERT(r, pool->currentBlock()->avail<4>() < 4); auto pNextBlock = pool->allocate<4>(4); validate_ptr(4, 4, pNextBlock, nullptr); - REPORTER_ASSERT(r, pool->totalSize() > pool->preallocSize()); + REPORTER_ASSERT(r, total_size(pool) > pool->preallocSize()); // Allocating more than avail() makes an another block - size_t currentSize = pool->totalSize(); + size_t currentSize = total_size(pool); size_t bigRequest = pool->currentBlock()->avail<4>() * 2; auto pTooBig = pool->allocate<4>(bigRequest); validate_ptr(4, bigRequest, pTooBig, nullptr); - REPORTER_ASSERT(r, pool->totalSize() > currentSize); + REPORTER_ASSERT(r, total_size(pool) > currentSize); // Allocating more than the default growth policy (1024 in this case), will fulfill the request - REPORTER_ASSERT(r, pool->totalSize() - currentSize < 4096); - currentSize = pool->totalSize(); + REPORTER_ASSERT(r, total_size(pool) - currentSize < 4096); + currentSize = total_size(pool); auto pReallyTooBig = pool->allocate<4>(4096); validate_ptr(4, 4096, pReallyTooBig, nullptr); - REPORTER_ASSERT(r, pool->totalSize() >= currentSize + 4096); + REPORTER_ASSERT(r, total_size(pool) >= currentSize + 4096); SkDEBUGCODE(pool->validate();) } @@ -276,7 +285,7 @@ DEF_TEST(GrBlockAllocatorGrowthPolicy, r) { GrSBlockAllocator pool{(GrowthPolicy) gp}; SkDEBUGCODE(pool->validate();) - REPORTER_ASSERT(r, kExpectedSizes[gp][0] == pool->totalSize()); + REPORTER_ASSERT(r, kExpectedSizes[gp][0] == total_size(pool)); for (int i = 1; i < kBlockCount; ++i) { REPORTER_ASSERT(r, kExpectedSizes[gp][i] == add_block(pool)); } @@ -327,7 +336,7 @@ DEF_TEST(GrBlockAllocatorReleaseBlock, r) { void* firstAlloc = alloc_byte(pool); - size_t b1Size = pool->totalSize(); + size_t b1Size = total_size(pool); size_t b2Size = add_block(pool); size_t b3Size = add_block(pool); size_t b4Size = add_block(pool); @@ -340,30 +349,31 @@ DEF_TEST(GrBlockAllocatorReleaseBlock, r) { // Remove the 3 added blocks, but always remove the i = 1 to test intermediate removal (and // on the last iteration, will test tail removal). - REPORTER_ASSERT(r, pool->totalSize() == b1Size + b2Size + b3Size + b4Size); + REPORTER_ASSERT(r, total_size(pool) == b1Size + b2Size + b3Size + b4Size); pool->releaseBlock(get_block(pool, 1)); REPORTER_ASSERT(r, block_count(pool) == 3); REPORTER_ASSERT(r, get_block(pool, 1)->metadata() == 3); - REPORTER_ASSERT(r, pool->totalSize() == b1Size + b3Size + b4Size); + REPORTER_ASSERT(r, total_size(pool) == b1Size + b3Size + b4Size); pool->releaseBlock(get_block(pool, 1)); REPORTER_ASSERT(r, block_count(pool) == 2); REPORTER_ASSERT(r, get_block(pool, 1)->metadata() == 4); - REPORTER_ASSERT(r, pool->totalSize() == b1Size + b4Size); + REPORTER_ASSERT(r, total_size(pool) == b1Size + b4Size); pool->releaseBlock(get_block(pool, 1)); REPORTER_ASSERT(r, block_count(pool) == 1); - REPORTER_ASSERT(r, pool->totalSize() == b1Size); + REPORTER_ASSERT(r, total_size(pool) == b1Size); // Since we're back to just the head block, if we add a new block, the growth policy should // match the original sequence instead of continuing with "b5Size'" + pool->resetScratchSpace(); size_t size = add_block(pool); REPORTER_ASSERT(r, size == b2Size); pool->releaseBlock(get_block(pool, 1)); // Explicitly release the head block and confirm it's reset pool->releaseBlock(get_block(pool, 0)); - REPORTER_ASSERT(r, pool->totalSize() == pool->preallocSize()); + REPORTER_ASSERT(r, total_size(pool) == pool->preallocSize()); REPORTER_ASSERT(r, block_count(pool) == 1); REPORTER_ASSERT(r, firstAlloc == alloc_byte(pool)); REPORTER_ASSERT(r, get_block(pool, 0)->metadata() == 0); // metadata reset too @@ -415,6 +425,69 @@ DEF_TEST(GrBlockAllocatorIterateAndRelease, r) { REPORTER_ASSERT(r, block_count(pool) == 1); } +DEF_TEST(GrBlockAllocatorScratchBlockReserve, r) { + GrSBlockAllocator<256> pool; + + size_t added = add_block(pool); + REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() == 0); + size_t total = pool->totalSize(); + pool->releaseBlock(pool->currentBlock()); + + // Total size shouldn't have changed, the released block should become scratch + REPORTER_ASSERT(r, pool->totalSize() == total); + REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() == added); + + // But a reset definitely deletes any scratch block + pool->reset(); + REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() == 0); + + // Reserving more than what's available adds a scratch block, and current block remains avail. + size_t avail = pool->currentBlock()->avail(); + size_t reserve = avail + 1; + pool->reserve(reserve); + REPORTER_ASSERT(r, (size_t) pool->currentBlock()->avail() == avail); + // And rounds up to the fixed size of this pool's growth policy + REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() >= reserve && + pool->testingOnly_scratchBlockSize() % 256 == 0); + + // Allocating more than avail activates the scratch block (so totalSize doesn't change) + size_t preAllocTotalSize = pool->totalSize(); + pool->allocate<1>(avail + 1); + REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() == 0); + REPORTER_ASSERT(r, pool->totalSize() == preAllocTotalSize); + + // When reserving less than what's still available in the current block, no scratch block is + // added. + pool->reserve(pool->currentBlock()->avail()); + REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() == 0); + + // Unless checking available bytes is disabled + pool->reserve(pool->currentBlock()->avail(), GrBlockAllocator::kIgnoreExistingBytes_Flag); + REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() > 0); + + // If kIgnoreGrowthPolicy is specified, the new scratch block should not have been updated to + // follow the size (which in this case is a fixed 256 bytes per block). + pool->resetScratchSpace(); + pool->reserve(32, GrBlockAllocator::kIgnoreGrowthPolicy_Flag); + REPORTER_ASSERT(r, pool->testingOnly_scratchBlockSize() > 0 && + pool->testingOnly_scratchBlockSize() < 256); + + // When requesting an allocation larger than the current block and the scratch block, a new + // block is added, and the scratch block remains scratch. + GrBlockAllocator::Block* oldTail = pool->currentBlock(); + avail = oldTail->avail(); + size_t scratchAvail = 2 * avail; + pool->reserve(scratchAvail); + REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() >= scratchAvail); // sanity + + // This allocation request is higher than oldTail's available, and the scratch size so we + // should add a new block and scratch size should stay the same. + scratchAvail = pool->testingOnly_scratchBlockSize(); + pool->allocate<1>(scratchAvail + 1); + REPORTER_ASSERT(r, pool->currentBlock() != oldTail); + REPORTER_ASSERT(r, (size_t) pool->testingOnly_scratchBlockSize() == scratchAvail); +} + // These tests ensure that the allocation padding mechanism works as intended struct TestMeta { int fX1; diff --git a/tests/GrTAllocatorTest.cpp b/tests/GrTAllocatorTest.cpp index ffe9c31b2f..c77725dd10 100644 --- a/tests/GrTAllocatorTest.cpp +++ b/tests/GrTAllocatorTest.cpp @@ -22,9 +22,13 @@ struct C { int fID; + // Under the hood, GrTAllocator and GrBlockAllocator round up to max_align_t. If 'C' was just + // 4 bytes, that often means the internal blocks can squeeze a few extra instances in. This + // is fine, but makes predicting a little trickier, so make sure C is a bit bigger. + int fPadding[4]; + static int gInstCnt; }; - int C::gInstCnt = 0; } @@ -156,6 +160,63 @@ static void run_allocator_test(GrTAllocator* allocator, skiatest::Reporter check_allocator(allocator, 100, 10, reporter); } + +template +static void run_reserve_test(skiatest::Reporter* reporter) { + constexpr int kItemsPerBlock = N + 4; // Make this a number > 1, even if N starting items == 1 + + GrTAllocator list(kItemsPerBlock); + size_t initialSize = list.allocator()->totalSize(); + // Should be able to add N instances of T w/o changing size from initialSize + for (int i = 0; i < N; ++i) { + list.push_back(C(i)); + } + REPORTER_ASSERT(reporter, initialSize == list.allocator()->totalSize()); + + // Reserve room for 2*kItemsPerBlock items + list.reserve(2 * kItemsPerBlock); + REPORTER_ASSERT(reporter, list.count() == N); // count shouldn't change though + + size_t reservedSize = list.allocator()->totalSize(); + REPORTER_ASSERT(reporter, reservedSize >= initialSize + 2 * kItemsPerBlock * sizeof(C)); + for (int i = 0; i < 2 * kItemsPerBlock; ++i) { + list.push_back(C(i)); + } + REPORTER_ASSERT(reporter, reservedSize == list.allocator()->totalSize()); + + // Make the next block partially fully (N > 0 but < kItemsPerBlock) + for (int i = 0; i < N; ++i) { + list.push_back(C(i)); + } + + // Reserve room again for 2*kItemsPerBlock, but reserve should automatically take account of the + // (kItemsPerBlock-N) that are still available in the active block + list.reserve(2 * kItemsPerBlock); + int extraReservedCount = kItemsPerBlock + N; + // Because GrTAllocator normally allocates blocks in fixed sizes, and extraReservedCount > + // items-per-block, it will always use that size and not that of the growth policy. + REPORTER_ASSERT(reporter, (size_t) list.allocator()->testingOnly_scratchBlockSize() >= + extraReservedCount * sizeof(C)); + + reservedSize = list.allocator()->totalSize(); + for (int i = 0; i < 2 * kItemsPerBlock; ++i) { + list.push_back(C(i)); + } + REPORTER_ASSERT(reporter, reservedSize == list.allocator()->totalSize()); + + // If we reserve a count < items-per-block, it will use the fixed size from the growth policy. + list.reserve(2); + REPORTER_ASSERT(reporter, (size_t) list.allocator()->testingOnly_scratchBlockSize() >= + kItemsPerBlock * sizeof(C)); + + // Ensure the reservations didn't initialize any more D's than anticipated + int expectedInstanceCount = 2 * (N + 2 * kItemsPerBlock); + REPORTER_ASSERT(reporter, expectedInstanceCount == C::gInstCnt); + + list.reset(); + REPORTER_ASSERT(reporter, 0 == C::gInstCnt); +} + DEF_TEST(GrTAllocator, reporter) { // Test combinations of allocators with and without stack storage and with different block sizes GrTAllocator a1(1); @@ -175,4 +236,10 @@ DEF_TEST(GrTAllocator, reporter) { GrTAllocator sa4; run_allocator_test(&sa4, reporter); + + run_reserve_test<1>(reporter); + run_reserve_test<2>(reporter); + run_reserve_test<3>(reporter); + run_reserve_test<4>(reporter); + run_reserve_test<5>(reporter); }