You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openmw-tes3mp/util/regions.d

645 lines
15 KiB
D

/*
OpenMW - The completely unofficial reimplementation of Morrowind
Copyright (C) 2008 Nicolay Korslund
Email: < korslund@gmail.com >
WWW: http://openmw.snaptoad.com/
This file (regions.d) is part of the OpenMW package.
OpenMW is distributed as free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License
version 3, as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
version 3 along with this program. If not, see
http://www.gnu.org/licenses/ .
*/
module util.regions;
private import std.gc;
private import std.string;
private import std.c.stdlib;
private import monster.util.string;
alias std.c.stdlib.malloc malloc;
class RegionManagerException : Exception
{
this(char[] msg, char[] name)
{ super( format("Memory Region Manager '%s': %s", name, msg ) ); }
}
// I tried this as an inner class, and it didn't even remotely work. I
// think inner class support in DMD sucks quite a bit. Works ok now
// (but still had some problems - template support in general is still
// pretty patchy.)
// UPDATE: The above comment was written a long time ago, it might
// work now. But if it ain't broke, don't fix it.
// A resizable array using a region for memory allocation. These can
// safely be resized back and forth without wasting large amounts of
// memory.
class RegionBuffer(T)
{
final:
private:
T[] buffer;
T[] inUse;
int reserve = 20;
RegionManager reg;
public:
// Size is initial size, reserve gives an indicator of the minimum
// amount to increase the array with at once.
this(RegionManager m, int size = 0, int reserve = 0)
{
reg = m;
if(reserve) this.reserve = reserve;
if(size) alloc(size);
}
new(uint size, RegionManager r)
{
return r.allocate(size).ptr;
}
delete(void *p) { assert(0); }
// Check if the buffer can hold 'size' more elements. If not,
// increase it. NOTE: This works even if data = null, and inUse
// is not. This allows us to make copy-on-resize slices.
private void alloc(uint size)
{
if(inUse.length + size <= buffer.length)
{
inUse = buffer[0..inUse.length+size];
return;
}
// Allocate a new array, with more entries than requested
buffer = reg.allocateT!(T)(buffer.length + size + reserve);
// Copy the old data
buffer[0..inUse.length] = inUse;
// Set up the array in use
inUse = buffer[0..(inUse.length+size)];
}
T opIndex(int i)
{
return inUse[i];
}
T opIndexAssign(T val, int i) { return inUse[i] = val; }
RegionBuffer opSlice(int x, int y)
{
RegionBuffer b = new(reg) RegionBuffer(reg,0,reserve);
b.inUse = inUse[x..y];
return b;
}
RegionBuffer opSlice() { return opSlice(0,inUse.length); }
RegionBuffer opCatAssign(T t)
{
alloc(1);
inUse[$-1] = t;
return this;
}
RegionBuffer opCatAssign(T[] t)
{
alloc(t.length);
inUse[($-t.length)..$] = t;
return this;
}
RegionBuffer opCatAssign(RegionBuffer t)
{
alloc(t.inUse.length);
inUse[($-t.inUse.length)..$] = t.inUse;
return this;
}
RegionBuffer opCat(T t) { return this.dup() ~= t; }
RegionBuffer opCat(T[] t) { return this.dup() ~= t; }
RegionBuffer opCat(RegionBuffer t) { return this.dup() ~= t; }
RegionBuffer dup()
{
RegionBuffer b = new(reg) RegionBuffer(reg, inUse.length, reserve);
b.inUse[] = inUse[];
return b;
}
uint length() { return inUse.length; }
void length(uint size)
{
// Grow array
if(size > inUse.length) alloc(size - inUse.length);
// Shrink array
else inUse = inUse[0..size];
}
// For direct access
T[] array() { return inUse; }
}
alias RegionBuffer!(char) RegionCharBuffer;
alias RegionBuffer!(int) RegionIntBuffer;
/*
* Region manager, a mark-sweep memory manager. You use it to allocate
* a lot of buffers, and when you are done with them you deallocate
* them all in one fell sweep.
*
*/
class RegionManager
{
final:
private:
// Identifying name, used for error messages.
char[] name;
// Use a default buffer size of one meg. Might change later.
const uint defaultBufferSize = 1024*1024;
// The size to use for new buffer.
uint bufferSize;
// Current amount of space that is 'lost' in unused end-of-buffer
// areas. Since we have proceeded to other buffers, this space will
// remain unused until freeAll is called.
uint lost;
ubyte[][] buffers; // Actual memory buffers
void *gcRanges[]; // List of ranges added to gc
ubyte[] left; // Slice of what is left of the current buffer
int currentBuffer; // Index into the next unused buffer
int currentRange; // Index into the next unused gcRanges entry
void fail(char[] msg)
{
throw new RegionManagerException(msg, name);
}
// We have run out of space, add a new buffer. I want this to be an
// exception rather than the rule, the default bufferSize should be
// large enough to prevent this from being necessary in most cases.
void nextBuffer()
{
/*
if(currentBuffer != 0)
writefln("WARNING: adding new buffer, number ", currentBuffer);
*/
// We should never have to increase the number of buffers!
if(currentBuffer >= buffers.length) fail("Out of buffers");
// Set up the buffer (if it is not already allocated.)
//buffers[currentBuffer].length = bufferSize;
if(buffers[currentBuffer].length != bufferSize)
{
assert(buffers[currentBuffer].length == 0);
ubyte *p = cast(ubyte*)malloc(bufferSize);
if(!p) fail("Malloc failed");
buffers[currentBuffer] = p[0..bufferSize];
}
// Remember the amount of space we just lost
lost += left.length;
// The entire buffer is available to us
left = buffers[currentBuffer];
// Point to the next unused buffer
currentBuffer++;
}
public:
this(char[] name = "", uint bufferSize = defaultBufferSize)
{
this.name = name;
this.bufferSize = bufferSize;
// Pointers are cheap. Let's preallocate these arrays big enough
// from the start. It's inflexible, but on purpose. We shouldn't
// NEED to grow them later, so we don't.
buffers.length = 100;
gcRanges.length = 10000;
freeAll();
}
~this()
{
// Don't leave any loose ends dangeling for the GC.
freeAll();
// Kill everything
foreach(ubyte[] arr; buffers)
free(arr.ptr);
delete buffers;
delete gcRanges;
}
// Allocates an array from the region.
ubyte[] allocate(uint size)
{
if(size > bufferSize)
fail(format("Tried to allocate %d, but maximum allowed allocation size is %d",
size, bufferSize));
// If the array cannot fit inside this buffer, get a new one.
if(size > left.length) nextBuffer();
ubyte[] ret = left[0..size];
left = left[size..$];
//writefln("Allocated %d, %d left in buffer", size, left.length);
return ret;
}
// Allocate an array and add it to the GC as a root region. This
// should be used for classes and other data that might contain
// pointers / class references to GC-managed data.
ubyte[] allocateGC(uint size)
{
if(currentRange >= gcRanges.length)
fail("No more available GC ranges");
ubyte[] ret = allocate(size);
// Add it to the GC
void *p = ret.ptr;
std.gc.addRange(p, p+ret.length);
gcRanges[currentRange++] = p;
return ret;
}
// Allocate an array of a specific type, eg. to allocate 4 ints, do
// int[] array = allocateT!(int)(4);
template allocateT(T)
{
T[] allocateT(uint number)
{
return cast(T[])allocate(number * T.sizeof);
}
}
alias allocateT!(int) getInts;
alias allocateT!(char) getString;
template newT(T)
{
T* newT()
{
return cast(T*)allocate(T.sizeof);
}
}
template allocateGCT(T)
{
T[] allocateGCT(uint number)
{
return cast(T[])allocateGC(number * T.sizeof);
}
}
// Copies an array of a given type
template copyT(T)
{
T[] copyT(T[] input)
{
T[] output = cast(T[]) allocate(input.length * T.sizeof);
output[] = input[];
return output;
}
}
alias copyT!(int) copy;
alias copyT!(char) copy;
// Copies a string and ensures that it is null-terminated
char[] copyz(char[] str)
{
char[] res = cast(char[]) allocate(str.length+1);
res[$-1] = 0;
res = res[0..$-1];
res[] = str[];
return res;
}
// Resets the region manager, but does not deallocate the
// buffers. To do that, delete the object.
void freeAll()
{
lost = 0;
currentBuffer = 0;
left = null;
// Free the ranges from the clutch of the GC's hand.
foreach(inout void *p; gcRanges[0..currentRange])
if(p)
{
std.gc.removeRange(p);
p = null;
}
currentRange = 0;
}
// Number of used buffers, including the current one
uint usedBuffers() { return currentBuffer; }
// Total number of allocated buffers
uint totalBuffers()
{
uint i;
// Count number of allocated buffers
while(i < buffers.length && buffers[i].length) i++;
return i;
}
// Total number of allocated bytes
uint poolSize()
{
return bufferSize * totalBuffers();
}
// Total number of bytes that are unavailable for use. (They might
// not be used, as such, if they are at the end of a buffer but the
// next buffer is in use.)
uint usedSize()
{
return currentBuffer*bufferSize - left.length;
}
// The total size of data that the user has requested.
uint dataSize()
{
return usedSize() - lostSize();
}
// Number of lost bytes
uint lostSize()
{
return lost;
}
// Total amount of allocated space that is not used
uint wastedSize()
{
return poolSize() - dataSize();
}
// Give some general info and stats
char[] toString()
{
return format("Memory Region Manager '%s':", name,
"\n pool %s (%d blocks)", comma(poolSize), totalBuffers,
"\n used %s", comma(usedSize),
"\n data %s", comma(dataSize),
"\n wasted %s (%.2f%%)", comma(wastedSize),
poolSize()?100.0*wastedSize()/poolSize():0,
"\n lost %s (%.2f%%)", comma(lost), usedSize()?100.0*lost/usedSize:0);
}
// Get a RegionBuffer of a given type
template getBuffer(T)
{
RegionBuffer!(T) getBuffer(int size = 0, int reserve = 0)
{
return new(this) RegionBuffer!(T)(this,size,reserve);
}
}
alias getBuffer!(char) getCharBuffer;
alias getBuffer!(int) getIntBuffer;
}
unittest
{
RegionManager r = new RegionManager("UT", 100);
// Test normal allocations first
assert(r.poolSize == 0);
assert(r.usedSize == 0);
assert(r.dataSize == 0);
assert(r.lostSize == 0);
assert(r.wastedSize == 0);
ubyte [] arr = r.allocate(30);
void *p = arr.ptr;
assert(p == r.buffers[0].ptr);
assert(arr.length == 30);
assert(r.poolSize == 100);
assert(r.usedSize == 30);
assert(r.dataSize == 30);
assert(r.lostSize == 0);
assert(r.wastedSize == 70);
arr = r.allocate(70);
assert(arr.ptr == p + 30);
assert(arr.length == 70);
assert(r.poolSize == 100);
assert(r.usedSize == 100);
assert(r.dataSize == 100);
assert(r.lostSize == 0);
assert(r.wastedSize == 0);
// Overflow the buffer
p = r.allocate(2).ptr;
assert(p == r.buffers[1].ptr);
assert(r.poolSize == 200);
assert(r.usedSize == 102);
assert(r.dataSize == 102);
assert(r.lostSize == 0);
assert(r.wastedSize == 98);
// Overflow the buffer and leave lost space behind
r.freeAll();
assert(r.poolSize == 200);
assert(r.usedSize == 0);
assert(r.dataSize == 0);
assert(r.lostSize == 0);
assert(r.wastedSize == 200);
r.allocate(1);
r.allocate(100);
assert(r.poolSize == 200);
assert(r.usedSize == 200);
assert(r.dataSize == 101);
assert(r.lostSize == 99);
assert(r.wastedSize == 99);
// Try to allocate a buffer that is too large
bool threw = false;
try r.allocate(101);
catch(RegionManagerException e)
{
threw = true;
}
assert(threw);
// The object should still be in a valid state.
// Try an allocation with roots
assert(r.currentRange == 0);
arr = r.allocateGC(50);
assert(r.poolSize == 300);
assert(r.usedSize == 250);
assert(r.dataSize == 151);
assert(r.lostSize == 99);
assert(r.wastedSize == 149);
assert(r.currentRange == 1);
assert(r.gcRanges[0] == arr.ptr);
int[] i1 = r.allocateGCT!(int)(10);
assert(i1.length == 10);
assert(r.poolSize == 300);
assert(r.usedSize == 290);
assert(r.dataSize == 191);
assert(r.lostSize == 99);
assert(r.currentRange == 2);
assert(r.gcRanges[1] == i1.ptr);
r.freeAll();
assert(r.currentRange == 0);
assert(r.poolSize == 300);
assert(r.usedSize == 0);
assert(r.dataSize == 0);
assert(r.lostSize == 0);
// Allocate some floats
float[] fl = r.allocateT!(float)(24);
assert(fl.length == 24);
assert(r.poolSize == 300);
assert(r.usedSize == 96);
assert(r.dataSize == 96);
assert(r.lostSize == 0);
// Copy an array
r.freeAll();
char[] stat = "hello little guy";
assert(r.dataSize == 0);
char[] copy = r.copy(stat);
assert(copy == stat);
assert(copy.ptr != stat.ptr);
copy[0] = 'a';
copy[$-1] = 'a';
assert(stat != copy);
assert(stat == "hello little guy");
assert(r.dataSize == stat.length);
// Test copyz()
r.freeAll();
stat = "ABC";
char *pp = cast(char*) r.copyz(stat).ptr;
assert(pp[2] == 'C');
assert(pp[3] == 0);
copy = r.copyz(stat);
assert(cast(char*)copy.ptr - pp == 4);
assert(pp[4] == 'A');
copy[0] = 'F';
assert(pp[3] == 0);
assert(pp[4] == 'F');
// Test of the buffer function
r.freeAll();
RegionBuffer!(int) b = r.getBuffer!(int)();
assert(b.inUse.length == 0);
assert(b.reserve == 20);
b.reserve = 5;
assert(b.length == 0);
b ~= 10;
b ~= 13;
assert(b.length == 2);
assert(b[0] == 10);
assert(b[1] == 13);
assert(b.buffer.length == 6);
p = b.buffer.ptr;
b.length = 0;
b.length = 6;
assert(p == b.buffer.ptr);
assert(b.length == 6);
b.length = 3;
assert(p == b.buffer.ptr);
assert(b.length == 3);
b[2] = 167;
b.length = 7;
assert(p != b.buffer.ptr); // The buffer was reallocated
assert(b.length == 7);
assert(b[2] == 167);
i1 = new int[5];
foreach(int v, inout int i; i1)
i = v;
p = b.buffer.ptr;
RegionBuffer!(int) a = b ~ i1;
assert(p != a.buffer.ptr); // A new buffer has been allocated
assert(p == b.buffer.ptr); // B should be unchanged
assert(a.length == b.length + i1.length);
for(int i=0; i < b.length; i++)
assert(a[i] == b[i]);
for(int i=0; i < i1.length; i++)
assert(a[i+7] == i);
// Make sure the arrays are truly different
a[5] = b[5] + 2;
assert(a[5] != b[5]);
// Make a slice
a = b[2..5];
assert(a.inUse.ptr == b.inUse.ptr+2);
a[1] = 4;
assert(a[1] == b[3]);
b[3] = -12;
assert(a[1] == b[3]);
a.length = a.length + 1;
assert(a.inUse.ptr != b.inUse.ptr+2);
a[1] = 4;
assert(a[1] != b[3]);
b[3] = -12;
assert(a[1] != b[3]);
r.freeAll();
}