diff --git a/bumpdriver.h b/bumpdriver.h index bbabb98..11c96b8 100644 --- a/bumpdriver.h +++ b/bumpdriver.h @@ -1,28 +1,32 @@ + +DriverPtr GL_WrapUserPointer(void *p); +DriverPtr GL_OffsetDriverPtr(DriverPtr p, int offset); + /** * Gives a definition of the vertices passed to the shedule* functions. * Use NULL if the data is not available/applicable to the suff you send. */ typedef struct { - float *vertices; + DriverPtr vertices; int vertexstride; - float *texcoords; + DriverPtr texcoords; int texcoordstride; - float *lightmapcoords; + DriverPtr lightmapcoords; int lightmapstride; - float *tangents; + DriverPtr tangents; int tangentstride; - float *binormals; + DriverPtr binormals; int binormalstride; - float *normals; + DriverPtr normals; int normalstride; - unsigned char *colors; + DriverPtr colors; int colorstride; } vertexdef_t; @@ -33,18 +37,6 @@ typedef struct { vec3_t objectvieworg; } lightobject_t; -/** -* Defines driver managed memory types -*/ -typedef enum {DM_SLOWREADWRITE, DM_SLOWREAD, DM_NORMAL} drivermem_t; - -//DM_SLOWREADWRITE: the memory is slow in writing and reading, won't be updated outside of -//the driver much -//DM_SLOWREAD: the memory is slow in reading (uncached or worse...), won't be read outside -//of the driver much. It supports decent writing speeds. (This is probably what you'll want -//most of the time.) -//DM_NORMAL: Fast reading and writing - /** * This is a generic bumpdriver struct, it contains al the driver routines */ @@ -56,18 +48,7 @@ typedef struct { void (*initDriver) (void); void (*freeDriver) (void); - //gets a pointer to driver memory, it can just return system memory too if the driver - //doesn't support it. - void *(*getDriverMem) (size_t size, drivermem_t hint); - - //frees all driver mem - //FIXME: do we need real deallocation support - void (*freeAllDriverMem) (void); - - //FIXME: Do we need fence like support? - //drawing code - void (*drawTriangleListBase) (vertexdef_t *verts, int *indecies, int numIndecies, shader_t *shader, int lightmapIndex);//-1 for no lightmap void (*drawTriangleListBump) (const vertexdef_t *verts, int *indecies, int numIndecies, shader_t *shader, const transform_t *tr, const lightobject_t *lo); void (*drawTriangleListSys) (vertexdef_t *verts, int *indecies, int numIndecies, shader_t *shader); diff --git a/gl_drivermem.c b/gl_drivermem.c new file mode 100644 index 0000000..6a5f21a --- /dev/null +++ b/gl_drivermem.c @@ -0,0 +1,382 @@ +/* +Copyright (C) 2003 Tenebrae Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +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 +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +--- + +Driver managed vertex memory + +The all routines return DriverPtr's that can be translated and used trough +the use of these routines. +A segment is just a single opengl VBO object, this should abstract the memory management +spanning difference VBO objects because of driver maximum VBO sizes. +*/ + +#include "quakedef.h" + +//number of segments to keep track of +#define MAX_SEGMENTS 256 +//maximum size of a segment (this maybe driver dependent) +#define MAX_SEGMENTSIZE (1024*1024*16) +//size of the allocated segments +#define OPTIMAL_SEGMENTSIZE (1024*1024*4) + +DriverPtr nullDriver = {0, 0}; //Segment + +typedef struct { + int size; + int freeOffset; + int mapCount; + qboolean isSystemMem; //This segment is system memory and not vbo mem (not supported or no free vbo space left) + void *nonVboData; //only nonull if isSystemMem is true + void *mapData; //only nonull if isSystemMem is true +} segmentdescriptor_t; + +static segmentdescriptor_t segmentDescriptors[MAX_SEGMENTS]; +static int numSegments = 0; + +/******************************************************************* + + Our own little abstraction layer that abstracts vbo and system mem + if vbo is not available. + +*******************************************************************/ + +static int GL_CreateSegment(size_t size, void *data, int usage) { + void *nonVboData; + qboolean isSystemMem = false; + + if (numSegments >= MAX_SEGMENTS) + Sys_Error("GL_CreateSegment: No segments left\n"); + + if (gl_vbo) { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, numSegments); + qglBufferDataARB(GL_ARRAY_BUFFER_ARB, size, data,usage); + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + nonVboData = NULL; + //fixme check for OUT_OF_MEMORY error + } else { + nonVboData = malloc(size); + isSystemMem = true; + if (data) { + memcpy(nonVboData,data, size); + } + } + + segmentDescriptors[numSegments].mapCount = 0; + segmentDescriptors[numSegments].nonVboData = nonVboData; + segmentDescriptors[numSegments].isSystemMem = isSystemMem; + segmentDescriptors[numSegments].size = size; + segmentDescriptors[numSegments].freeOffset = 0; + numSegments++; + return numSegments-1; +} + + +static void GL_FreeSegment(int segment) { + segmentdescriptor_t *s; + + if (segment != (numSegments-1)) + Sys_Error("GL_FreeSegment: Only last created segment can be freed\n"); + + s = &segmentDescriptors[segment]; + if (s->isSystemMem) { + if (s->nonVboData) + free(s->nonVboData); + } else { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, segment); + qglBufferDataARB(GL_ARRAY_BUFFER_ARB, 0, NULL, GL_STREAM_DRAW_ARB); + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + } + + numSegments--; +} +/* +void *GL_MapToUserSpace(DriverPtr p) { + byte *data; + segmentdescriptor_t *s; + + if ((p.segment < 0) || (p.segment >= numSegments)) Sys_Error("GL_MapToUserSpace: Invalid segment"); + s = &segmentDescriptors[p.segment]; + + if (s->isSystemMem) { + return ((byte *)s->nonVboData)+p.offset; + } else { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, p.segment); + data = qglMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_READ_WRITE_ARB); + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + return data+p.offset; + } +} + +void GL_UnmapFromUserSpace(DriverPtr p) { + segmentdescriptor_t *s; + + if ((p.segment < 0) || (p.segment >= numSegments)) Sys_Error("GL_UnmapFromUserSpace: Invalid segment"); + s = &segmentDescriptors[p.segment]; + if (!s->isSystemMem) { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, p.segment); + if (!qglUnmapBufferARB(GL_ARRAY_BUFFER_ARB)) { + Sys_Error("GL_UnmapFromUserSpace: Buffer data store was corrupted\n"); + } + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + } +} +*/ +/** +Maps the driver mem pointer to the proces's adress space +*/ +void *GL_MapToUserSpace(DriverPtr p) { + byte *data; + segmentdescriptor_t *s; + + if ((p.segment < 0) || (p.segment >= numSegments)) Sys_Error("GL_MapToUserSpace: Invalid segment"); + s = &segmentDescriptors[p.segment]; + + if (s->isSystemMem) { + return ((byte *)s->nonVboData)+p.offset; + } else { + if (s->mapCount == 0) { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, p.segment); + s->mapData = qglMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_READ_WRITE_ARB); + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + } + s->mapCount++; + return (byte *)s->mapData+p.offset; + } +} + +/** +Unmaps the driver mem pointer from the adress space +This MUST! be called before the driver can use the memory +*/ +void GL_UnmapFromUserSpace(DriverPtr p) { + segmentdescriptor_t *s; + + if ((p.segment < 0) || (p.segment >= numSegments)) Sys_Error("GL_UnmapFromUserSpace: Invalid segment"); + s = &segmentDescriptors[p.segment]; + if (!s->isSystemMem) { + s->mapCount--; + if (s->mapCount == 0) { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, p.segment); + if (!qglUnmapBufferARB(GL_ARRAY_BUFFER_ARB)) { + Sys_Error("GL_UnmapFromUserSpace: Buffer data store was corrupted\n"); + } + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + } + } +} + +/** +Converts a normal pointer to a VBO compatible pointer (used for some of the geometry that is still in system mem) +*/ +DriverPtr GL_WrapUserPointer(void *p) { + DriverPtr dr; + dr.segment = 0; + dr.offset = (byte *)p-NULL; + return dr; +} + +/** +This is probably faster than doing a Map and then a memcpy and then an unmap +*/ +void drivermemcpy(DriverPtr dest, void *src, size_t size) { + segmentdescriptor_t *s; + + if ((dest.segment < 0) || (dest.segment >= numSegments)) Sys_Error("GL_UnmapFromUserSpace: Invalid segment"); + s = &segmentDescriptors[dest.segment]; + if (!s->isSystemMem) { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, dest.segment); + qglBufferSubDataARB(GL_ARRAY_BUFFER_ARB, dest.offset, size, src); + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + } else { + memcpy((byte *)s->nonVboData+dest.offset, src, size); + } +} + +//This will have less vbo state changes, but the spec mentions binding another is cheap anyway +//it's the pointer changing that is expensive +#define RESET_VBO 1 + +void GL_TexCoordPointer(GLint size, GLenum type, GLsizei stride, DriverPtr p) { + segmentdescriptor_t *s; + if ((p.segment < 0) || (p.segment >= numSegments)) Sys_Error("GL_MapToUserSpace: Invalid segment"); + s = &segmentDescriptors[p.segment]; + + if (s->isSystemMem) { + if (gl_vbo) qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + glTexCoordPointer(size, type, stride, (byte *)s->nonVboData+p.offset); + } else { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, p.segment); + glTexCoordPointer(size, type, stride, (byte *)s->nonVboData+p.offset); +#ifdef RESET_VBO + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); +#endif + } +} + +void GL_VertexPointer(GLint size, GLenum type, GLsizei stride, DriverPtr p) { + segmentdescriptor_t *s; + if ((p.segment < 0) || (p.segment >= numSegments)) Sys_Error("GL_MapToUserSpace: Invalid segment"); + s = &segmentDescriptors[p.segment]; + + if (s->isSystemMem) { + if (gl_vbo) qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + glVertexPointer(size, type, stride, (byte *)s->nonVboData+p.offset); + } else { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, p.segment); + glVertexPointer(size, type, stride, (byte *)s->nonVboData+p.offset); +#ifdef RESET_VBO + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); +#endif + } +} + +void GL_NormalPointer(GLenum type, GLsizei stride, DriverPtr p) { + segmentdescriptor_t *s; + if ((p.segment < 0) || (p.segment >= numSegments)) Sys_Error("GL_MapToUserSpace: Invalid segment"); + s = &segmentDescriptors[p.segment]; + + if (s->isSystemMem) { + if (gl_vbo) qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + glNormalPointer(type, stride, (byte *)s->nonVboData+p.offset); + } else { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, p.segment); + glNormalPointer(type, stride, (byte *)s->nonVboData+p.offset); +#ifdef RESET_VBO + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); +#endif + } +} + +void GL_ColorPointer(GLint size, GLenum type, GLsizei stride, DriverPtr p) { + segmentdescriptor_t *s; + if ((p.segment < 0) || (p.segment >= numSegments)) Sys_Error("GL_MapToUserSpace: Invalid segment"); + s = &segmentDescriptors[p.segment]; + + if (s->isSystemMem) { + if (gl_vbo) qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + glColorPointer(size, type, stride, (byte *)s->nonVboData+p.offset); + } else { + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, p.segment); + glColorPointer(size, type, stride, (byte *)s->nonVboData+p.offset); +#ifdef RESET_VBO + qglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); +#endif + } +} + +DriverPtr GL_OffsetDriverPtr (DriverPtr p, int offset) { + p.offset+=offset; + return p; +} + + +/******************************************************************* + + Driver memory public interface + +*******************************************************************/ + +/** + Allocates size bytes of memory and copies data to the block. + Returns a pointer to driver memory. + This memory is "slow" if you use the map routines, it should + not be written or read often. + This memory cannot be freed, it is freed automatically when + GL_FreeAll is called. +*/ +DriverPtr GL_StaticAlloc(size_t size, void *data) { + segmentdescriptor_t *s; + int i, segment; + DriverPtr p; + + if (size > MAX_SEGMENTSIZE) + Sys_Error("GL_StaticAlloc: Tried to allocate more than MAX_SEGMENTSIZE vertex buffer memory\n"); + + //If it's to big put it in a separate segment + if (size > OPTIMAL_SEGMENTSIZE-(OPTIMAL_SEGMENTSIZE/4)) { + int segment = GL_CreateSegment(size, data, GL_STATIC_DRAW_ARB); + segmentDescriptors[segment].freeOffset = size; + p.segment = segment; + p.offset = 0; + return p; + } + + //Find space in an existing segment + for (i=0, s=segmentDescriptors; isize - s->freeOffset)) { + p.segment = i; + p.offset = s->freeOffset; + s->freeOffset += size; + drivermemcpy(p, data, size); + return p; + } + } + + //Allocate a new segment + segment = GL_CreateSegment(OPTIMAL_SEGMENTSIZE, NULL, GL_STATIC_DRAW_ARB); + segmentDescriptors[segment].freeOffset = size; + p.segment = segment; + p.offset = 0; + drivermemcpy(p, data, size); + return p; +} + +/** + Allocates size bytes of memory and copies data to the block. + Returns a pointer to driver memory. + This is write once every few frames use multiple times every frame + sort of memory, so reasonably fast to write. + This memory cannot be freed, it is freed automatically when + GL_FreeAll is called (Use the vertex cache for dynamically changing + memory) +*/ +DriverPtr GL_DynamicAlloc(size_t size, void *data) { + //Allocate a new segment + int segment = GL_CreateSegment(size, data, GL_DYNAMIC_DRAW_ARB); + DriverPtr p; + segmentDescriptors[segment].freeOffset = size; + p.segment = segment; + p.offset = 0; + return p; +} + +/** + Frees all driver memory. + (This should be called at level changes for example) +*/ +void GL_FreeAll(void) { + int i; + for (i=numSegments-1; i>=0; i--) { + //free this segment + GL_FreeSegment(i); + } +} + +void GL_InitDriverMem(void) { + //vbo with id 0 is a special case in opengl make a fake entry in our segment table + segmentDescriptors[0].freeOffset = 0; + segmentDescriptors[0].size = 0; + segmentDescriptors[0].nonVboData = 0; + segmentDescriptors[0].isSystemMem = true; + numSegments = 1; +} + +void GL_FreeDriverMem(void) { + GL_FreeAll(); +} \ No newline at end of file diff --git a/gl_drivermem.h b/gl_drivermem.h new file mode 100644 index 0000000..01554a5 --- /dev/null +++ b/gl_drivermem.h @@ -0,0 +1,20 @@ + +extern DriverPtr nullDriver; +#define DRVNULL nullDriver + +#define IsNullDriver(d) (((d).segment == nullDriver.segment) && ((d).offset == nullDriver.offset)) + + +void *GL_MapToUserSpace(DriverPtr p); +void GL_UnmapFromUserSpace(DriverPtr p); +void drivermemcpy(DriverPtr dest, void *src, size_t size); +DriverPtr GL_StaticAlloc(size_t size, void *data); +DriverPtr GL_DynamicAlloc(size_t size, void *data); +void GL_FreeAll(void); +void GL_InitDriverMem(void); +void GL_FreeDriverMem(void); + +void GL_InitVertexCache(void); +void GL_FreeVertexCache(void); +void GL_AllocVertexCache(const size_t size, DriverPtr *owner); +void GL_FlushVertexCache(void); \ No newline at end of file diff --git a/gl_vertexcache.c b/gl_vertexcache.c new file mode 100644 index 0000000..f12097b --- /dev/null +++ b/gl_vertexcache.c @@ -0,0 +1,269 @@ +/* +Copyright (C) 2003 Tenebrae Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +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 +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +--- + +Vertex memory cache, this is used for dynamically changing stuff, like +skinned meshes or interpolated keyframes. (Wich are all done on the +cpu because of shadow volumes) +Oh, and this code is based on the Q1 surface cache... (A rover that +throws away old stuff untill it has enough space) + +*/ + +#include "quakedef.h" + +typedef struct vertexcacheitem_s +{ + struct vertexcacheitem_s *next; + DriverPtr *owner;// NULL is an empty chunk of memory + size_t size; // of data part + DriverPtr buffer; //Actual memory +} vertexcacheitem_t; + +//A single vertex cache, a cache is never bigger than 65k verts so we may have +//multiple caches +typedef struct { + DriverPtr buffer; //Actual memory this cache is controlling + vertexcacheitem_t *items; + vertexcacheitem_t *rover; + size_t size; //Size in bytes of this cache + size_t free; //Free memory in this cache, it may be fragmented tough +} vertexcache_t; + +#define VERTEXCACHE_DEBUG 1 +#define GUARDSIZE 4 + +//These are used to detect if we are thrasing the cache +static qboolean gl_roverwrapped; +static size_t gl_initial_offset; + +static vertexcacheitem_t *AllocCacheItem(void) { + return malloc(sizeof(vertexcacheitem_t)); +} + +static void FreeCacheItem(vertexcacheitem_t *i) { + free(i); +} + +/** + This may be dead slow, if it's uncached mem: debug only +*/ +static void VC_CheckCacheGuard (vertexcache_t *c) +{ + int i; + byte *s = GL_MapToUserSpace(c->buffer); + + s += c->size; + for (i=0 ; ibuffer); +} + +/** + This may be dead slow, if it's uncached mem: debug only +*/ +static void VC_ClearCacheGuard (vertexcache_t *c) +{ + int i; + byte *s = GL_MapToUserSpace(c->buffer); + + s += c->size; + for (i=0 ; ibuffer); +} + +static void VC_InitVertexCache(vertexcache_t *c, size_t size) +{ + vertexcacheitem_t *i; + + Con_Printf ("%ik vertex cache\n", size/1024); + + c->buffer = GL_DynamicAlloc(size + GUARDSIZE, NULL); + c->size = size; + c->free = size; + c->items = i = AllocCacheItem(); + c->rover = i; + + i->next = NULL; + i->owner = NULL; + i->size = size; + i->buffer = c->buffer; + + VC_ClearCacheGuard(c); +} + +static void VC_FlushVertexCache(vertexcache_t *c) +{ + vertexcacheitem_t *i, *n; + + i=c->items; + while(i) + { + if (i->owner) + *i->owner = DRVNULL; + n=i->next; + FreeCacheItem(i); + i = n; + } + + c->items = i = AllocCacheItem(); + i->next = NULL; + i->owner = NULL; + i->size = c->size; + i->buffer = c->buffer; + c->free = c->size; + c->rover = i; +} + +static void VC_FreeVertexCache(vertexcache_t *c) +{ + VC_FlushVertexCache(c); + FreeCacheItem(c->items); + c->items = NULL; +} + +static vertexcacheitem_t *VC_VertecCacheAlloc (vertexcache_t *c, size_t size) +{ + vertexcacheitem_t *new; + qboolean wrapped_this_time; + + if ((size <= 0) || (size > 0x400000)) //4 megabytes santity check + Sys_Error ("GL_VertecCacheAlloc: bad cache size %d\n", size); + + size = (size + 3) & ~3; + if (size > c->size) + Sys_Error ("GL_VertecCacheAlloc: %i > cache size",size); + + if (size > c->free) + Sys_Error ("GL_VertecCacheAlloc: %i > cache free",size); + + // if there is not size bytes after the rover, reset to the start + wrapped_this_time = false; + + if ( !c->rover || c->rover->buffer.offset > c->size - size) + { + if (c->rover) + { + wrapped_this_time = true; + } + c->rover = c->items; + } + + // colect and free surfcache_t blocks until the rover block is large enough + new = c->rover; + if (c->rover->owner) + *c->rover->owner = DRVNULL; + + while (new->size < size) + { + // free another + vertexcacheitem_t *old = c->rover; + c->rover = c->rover->next; + FreeCacheItem(old); + if (!c->rover) + Sys_Error ("GL_VertecCacheAlloc: hit the end of memory"); + if (c->rover->owner) + *c->rover->owner = DRVNULL; + + new->size += c->rover->size; + new->next = c->rover->next; + } + + // create a fragment out of any leftovers + if (new->size - size > 256) + { + c->rover = AllocCacheItem(); + c->rover->size = new->size - size; + c->rover->next = new->next; + c->rover->owner = NULL; + c->rover->buffer.segment = new->buffer.segment; + c->rover->buffer.offset = new->buffer.offset+size; + new->next = c->rover; + new->size = size; + } + else + c->rover = new->next; + + new->owner = NULL; // should be set properly after return + + if (gl_roverwrapped) + { + if (wrapped_this_time || (c->rover->buffer.offset >= gl_initial_offset)) + r_cache_thrash = true; + } + else if (wrapped_this_time) + { + gl_roverwrapped = true; + } + +#ifdef VERTEXCACHE_DEBUG + //Only in debug as this can be very slow due to the driver mem being uncached + VC_CheckCacheGuard (c); +#endif + return new; +} + +static void VC_VertecCacheDump (vertexcache_t *c) +{ + vertexcacheitem_t *test; + + for (test = c->items; test; test=test->next) { + if (test == c->rover) + Con_Printf("ROVER:\n"); + Con_Printf("%p : segment(%i) offset(%i) bytes(%i) owner(%p)\n",test, test->buffer.segment, test->buffer.offset, test->size, test->owner); + } +} + +/************************* + + Public interface + +*************************/ + +#define VERTEX_CACHE_SIZE (1024*1024*8) +vertexcache_t cache; + +void GL_InitVertexCache(void) { + VC_InitVertexCache(&cache, VERTEX_CACHE_SIZE); +} + +void GL_FreeVertexCache(void) { + VC_FreeVertexCache(&cache); +} + +/** + Owner will we overwritten with the pointer, if the cache later decides to free the allocated + cache spot, owner will be overwritten with NULL + So owner should point to "stable" memory, no temporaries on the stack please! +*/ +void GL_AllocVertexCache(const size_t size, DriverPtr *owner) { + vertexcacheitem_t *r = VC_VertecCacheAlloc(&cache, size); + *owner = r->buffer; + r->owner = owner; +} + +void GL_FlushVertexCache(void) { + VC_FlushVertexCache(&cache); +} \ No newline at end of file