Setting A Dynamic Buffer’s Capacity

Is it possible to manually set the capacity of a Dynamic Buffer, when it is created?

I have a case in which multiple Archetypes will have a DynamicBuffer of the same IBufferElementData. But, each Archetype may require a different capacity.

I don’t believe using InternalBufferCapacityAttribute will work in this case, since that will give all DynamicBuffers of this type the same capacity, throughout the executable.

The capacities for each archetype are also only knowable during the Entity conversation process. So that’s before runtime (using subscenes), but not early enough to include it as a constant value in an InternalBufferCapacityAttribute.

But, if I can assign the capacities manually when creating the DynamicBuffers, I can ensure that each Archetype has only the ram it needs. No wasted capacity is oversized buffers, and never any overflow onto the heap - guaranteed.

Thanks for any advice!

if it were possible, you would end up with a different archetype for each different capacity you have (because a different capacity implies a different memory layout). this would potentially end up with huge / uncontrollable fragmentation (i.e. you will still waste 16k of memory for the chunk)

if you want to compact the memory to the max, you could define different IBufferElementData with the same data but different capacity, then in your conversion script add the appropriate buffer based on the number of elements. you could add implicit conversions to ease the code (or use Reinterpret<T>)

(you may also want to have different algorithms for small and large buffers, given they are different data)

1 Like

Thank you for the reply. Different archetypes for each capacity is my goal. I write my code such that my entities never (or almost never) change archetypes once they are created. Would you help me understand how that would still create waste?

I don’t want to create a different IBufferElementData type for each archetype, for several reasons. We’re talking about an unbounded ‘n’ number of archetypes, each with different buffer capacities depending on how many child entities the root entity has. That would mean defining a new IBufferElementData each time a new archetype was introduced, and maintaining it every time the number of children might change during production. It just doesn’t sound realistically maintainable. The other reason is that it would be a nightmare to write an maintain Systems and Jobs to handle ‘n’ IBufferElementData types, which are all actually variations of one set of data. It’s just not feasible.

this.
essentially if you can’t control what your ‘n’ is, then you will have potentially many cases in which you only have one or few entities for each capacity. that will allocate an entire chunk (16kB) that will be almost empty.

you don’t manually create a component for each n, but instead decide one (or more) thresholds, define your types for those (+ 1 “unbounded”) and during conversion you assign a buffer depending on your data.
so you have something like:

[DefaultCapacity(4)] struct SmallBuffer:IBufferElementData{int value;}
[DefaultCapacity(32)] struct MediumBuffer:IBufferElementData{int value;}
[DefaultCapacity(0)] struct LargeBuffer:IBufferElementData{int value;}

(note the 0 on LargeBuffer. over a certain amount of data it may be actually convenient to allocate on the heap.)

then you convert your entities with

if (data.length <= 4) AddBuffer<Small>();
else if (data.length <= 32) AddBuffer<Medium>();
else AddBuffer<Large>();

you will only have a couple of types and the flexibility to optimize the memory (vs code verbosity)

Thank you for the reply! I see and appreciate your approach. However, I’m not sure it’s a good solution in this case. Here’s why:

This is my bad. I used the word “unbounded”, which was dumb. It’s not true. The number of archetypes in my project is bounded. ‘n’ is always a known quantity. I should have said something like, “As a generalized approach to development, this way would need to stay viable across multiple projects, each with a different and potentially large number of archetypes.”

As for maximizing chunk usage - that’s a topic that’s unrelated to the use of Dynamic Buffers…Let me give an example:

Let’s say, in a worst case scenario, I had a different archetype for each type of enemy in my project: “Enemy0” - “Enemy49”. Depending on how many instance of each enemy type I have loaded at once, I might end up with one or two entities in a mostly-empty chunk. If the current game environment had just one enemy of each type, I could end up with 50, mostly empty chunks. But that’s governed by the game design. If each of those enemies actually has a different archetype, then each one would have to live in a different chunk. And here, we’re talking about archetypes without any Dynamic Buffers.

Now throw a differently sized Dynamic Buffer on each archetype (again, the worst case scenario). Nothing changes. Each of the 50 entities would still have to occupy it’s own, mostly-empty chunk. That situation is governed by the game design’s enemy needs, not whether or not they have differently sized Dynamic Buffers.


Continuing with this example: If I committed to the approach you described, I would need to make 50 different IBufferElementData types, each with a different Default Capacity. Each of these IBufferElementDatas would be identical, except for their Type name and Default Capacity attribute.

  1. If I wanted to change the data layout of all of those variant Types, I would now need to do it in all 50. If I for whatever reason needed to add a 51st capacity, the workload grows.

  2. All of these IBufferElementData variants store the same contextual data. So how would I go about writing a single JobComponentSystem to process all of these different variants at the same time? I don’t think I could use any of the build in convenience Job types, since I believe they currently max out at 6 Component types. I believe I would have to use IJobChunk, and then filter through all of the different IBufferElementData types inside the Job, until I found the right one for each entity. Or maybe I could write 50 different Job types, and filter for the right IBufferElementData on the main thread. Or perhaps I could do some trickery with a common interface. But now, suddenly, I need to add 3 new enemy types. I’ll potentially have to revisit every one of my systems that does this. And that’s just the game’s enemies!


Vs. If you could set the initial capacity of a Dynamic Buffer differently for each archetype:

  1. You could just use a single IBufferElementData type.
  2. You would only need to filter for that one type in a JobComponentSystem.
  3. The amount of ram taken up by DynamicBuffer wouldn’t be oversized for any of your archetypes. It would be as optimized as possible.

Unity seems to handle this fine. You can force two DBs of the same type to have different capacities (and therefore different archetypes) by resizing them differently. So I’m wondering if the only limit here is the absence of a method to set the capacity.

You can set the initial size of a DynamicBuffer (and choose either internal or external) and you can also change its size later on at runtime.

        /// <summary>
        /// Increases the buffer capacity and length.
        /// </summary>
        /// <param name="length">The new length of the buffer.</param>
        public void ResizeUninitialized(int length)

        /// <summary>
        /// Increases the buffer capacity without increasing its length.
        /// </summary>
        /// <param name="length">The new buffer capacity.</param>
        public void Reserve(int length)
  • The initial capacity if not explicitly set with [InternalBufferCapacity] defaults to 128 / elementSize.

  • The size of the internal capacity (storage inside the chunk) can’t change.

  • Archetype changes won’t affect whether it’s using internal storage or separate heap allocation.

  • Performance difference should be negligible (data locality/pointer deref). Probably most people using DBs right now are not using internal storage because the default capacity is low.

  • Once the capacity goes beyond the internal size (because you either went over internal capacity or explicitly changed it with ResizeUninitialized()/Reserve()) then it will be moved to a new allocation external to the chunk.

  • The Internal space for buffers exists there whether you’re using it or not. That enables TrimExcess() to let you switch back to using into internal storage later when the element count is back under capacity.

public class SomeConvertToEntityScript : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var myBuffer = dstManager.AddBuffer<SomeBufferElement>(entity);
 
        // If you're okay with it being allocated separately on the heap,
        // then you can resize it larger than internal capacity.
        myBuffer.ResizeUninitialized(100);
    }
}

// Define how much internal storage you want.
[InternalBufferCapacity(50)]
public struct SomeBufferElement : IBufferElementData
{
    public int Value;
}

you don’t declare 50 different buffers.
you find out what is the length that 99% of your entities have, then optimize for that.

a dynamic buffer always occupy chunk memory defined by its [DefaultCapacity] attribute. if it has more than that, it will use a separate allocation for all the elements, while still “wasting” chunk memory (because the layout of the archetype is fixed).

if you only have archetypes with 1 entity anyways, you can declare the buffer capacity as 16k (minus size of the rest) and as you said nothing changes.

otherwise, if most of your entities have length e.g. between 40 and 50, you declare 50 capacity and only waste less than 10, with 5 on average.

if you have a high capacity and want to further optimize the few (or not-so-few) with length above that, you declare an additional buffer with 0 capacity for those.
if you have a bimodal distribution (e.g. half are length <5, the other half is length 90~100) you declare 2 buffers with capacity 5 and 100.

regarding the layout:
you can declare a single “BufferData” struct, and your buffers can have a single BufferData field, then either Reinterpret<>() or have utility functions operating on BufferData.

the queries are an OR for the buffers you declare, therefore you need to use IJobChunk or multiple jobs (that can delegate to a single utility function

I really do appreciate you writing. But I think something is getting lost in communication.

This is a somewhat optimized solution:

This is a 100% optimized solution:

  • which would also require less code maintenance.

Why promote the less optimized solution?

because the other one is not possible with the current release, unless you edit the Entities code to support that.

currently the buffer capacity is determined by the type, like any attribute that the components have. an archetype is just a set of types.

also if the same buffer could have multiple capacities based on an archetype, what would happen if you add/remove a tag (thus change the archetype) and the capacity is different?

…People make cases for things that aren’t yet possible all the time in this forum.

To answer this question: that would create a new archetype. Same as if the DB wasn’t there. Different archetypes can already have different capacity DBs of the same IElementBufferData type. The code already supports this.

I believe all that’s missing is a method to set a DB’s capacity. It’s likely just never come up.

yes but the data cannot be moved to the new archetype with memcpy only, because there would be some logic happening per-entity (if the new capacity is less than length, then you need to malloc for the buffer and copy data there. if the old buffer was on the heap and the new capacity is large enough, then you need to delete the old buffer).

that also cannot be batched because the stride is different.

you also need to define the capacity for each possible archetype in your project, that grows exponentially with the number of components

Apologies - There must be a communication breakdown, on your side or mine.

Because:

This statement doesn’t make sense to me. One buffer couldn’t have multiple capacities. You would need a different DynamicBuffer for each capacity.

^ I’m also confused by this.

If:

  1. Entity ‘foo’ belongs to archetype ‘A’
  2. Archetype ‘A’ includes a DynamicBuffer with a capacity of 10
  3. You add a new Component to Entity ‘foo’…

Then: Entity ‘foo’ would now belong to a different archetype, ‘B’. Archetype ‘B’ would also contain a DynamicBuffer with a capacity of 10. A new chunk for Archetype ‘B’ would be created, to store this data.

How could the new DynamicBuffer in Archetype B have a “new capacity is less than length”?

And:

^ This can be avoided by never over-allocating your DynamicBuffers.

This is true across the project, but not inside of a chunk. All members of the same archetype would have the same DB capacity.

^ This is my goal. This isn’t a drawback - it’s what I intentionally want to do. The work would only grow exponentionally if I have to use a DefaultCapacity attribute, and define a new IBufferElementData for each archetype.

What I’m proposing would let me do it through code, during the EntityConversion process.

exponentially = if a project has e.g. 30 component types, then the possible archetypes that can exist is 2^30 = 1.073.741.824
also when you add a component, the archetypes double (existing archetypes + each of them with the new component added)
you would need to define the capacity for all of those in an exhaustive and unambiguous manner

then goto my first post

but what if you defined elsewhere that your archetype B must have 5 or 20 capacity?

this requires the capacity of a buffer to be consistently defined, not to depend on it’s archetype (e.g. my buffer is in archetype A with 10 capacity and 10 length, then I move the entity to an archetype with capacity 5)

I am not talking inside a chunk.

  • when you add/remove a tag to an entity query, DOTS is already optimized to not move anything but re-label the chunks, given they have the same layout – and this can be known only based on the fact that what you are adding is 0 size
  • when you batch add/remove a component with data, the optimal path could be to find where each component array is stored (all components of the same type are stored contiguously) and memcpy that, then repeat for each component. if buffers can have different capacity, then you can’t batch-copy the data but you need to calculate the offset of each entity. also you need to detect if you need to allocate or release heap memory

No, no: I was saying would need a different DynamicBuffer for each capacity. Not a different IBufferElementData for each. I think you might be using the term “DynamicBuffer” to describe both.

A “DynamicBuffer” can only have one capacity, like a collection. But one IBufferElementData can be used in multiple DynamicBuffers with different capacities.

^ I thought this was only true inside a chunk, and that even chunks of the same archetype may end up spread out in ram, as new ones are allocated. Am I wrong about that? Are all components of the same type in the entire game, across multiple archetypes, stored contiguously?

  1. so you basically want to define multiple ComponentType for one IBufferElementData struct, like the old FixedArray(typeof(X), N)?
    I think FixedArray was removed for a reason…

  2. that is true only inside a chunk, but a chunk is still huge wrt. a cache line.
    when copying data with an EQ, DOTS could do like “grab a chunk, memcpy each component array, go to next chunk”. (unless it needs to deal with different buffer capacities)