• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

SC2 Object Allocation

Status
Not open for further replies.
Level 31
Joined
Jul 10, 2007
Messages
6,306
One of the nice things about Warcraft 3 was that it didn't have memory limits. SC2 on the other hand does have memory limits.

Now let us consider struct allocation and deallocation. Warcraft 3 vjass simply used a stack to spew out and recycle unique instances of a given struct. In SC2, the instances can be declared on the fly, but only globally declared instances may be kept (no structrefs allowed). This is problematic as a map may have many units, thus requiring many structs, thus running into the memory cap.

If one were to use 8192 as the limit for each struct, the cap would be reached after 10-20 structs.

One can use the data table to allocate sets of data, but this is extremely, extremely, extremely slow and extremely clunky.


So then what is the solution?


Thoughts and opinions please, I am trying to solve this ^_^.

Consider a map with 12 players and 600 units for each player with 30 structs (with multiple instances of each struct) containing lots of data for each and every unit. Impossible for SC2 to handle?

The above is a very realistic scenario for me (from combat system and AI alone), so I really need to nail down a good way to do object allocation in SC2.

I look forward to hearing people's feedback ^_-.

edit
Look at the below code for a first hand look at how most things just really aren't possible in SC2 atm (unless we can figure out a good way to do the above)
Code:
const int LIST_SIZE = 100;
struct List {
    int[LIST_SIZE] next;
    int[LIST_SIZE] prev;
};
void addToList(structref<List> list, int node) {
    list.prev[node] = list.prev[0];
    list.next[node] = 0;
    list.next[list.prev[0]] = node;
    list.prev[0] = node;
}
void removeFromList(structref<List> list, int node) {
    list.prev[list.next[node]] = list.next[node];
    list.next[list.prev[node]] = list.prev[node];
}

const int QUEUE_SIZE = 100;
struct Queue {
    int[QUEUE_SIZE] next;
    int last;
};
void pushOnQueue(structref<Queue> queue, int node) {
    queue.next[queue.last] = node;
    queue.last = node;
    queue.next[node] = 0;
}
void popFromQueue(structref<Queue> queue) {
    queue.next[0] = queue.next[queue.next[0]];
}

const int MAX_ALLOC = 100;
typedef int[MAX_ALLOC] recyclerARR;
struct Recycler {
    recyclerARR stack;
    int instanceCount;
};
int allocate(structref<Recycler> recycler) {
    int pointer = recycler.stack[0];

    if (!pointer) {
        if (recycler.instanceCount == MAX_ALLOC) {
            TriggerDebugOutput(1, StringToText("ALLOCATION OVERFLOW"), true);
            return 0;
        } //if

        pointer = recycler.instanceCount + 1;
        recycler.instanceCount = pointer;
    } //if
    else {
        recycler.stack[0] = recycler.stack[pointer];
        recycler.stack[pointer] = -1;
    } //else

    return pointer;
} //allocate
void deallocate(structref<Recycler> recycler, int pointer) {
    if (recycler.stack[pointer] != -1) {
        TriggerDebugOutput(1, StringToText("ATTEMPT TO DEALLOCATE NULL POINTER"), true);
        return;
    } //if

    recycler.stack[pointer] = recycler.stack[0];
    recycler.stack[0] = pointer;
}

//Stack: count++, count--, very simple




//example
//
//  Recycler listRecycler
//  List list;
//  int node = allocate(listRecycler);
//  addToList(list, node);
//  removeFromList(list, node);
//  deallocate(listRecycler, node);

edit
Template support, either in a 3rd party language or in Galaxy, would be a step in the right direction.
 
Last edited:

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
The memory limit has been extended in patch 1.5. It is still finite but substantially larger (maybe even a few MB).

I advise you do some embedded system programming and come back. You will hear how silly you are sounding after trying to make devises with 2KB of memory with blocks of 255 bytes do complex stuff like interfacing with Bluetooth HCI.

1. Use simpler objects. Even if memory overhead is larger on average you can use memory more efficiently as many systems would share the same memory pool for that object type.
2. Use singletons. There is no need to dynamically allocate objects that for all purposes behave constant. An example would be systems that are per player and systems that are one object per use.
3. Do not excessively allocate arrays. It might be worth having a backup test for the rare case of when an object pool is depleted rather than allocating space that will never be used.

Do not think this is limited to embedded system design only. When designing operating systems like Linux or Windows they also have similar problems as you do not have access to a layer to support dynamic memory allocation (let alone how inefficient it is).

If StarCraft II supported proper C typecasting this would be no problem as one could write their own malloc then but sadly it does not (at least what I have seen).

I think you are used to poor scripting for WC3 and do not realise that you do not need to do such things in SC2.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Welp, here we go, dynamic memory allocation for SC2, thank me later. It still needs work so that it works with any type, but yar ;p.

This should probably be implemented into a 3rd party language.
Code:
//debug purposes only
void print(string msg) {
    TriggerDebugOutput(1, StringToText(msg), true);
}

static int[9] powers;
static const int MEMORY_SIZE = 8192;
static int[MEMORY_SIZE] memory;
static byte[MEMORY_SIZE/8] deallocatedCollections;

static const int TYPE_INT = 1;

static int allocatedMemory = 0;
static int freedMemory = 0;
static int releasedMemory = 0;

static bool initialized = false;

static bool getDeallocated(int pointer) { return 1 == (deallocatedCollections[pointer/8]/powers[pointer%8])%2; }
static void setDeallocated(int pointer, bool val) {
    //section: pointer/8
    //index: pointer%8
    //number: deallocatedCollections[section]
    //left: number/powers[index + 1]
    //middle: number/powers[index]%2
    //rightSize: powers[index]
    //right: number%rightSize
    //number = (left*2 + BoolToInt(val))*rightSize + right
    deallocatedCollections[pointer/8] = (deallocatedCollections[pointer/8]/powers[pointer%8 + 1]*2 + BoolToInt(val))*powers[pointer%8] + deallocatedCollections[pointer/8]%powers[pointer%8];
}

int read(int pointer) { return memory[pointer - TYPE_INT]; }
void write(int pointer, int value) { memory[pointer - TYPE_INT] = value; }

int getArraySize(int pointer) { return DataTableGetInt(true, "ARR_SIZE" + IntToString(pointer)); }
static void setArraySize(int pointer, int size) { DataTableSetInt(true, "ARR_SIZE" + IntToString(pointer), size); }

static int getMemorySize(int pointer) { return DataTableGetInt(true, "MEM_SIZE" + IntToString(pointer)); }
static void setMemorySize(int pointer, int size) { DataTableSetInt(true, "MEM_SIZE" + IntToString(pointer), size); }

static int getPosition(int pointer) { return DataTableGetInt(true, "HEAP_POSITION" + IntToString(pointer)); }
static int getPointer(int position) { return DataTableGetInt(true, "HEAP_POINTER" + IntToString(position)); }
static void setPosition(int pointer, int position) { DataTableSetInt(true, "HEAP_POSITION" + IntToString(pointer), position); }
static void setPointer(int position, int pointer) { DataTableSetInt(true, "HEAP_POINTER" + IntToString(position), pointer); }

static int getParentPosition(int position) { return position/2; }
static int getLeftPosition(int position) { return position*2; }
static int getRightPosition(int position) { return position*2 + 1; }

static void link(int pointer, int pointerPosition) {
    setPointer(pointerPosition, pointer);
    setPosition(pointer, pointerPosition);
}

static void initialize() {
    if (!initialized) {
        initialized = true;

        powers[0] = 1;
        powers[1] = 2;
        powers[2] = 4;
        powers[3] = 8;
        powers[4] = 16;
        powers[5] = 32;
        powers[6] = 64;
        powers[7] = 128;
        powers[8] = 256;
    } //if
}

//debug purposes only
void printMemory() {
    print("Used  Memory: " + IntToString(allocatedMemory - releasedMemory));
    print("Heap Size: " + IntToString(freedMemory));
}

static void bubbleUp(int pointer) {
    int pointerSize = getMemorySize(pointer);
    int pointerPosition = getPosition(pointer);
    int parentPosition = getParentPosition(pointerPosition);
    int parentPointer = getPointer(parentPosition);

    while (0 != parentPosition && getMemorySize(parentPointer) < pointerSize) {
        link(parentPointer, pointerPosition);

        pointerPosition = parentPosition;
        parentPosition = getParentPosition(pointerPosition);
        parentPointer = getPointer(parentPosition);
    }

    link(pointer, pointerPosition);
}

static void bubbleDown(int pointer) {
    int pointerSize  = getMemorySize(pointer);
    int pointerPosition = getPosition(pointer);

    int leftPosition = getLeftPosition(pointerPosition);
    int rightPosition = getRightPosition(pointerPosition);

    int leftPointer = getPointer(leftPosition);
    int rightPointer = getPointer(rightPosition);

    int leftSize = getMemorySize(leftPointer);
    int rightSize = getMemorySize(rightPointer);

    while ((0 != leftPointer && leftSize > pointerSize) || (0 != rightPointer && rightSize > pointerSize)) {
        if (0 == rightPointer || (0 != leftPointer && leftSize > rightSize)) {
            link(leftPointer, pointerPosition);

            pointerPosition = leftPosition;
        }
        else {
            link(rightPointer, pointerPosition);

            pointerPosition = rightPosition;
        }

        leftPosition = getLeftPosition(pointerPosition);
        rightPosition = getRightPosition(pointerPosition);

        leftPointer = getPointer(leftPosition);
        rightPointer = getPointer(rightPosition);

        leftSize = getMemorySize(leftPointer);
        rightSize = getMemorySize(rightPointer);
    }

    link(pointer, pointerPosition);
}

static void recycle(int pointerPosition) {
    int pointer = getPointer(freedMemory);

    setPointer(freedMemory, 0);

    link(pointer, pointerPosition);

    if (pointerPosition != 1) {
        bubbleUp(pointer);
    } //if

    bubbleDown(pointer);

    freedMemory = freedMemory - 1;
}

static void defragment(int pointer) {
    int pointerPosition = getPosition(pointer);
    int size = getMemorySize(pointer);
    int mergeSize;

    if (0 != pointer && getDeallocated(pointer - 1)) {
        setDeallocated(pointer, false);
        setMemorySize(pointer, 0);

        mergeSize = getMemorySize(pointer - 1);
        TriggerDebugOutput(1, StringToText("(") + IntToText(pointer - mergeSize) + StringToText(" .. ") + IntToText(pointer - 1) + StringToText(") + (") + IntToText(pointer) + StringToText(" .. ") + IntToText(pointer + size - 1) + StringToText(")"), true);
        size = size + mergeSize;

        if (1 < mergeSize) {
            setDeallocated(pointer - 1, false);
            setMemorySize(pointer - 1, 0);
        } //if

        pointer = pointer - mergeSize;

        recycle(getPosition(pointer));
    }

    if (pointer + size < MEMORY_SIZE && getDeallocated(pointer + size)) {
        setDeallocated(pointer + size - 1, false);
        setMemorySize(pointer + size - 1, 0);

        mergeSize = getMemorySize(pointer + size);
        TriggerDebugOutput(1, StringToText("(") + IntToText(pointer) + StringToText(" .. ") + IntToText(pointer + size - 1) + StringToText(") + (") + IntToText(pointer + size) + StringToText(" .. ") + IntToText(pointer + size + mergeSize - 1) + StringToText(")"), true);
        size = size + mergeSize;

        if (1 < mergeSize) {
            setDeallocated(pointer + size, false);
            setMemorySize(pointer + size, 0);
        } //if

        recycle(getPosition(pointer + size));
    }

    setMemorySize(pointer, size);
    setMemorySize(pointer + size - 1, size);

    link(pointer, pointerPosition);

    bubbleUp(pointer);
}

int allocate(int size) {
    int pointer = getPointer(1);
    int memorySize = getMemorySize(pointer);

    int pointerPosition;

    initialize();

    if (0 == freedMemory || memorySize < size) {
        if (allocatedMemory + size >= MEMORY_SIZE) {
            TriggerDebugOutput(1, StringToText("ALLOCATE OVERFLOW"), true);
            return 0;
        } //if

        pointer = allocatedMemory;
        allocatedMemory = allocatedMemory + size;
    } //if
    else {
        setMemorySize(pointer, 0);
        setDeallocated(pointer, false);

        setMemorySize(pointer + size, memorySize - size);
        setMemorySize(pointer + memorySize - 1, memorySize - size);

        if (0 == memorySize - size) {
            recycle(1);

            setDeallocated(pointer + memorySize - 1, false);
        }
        else {
            link(pointer + size, 1);
            setDeallocated(pointer + size, true);

            bubbleDown(pointer + size);
        }

        releasedMemory = releasedMemory - size;
    }

    pointer = pointer + TYPE_INT;
    setArraySize(pointer, size);

    return pointer;
}

void deallocate(int pointer) {
    int size = getArraySize(pointer);

    pointer = pointer - TYPE_INT;
    
    if (0 == size) {
        TriggerDebugOutput(1, StringToText("ATTEMPT TO DEALLOCATE NULL POINTER"), true);
        return;
    } //if

    setMemorySize(pointer, size);
    setArraySize(pointer + TYPE_INT, 0);

    releasedMemory = releasedMemory + size;

    freedMemory = freedMemory + 1;
    link(pointer, freedMemory);
    bubbleUp(pointer);

    setDeallocated(pointer, true);
    setDeallocated(pointer + size - 1, true);

    setMemorySize(pointer + size - 1, size);

    defragment(pointer);
}

Demo
Code:
include "E:\\Program Files (x86)\\StarCraft II\\Mods\\allocate.galaxy"

//int allocate(int size)
//void deallocate(int pointer)
//void reset(int pointer)
void tester() {
    int[50] arr;
    int i = 50;

    while (i != 0) {
        i = i - 1;
        arr[i] = allocate(RandomInt(10, 25));
    }

    printMemory();

    deallocate(arr[33]);
    deallocate(arr[23]);
    deallocate(arr[11]);
    deallocate(arr[1]);
    deallocate(arr[5]);
    deallocate(arr[3]);
    //deallocate(arr[12]);
    deallocate(arr[21]);
    deallocate(arr[22]);

    //deallocate(arr[33]);
    //deallocate(arr[22]);
    //deallocate(arr[11]);
    //deallocate(arr[0]);
    //deallocate(arr[44]);
    //deallocate(arr[48]);
    //deallocate(arr[42]);
    //deallocate(arr[41]);

    arr[22] = allocate(50);
    arr[21] = allocate(5);
    arr[23] = allocate(8);

    deallocate(arr[22]);
    deallocate(arr[21]);
    deallocate(arr[23]);

    i = 50;
    while (i != 0) {
        i = i - 1;
        if (getArraySize(arr[i]) > 0) {
            deallocate(arr[i]);
        }
    }

    printMemory();

    //deallocate(arr2);
    //deallocate(arr);

    //print(IntToString(freedMemory));

    //print("Pointer: " + IntToString(arr));
    //print("Size: "+IntToString(size));
}
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
It uses datatables...

A possible allocation scheme would be General Purpose Objects (GPOs). The idea is that you break up memory into pre-defined objects which include a variety of types. As these units are standardised allocation and de-allocation becomes very easy. Internal memory fragemention is a problem then but this could be reduced by grouping many objects together into a single object and will not be a concern for small GPO sizes.

An example of what a GPO might have.
4 int
2 fixed
4 byte
2 unit
1 unitgroup
1 playergroup
1 string

With the total memory heap of the Galaxy system being 2 MB (2097152 Bytes) that would allow you to allocate approximately 40,000 such GPOs (assuming complex types are 4 bytes) while still having plenty of memory left for map systems. The system of allocation and de-allocation would be lightning fast as well.
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
Not sure I get what you mean

Here are the heaps (evil Galaxy, had to use 1 heap for each primitive)
Code:
    static int[MEMORY_TYPE_COUNT] allocatedMemory;
    static int[MEMORY_TYPE_COUNT] heapSize;
    static int[MEMORY_TYPE_COUNT] releasedMemory;

    //pointer size manipulation
    static int  getMemorySize       (int pointer)                               { return    DataTableGetInt(true, "MEM_SIZE" + IntToString(pointer)); }
    static void setMemorySize       (int pointer, int size)                     {           DataTableSetInt(true, "MEM_SIZE" + IntToString(pointer), size); }

    //pointer.heapPosition manipulation
    static int  getPosition         (int pointer, int memoryType)               { return    DataTableGetInt(true, "HEAP_POSITION_" + IntToString(memoryType) + "->" + IntToString(pointer)); }
    static void setPosition         (int pointer, int position, int memoryType) {           DataTableSetInt(true, "HEAP_POSITION_" + IntToString(memoryType) + "->" + IntToString(pointer), position); }

    //heapPosition.pointer manipulation
    static int  getPointer          (int position, int memoryType)              { return    DataTableGetInt(true, "HEAP_POINTER_" + IntToString(memoryType) + "->" + IntToString(position)); }
    static void setPointer          (int position, int pointer, int memoryType) {           DataTableSetInt(true, "HEAP_POINTER_" + IntToString(memoryType) + "->" + IntToString(position), pointer); }

And the defrag stuff (used byte array instead of bool to save memory)
Code:
    static byte[
        ( MEMORY_SIZE_ABILCMD   + MEMORY_SIZE_AIFILTER              + MEMORY_SIZE_ACTOR     + MEMORY_SIZE_BOOL          + MEMORY_SIZE_BANK 
        + MEMORY_SIZE_BYTE      + MEMORY_SIZE_CAMERAINFO            + MEMORY_SIZE_COLOR     + MEMORY_SIZE_DOODAD        + MEMORY_SIZE_FIXED
        + MEMORY_SIZE_INT       + MEMORY_SIZE_MARKER                + MEMORY_SIZE_ORDER     + MEMORY_SIZE_PLAYERGROUP   + MEMORY_SIZE_POINT
        + MEMORY_SIZE_REGION    + MEMORY_SIZE_SOUND                 + MEMORY_SIZE_SOUNDLINK + MEMORY_SIZE_STRING        + MEMORY_SIZE_TEXT
        + MEMORY_SIZE_TIMER     + MEMORY_SIZE_TRANSMISSIONSOURCE    + MEMORY_SIZE_TRIGGER   + MEMORY_SIZE_UNIT          + MEMORY_SIZE_UNITFILTER
        + MEMORY_SIZE_UNITGROUP + MEMORY_SIZE_UNITREF               + MEMORY_SIZE_REVEALER  + MEMORY_SIZE_WAVE          + MEMORY_SIZE_WAVEINFO
        + MEMORY_SIZE_WAVETARGET )/8
    ] memoryChunks;

    //powers of 2 are used with memory fragments
    static int[9] powerOf2;
    static void initializePowersOf2() {
        powerOf2[0] = 1;
        powerOf2[1] = 2;
        powerOf2[2] = 4;
        powerOf2[3] = 8;
        powerOf2[4] = 16;
        powerOf2[5] = 32;
        powerOf2[6] = 64;
        powerOf2[7] = 128;
        powerOf2[8] = 256;
    } //initializePowersOf2

    static bool isMemoryChunkLimit      (int pointer)           { return 1 == (memoryChunks[pointer/8]/powerOf2[pointer%8])%2; }
    static void setMemoryChunkLimitFlag (int pointer, bool val) {              memoryChunks[pointer/8] = (memoryChunks[pointer/8]/powerOf2[pointer%8 + 1]*2 + BoolToInt(val))*powerOf2[pointer%8] + memoryChunks[pointer/8]%powerOf2[pointer%8]; }

As you see, the heap has 2 fields, one for pointer->position and the other for position->pointer. That is it.


The heap itself is just an integer representing heap size ; |.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
(Though I'm not sure if it does or doesn't do that because I'm still learning Galaxy ;D)

It doesn't. It's similar to those really fast c++ malloc schemes where they have memory reserved for the program and the program only uses that memory. This is the same idea (not like we had a choice but to do it this way to begin with since SC2 doesn't support dynamic memory allocation).
 
Oh I see.
I didn't know there was a memory limit before. I read that just now when I actually took the time to read the actual content of your post rather than just the script you put up :p

This memory limit doesn't seem reasonable to me at all.
Reminds me of how I was playing Minecraft a couple of days ago and that silly willy Java Virtual Machine ran out of memory. ;|
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,198
Why do you have a pre-computed array for powers of 2? StarCraft II does support left wise bit shift (<<).

Power(2, N) = 1<<N

2 MB is plenty of memory.

Can you stop showing us meaningless extracts of your code all the time. It is hard to judge what you are doing.
 
Last edited:
Status
Not open for further replies.
Top