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/old_d_version/util/regions.d

634 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 ) ); }
}
// 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(ulong 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;
}
ulong length() { return inUse.length; }
void length(ulong 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 ulong defaultBufferSize = 1024*1024;
// The size to use for new buffers.
ulong 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.
ulong 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()
{
// 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 = "", ulong 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;
}
ulong getBufferSize() { return bufferSize; }
// Allocates an array from the region.
ubyte[] allocate(ulong 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(ulong 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(ulong 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(ulong 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 GC's evil clutch.
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
ulong usedBuffers() { return currentBuffer; }
// Total number of allocated buffers
ulong totalBuffers()
{
ulong i;
// Count number of allocated buffers
while(i < buffers.length && buffers[i].length) i++;
return i;
}
// Total number of allocated bytes
ulong 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.)
ulong usedSize()
{
return currentBuffer*bufferSize - left.length;
}
// The total size of data that the user has requested.
ulong dataSize()
{
return usedSize() - lostSize();
}
// Number of lost bytes
ulong lostSize()
{
return lost;
}
// Total amount of allocated space that is not used
ulong 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();
}