| /*------------------------------------------------------------------------- |
| * drawElements Quality Program OpenGL Utilities |
| * --------------------------------------------- |
| * |
| * Copyright 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| *//*! |
| * \file |
| * \brief Draw call utilities. |
| *//*--------------------------------------------------------------------*/ |
| |
| #include "gluDrawUtil.hpp" |
| #include "gluRenderContext.hpp" |
| #include "gluObjectWrapper.hpp" |
| #include "glwFunctions.hpp" |
| #include "glwEnums.hpp" |
| #include "deInt32.h" |
| #include "deMemory.h" |
| |
| #include <vector> |
| #include <set> |
| #include <iterator> |
| |
| namespace glu |
| { |
| namespace |
| { |
| |
| struct VertexAttributeDescriptor |
| { |
| int location; |
| VertexComponentType componentType; |
| VertexComponentConversion convert; |
| int numComponents; |
| int numElements; |
| int stride; //!< Stride or 0 if using default stride. |
| const void* pointer; //!< Pointer or offset. |
| |
| VertexAttributeDescriptor (int location_, |
| VertexComponentType componentType_, |
| VertexComponentConversion convert_, |
| int numComponents_, |
| int numElements_, |
| int stride_, |
| const void* pointer_) |
| : location (location_) |
| , componentType (componentType_) |
| , convert (convert_) |
| , numComponents (numComponents_) |
| , numElements (numElements_) |
| , stride (stride_) |
| , pointer (pointer_) |
| { |
| } |
| |
| VertexAttributeDescriptor (void) |
| : location (0) |
| , componentType (VTX_COMP_TYPE_LAST) |
| , convert (VTX_COMP_CONVERT_LAST) |
| , numComponents (0) |
| , numElements (0) |
| , stride (0) |
| , pointer (0) |
| { |
| } |
| }; |
| |
| struct VertexBufferLayout |
| { |
| int size; |
| std::vector<VertexAttributeDescriptor> attributes; |
| |
| VertexBufferLayout (int size_ = 0) |
| : size(size_) |
| { |
| } |
| }; |
| |
| struct VertexBufferDescriptor |
| { |
| deUint32 buffer; |
| std::vector<VertexAttributeDescriptor> attributes; |
| |
| VertexBufferDescriptor (deUint32 buffer_ = 0) |
| : buffer(buffer_) |
| { |
| } |
| }; |
| |
| class VertexBuffer : public Buffer |
| { |
| public: |
| enum Type |
| { |
| TYPE_PLANAR = 0, //!< Data for each vertex array resides in a separate contiguous block in buffer. |
| TYPE_STRIDED, //!< Vertex arrays are interleaved. |
| |
| TYPE_LAST |
| }; |
| |
| VertexBuffer (const RenderContext& context, int numBindings, const VertexArrayBinding* bindings, Type type = TYPE_PLANAR); |
| ~VertexBuffer (void); |
| |
| const VertexBufferDescriptor& getDescriptor (void) const { return m_layout; } |
| |
| private: |
| VertexBuffer (const VertexBuffer& other); |
| VertexBuffer& operator= (const VertexBuffer& other); |
| |
| VertexBufferDescriptor m_layout; |
| }; |
| |
| class IndexBuffer : public Buffer |
| { |
| public: |
| IndexBuffer (const RenderContext& context, IndexType indexType, int numIndices, const void* indices); |
| ~IndexBuffer (void); |
| |
| private: |
| IndexBuffer (const IndexBuffer& other); |
| IndexBuffer& operator= (const IndexBuffer& other); |
| }; |
| |
| static deUint32 getVtxCompGLType (VertexComponentType type) |
| { |
| switch (type) |
| { |
| case VTX_COMP_UNSIGNED_INT8: return GL_UNSIGNED_BYTE; |
| case VTX_COMP_UNSIGNED_INT16: return GL_UNSIGNED_SHORT; |
| case VTX_COMP_UNSIGNED_INT32: return GL_UNSIGNED_INT; |
| case VTX_COMP_SIGNED_INT8: return GL_BYTE; |
| case VTX_COMP_SIGNED_INT16: return GL_SHORT; |
| case VTX_COMP_SIGNED_INT32: return GL_INT; |
| case VTX_COMP_FIXED: return GL_FIXED; |
| case VTX_COMP_HALF_FLOAT: return GL_HALF_FLOAT; |
| case VTX_COMP_FLOAT: return GL_FLOAT; |
| default: |
| DE_ASSERT(false); |
| return GL_NONE; |
| } |
| } |
| |
| static int getVtxCompSize (VertexComponentType type) |
| { |
| switch (type) |
| { |
| case VTX_COMP_UNSIGNED_INT8: return 1; |
| case VTX_COMP_UNSIGNED_INT16: return 2; |
| case VTX_COMP_UNSIGNED_INT32: return 4; |
| case VTX_COMP_SIGNED_INT8: return 1; |
| case VTX_COMP_SIGNED_INT16: return 2; |
| case VTX_COMP_SIGNED_INT32: return 4; |
| case VTX_COMP_FIXED: return 4; |
| case VTX_COMP_HALF_FLOAT: return 2; |
| case VTX_COMP_FLOAT: return 4; |
| default: |
| DE_ASSERT(false); |
| return 0; |
| } |
| } |
| |
| static deUint32 getIndexGLType (IndexType type) |
| { |
| switch (type) |
| { |
| case INDEXTYPE_UINT8: return GL_UNSIGNED_BYTE; |
| case INDEXTYPE_UINT16: return GL_UNSIGNED_SHORT; |
| case INDEXTYPE_UINT32: return GL_UNSIGNED_INT; |
| default: |
| DE_ASSERT(false); |
| return 0; |
| } |
| } |
| |
| static int getIndexSize (IndexType type) |
| { |
| switch (type) |
| { |
| case INDEXTYPE_UINT8: return 1; |
| case INDEXTYPE_UINT16: return 2; |
| case INDEXTYPE_UINT32: return 4; |
| default: |
| DE_ASSERT(false); |
| return 0; |
| } |
| } |
| |
| static deUint32 getPrimitiveGLType (PrimitiveType type) |
| { |
| switch (type) |
| { |
| case PRIMITIVETYPE_TRIANGLES: return GL_TRIANGLES; |
| case PRIMITIVETYPE_TRIANGLE_STRIP: return GL_TRIANGLE_STRIP; |
| case PRIMITIVETYPE_TRIANGLE_FAN: return GL_TRIANGLE_FAN; |
| case PRIMITIVETYPE_LINES: return GL_LINES; |
| case PRIMITIVETYPE_LINE_STRIP: return GL_LINE_STRIP; |
| case PRIMITIVETYPE_LINE_LOOP: return GL_LINE_LOOP; |
| case PRIMITIVETYPE_POINTS: return GL_POINTS; |
| case PRIMITIVETYPE_PATCHES: return GL_PATCHES; |
| default: |
| DE_ASSERT(false); |
| return 0; |
| } |
| } |
| |
| //! Lower named bindings to locations and eliminate bindings that are not used by program. |
| template<typename InputIter, typename OutputIter> |
| static OutputIter namedBindingsToProgramLocations (const glw::Functions& gl, deUint32 program, InputIter first, InputIter end, OutputIter out) |
| { |
| for (InputIter cur = first; cur != end; ++cur) |
| { |
| const BindingPoint& binding = cur->binding; |
| if (binding.type == BindingPoint::BPTYPE_NAME) |
| { |
| DE_ASSERT(binding.location >= 0); |
| int location = gl.getAttribLocation(program, binding.name.c_str()); |
| if (location >= 0) |
| { |
| // Add binding.location as an offset to accommodate matrices. |
| *out = VertexArrayBinding(BindingPoint(location + binding.location), cur->pointer); |
| ++out; |
| } |
| } |
| else |
| { |
| *out = *cur; |
| ++out; |
| } |
| } |
| |
| return out; |
| } |
| |
| static deUint32 getMinimumAlignment (const VertexArrayPointer& pointer) |
| { |
| // \todo [2013-05-07 pyry] What is the actual min? |
| DE_UNREF(pointer); |
| return (deUint32)sizeof(float); |
| } |
| |
| template<typename BindingIter> |
| static bool areVertexArrayLocationsValid (BindingIter first, BindingIter end) |
| { |
| std::set<int> usedLocations; |
| for (BindingIter cur = first; cur != end; ++cur) |
| { |
| const BindingPoint& binding = cur->binding; |
| |
| if (binding.type != BindingPoint::BPTYPE_LOCATION) |
| return false; |
| |
| if (usedLocations.find(binding.location) != usedLocations.end()) |
| return false; |
| |
| usedLocations.insert(binding.location); |
| } |
| |
| return true; |
| } |
| |
| // \todo [2013-05-08 pyry] Buffer upload should try to match pointers to reduce dataset size. |
| |
| static void appendAttributeNonStrided (VertexBufferLayout& layout, const VertexArrayBinding& va) |
| { |
| const int offset = deAlign32(layout.size, getMinimumAlignment(va.pointer)); |
| const int elementSize = getVtxCompSize(va.pointer.componentType)*va.pointer.numComponents; |
| const int size = elementSize*va.pointer.numElements; |
| |
| // Must be assigned to location at this point. |
| DE_ASSERT(va.binding.type == BindingPoint::BPTYPE_LOCATION); |
| |
| layout.attributes.push_back(VertexAttributeDescriptor(va.binding.location, |
| va.pointer.componentType, |
| va.pointer.convert, |
| va.pointer.numComponents, |
| va.pointer.numElements, |
| 0, // default stride |
| (const void*)(deUintptr)offset)); |
| layout.size = offset+size; |
| } |
| |
| template<typename BindingIter> |
| static void computeNonStridedBufferLayout (VertexBufferLayout& layout, BindingIter first, BindingIter end) |
| { |
| for (BindingIter iter = first; iter != end; ++iter) |
| appendAttributeNonStrided(layout, *iter); |
| } |
| |
| static void copyToLayout (void* dstBasePtr, const VertexAttributeDescriptor& dstVA, const VertexArrayPointer& srcPtr) |
| { |
| DE_ASSERT(dstVA.componentType == srcPtr.componentType && |
| dstVA.numComponents == srcPtr.numComponents && |
| dstVA.numElements == srcPtr.numElements); |
| |
| const int elementSize = getVtxCompSize(dstVA.componentType)*dstVA.numComponents; |
| const bool srcHasCustomStride = srcPtr.stride != 0 && srcPtr.stride != elementSize; |
| const bool dstHasCustomStride = dstVA.stride != 0 && dstVA.stride != elementSize; |
| |
| if (srcHasCustomStride || dstHasCustomStride) |
| { |
| const int dstStride = dstVA.stride != 0 ? dstVA.stride : elementSize; |
| const int srcStride = srcPtr.stride != 0 ? srcPtr.stride : elementSize; |
| |
| for (int ndx = 0; ndx < dstVA.numElements; ndx++) |
| deMemcpy((deUint8*)dstBasePtr + (deUintptr)dstVA.pointer + ndx*dstStride, (const deUint8*)srcPtr.data + ndx*srcStride, elementSize); |
| } |
| else |
| deMemcpy((deUint8*)dstBasePtr + (deUintptr)dstVA.pointer, srcPtr.data, elementSize*dstVA.numElements); |
| } |
| |
| void uploadBufferData (const glw::Functions& gl, deUint32 buffer, deUint32 usage, const VertexBufferLayout& layout, const VertexArrayPointer* srcArrays) |
| { |
| // Create temporary data buffer for upload. |
| std::vector<deUint8> localBuf(layout.size); |
| |
| for (int attrNdx = 0; attrNdx < (int)layout.attributes.size(); ++attrNdx) |
| copyToLayout(&localBuf[0], layout.attributes[attrNdx], srcArrays[attrNdx]); |
| |
| gl.bindBuffer(GL_ARRAY_BUFFER, buffer); |
| gl.bufferData(GL_ARRAY_BUFFER, (int)localBuf.size(), &localBuf[0], usage); |
| gl.bindBuffer(GL_ARRAY_BUFFER, 0); |
| GLU_EXPECT_NO_ERROR(gl.getError(), "Uploading buffer data failed"); |
| } |
| |
| // VertexBuffer |
| |
| VertexBuffer::VertexBuffer (const RenderContext& context, int numBindings, const VertexArrayBinding* bindings, Type type) |
| : Buffer(context) |
| { |
| const glw::Functions& gl = context.getFunctions(); |
| const deUint32 usage = GL_STATIC_DRAW; |
| VertexBufferLayout layout; |
| |
| if (!areVertexArrayLocationsValid(bindings, bindings+numBindings)) |
| throw tcu::TestError("Invalid vertex array locations"); |
| |
| if (type == TYPE_PLANAR) |
| computeNonStridedBufferLayout(layout, bindings, bindings+numBindings); |
| else |
| throw tcu::InternalError("Strided layout is not yet supported"); |
| |
| std::vector<VertexArrayPointer> srcPtrs(numBindings); |
| for (int ndx = 0; ndx < numBindings; ndx++) |
| srcPtrs[ndx] = bindings[ndx].pointer; |
| |
| DE_ASSERT(srcPtrs.size() == layout.attributes.size()); |
| if (!srcPtrs.empty()) |
| uploadBufferData(gl, m_object, usage, layout, &srcPtrs[0]); |
| |
| // Construct descriptor. |
| m_layout.buffer = m_object; |
| m_layout.attributes = layout.attributes; |
| } |
| |
| VertexBuffer::~VertexBuffer (void) |
| { |
| } |
| |
| // IndexBuffer |
| |
| IndexBuffer::IndexBuffer (const RenderContext& context, IndexType indexType, int numIndices, const void* indices) |
| : Buffer(context) |
| { |
| const glw::Functions& gl = context.getFunctions(); |
| const deUint32 usage = GL_STATIC_DRAW; |
| |
| gl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_object); |
| gl.bufferData(GL_ELEMENT_ARRAY_BUFFER, numIndices*getIndexSize(indexType), indices, usage); |
| gl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); |
| GLU_EXPECT_NO_ERROR(gl.getError(), "Uploading index data failed"); |
| } |
| |
| IndexBuffer::~IndexBuffer (void) |
| { |
| } |
| |
| static inline VertexAttributeDescriptor getUserPointerDescriptor (const VertexArrayBinding& vertexArray) |
| { |
| DE_ASSERT(vertexArray.binding.type == BindingPoint::BPTYPE_LOCATION); |
| |
| return VertexAttributeDescriptor(vertexArray.binding.location, |
| vertexArray.pointer.componentType, |
| vertexArray.pointer.convert, |
| vertexArray.pointer.numComponents, |
| vertexArray.pointer.numElements, |
| vertexArray.pointer.stride, |
| vertexArray.pointer.data); |
| } |
| |
| //! Setup VA according to allocation spec. Assumes that other state (VAO binding, buffer) is set already. |
| static void setVertexAttribPointer (const glw::Functions& gl, const VertexAttributeDescriptor& va) |
| { |
| const bool isIntType = de::inRange<int>(va.componentType, VTX_COMP_UNSIGNED_INT8, VTX_COMP_SIGNED_INT32); |
| const bool isSpecialType = de::inRange<int>(va.componentType, VTX_COMP_FIXED, VTX_COMP_FLOAT); |
| const deUint32 compTypeGL = getVtxCompGLType(va.componentType); |
| |
| DE_ASSERT(isIntType != isSpecialType); // Must be either int or special type. |
| DE_ASSERT(isIntType || va.convert == VTX_COMP_CONVERT_NONE); // Conversion allowed only for special types. |
| DE_UNREF(isSpecialType); |
| |
| gl.enableVertexAttribArray(va.location); |
| |
| if (isIntType && va.convert == VTX_COMP_CONVERT_NONE) |
| gl.vertexAttribIPointer(va.location, va.numComponents, compTypeGL, va.stride, va.pointer); |
| else |
| gl.vertexAttribPointer(va.location, va.numComponents, compTypeGL, va.convert == VTX_COMP_CONVERT_NORMALIZE_TO_FLOAT ? GL_TRUE : GL_FALSE, va.stride, va.pointer); |
| } |
| |
| //! Setup vertex buffer and attributes. |
| static void setVertexBufferAttributes (const glw::Functions& gl, const VertexBufferDescriptor& buffer) |
| { |
| gl.bindBuffer(GL_ARRAY_BUFFER, buffer.buffer); |
| |
| for (std::vector<VertexAttributeDescriptor>::const_iterator vaIter = buffer.attributes.begin(); vaIter != buffer.attributes.end(); ++vaIter) |
| setVertexAttribPointer(gl, *vaIter); |
| |
| gl.bindBuffer(GL_ARRAY_BUFFER, 0); |
| } |
| |
| static void disableVertexArrays (const glw::Functions& gl, const std::vector<VertexArrayBinding>& bindings) |
| { |
| for (std::vector<VertexArrayBinding>::const_iterator vaIter = bindings.begin(); vaIter != bindings.end(); ++vaIter) |
| { |
| DE_ASSERT(vaIter->binding.type == BindingPoint::BPTYPE_LOCATION); |
| gl.disableVertexAttribArray(vaIter->binding.location); |
| } |
| } |
| |
| #if defined(DE_DEBUG) |
| static bool isProgramActive (const RenderContext& context, deUint32 program) |
| { |
| // \todo [2013-05-08 pyry] Is this query broken? |
| /* deUint32 activeProgram = 0; |
| context.getFunctions().getIntegerv(GL_ACTIVE_PROGRAM, (int*)&activeProgram); |
| GLU_EXPECT_NO_ERROR(context.getFunctions().getError(), "oh"); |
| return activeProgram == program;*/ |
| DE_UNREF(context); |
| DE_UNREF(program); |
| return true; |
| } |
| |
| static bool isDrawCallValid (int numVertexArrays, const VertexArrayBinding* vertexArrays, const PrimitiveList& primitives) |
| { |
| if (numVertexArrays < 0) |
| return false; |
| |
| if ((primitives.indexType == INDEXTYPE_LAST) != (primitives.indices == 0)) |
| return false; |
| |
| if (primitives.numElements < 0) |
| return false; |
| |
| if (!primitives.indices) |
| { |
| for (int ndx = 0; ndx < numVertexArrays; ndx++) |
| { |
| if (primitives.numElements > vertexArrays[ndx].pointer.numElements) |
| return false; |
| } |
| } |
| // \todo [2013-05-08 pyry] We could walk whole index array and determine index range |
| |
| return true; |
| } |
| #endif // DE_DEBUG |
| |
| static inline void drawNonIndexed (const glw::Functions& gl, PrimitiveType type, int numElements) |
| { |
| deUint32 mode = getPrimitiveGLType(type); |
| gl.drawArrays(mode, 0, numElements); |
| } |
| |
| static inline void drawIndexed (const glw::Functions& gl, PrimitiveType type, int numElements, IndexType indexType, const void* indexPtr) |
| { |
| deUint32 mode = getPrimitiveGLType(type); |
| deUint32 indexGLType = getIndexGLType(indexType); |
| |
| gl.drawElements(mode, numElements, indexGLType, indexPtr); |
| } |
| |
| } // anonymous |
| |
| void drawFromUserPointers (const RenderContext& context, deUint32 program, int numVertexArrays, const VertexArrayBinding* vertexArrays, const PrimitiveList& primitives, DrawUtilCallback* callback) |
| { |
| const glw::Functions& gl = context.getFunctions(); |
| std::vector<VertexArrayBinding> bindingsWithLocations; |
| |
| DE_ASSERT(isDrawCallValid(numVertexArrays, vertexArrays, primitives)); |
| DE_ASSERT(isProgramActive(context, program)); |
| |
| // Lower bindings to locations. |
| namedBindingsToProgramLocations(gl, program, vertexArrays, vertexArrays+numVertexArrays, std::inserter(bindingsWithLocations, bindingsWithLocations.begin())); |
| |
| TCU_CHECK(areVertexArrayLocationsValid(bindingsWithLocations.begin(), bindingsWithLocations.end())); |
| |
| // Set VA state. |
| for (std::vector<VertexArrayBinding>::const_iterator vaIter = bindingsWithLocations.begin(); vaIter != bindingsWithLocations.end(); ++vaIter) |
| setVertexAttribPointer(gl, getUserPointerDescriptor(*vaIter)); |
| |
| if (callback) |
| callback->beforeDrawCall(); |
| |
| if (primitives.indices) |
| drawIndexed(gl, primitives.type, primitives.numElements, primitives.indexType, primitives.indices); |
| else |
| drawNonIndexed(gl, primitives.type, primitives.numElements); |
| |
| if (callback) |
| callback->afterDrawCall(); |
| |
| // Disable attribute arrays or otherwise someone later on might get crash thanks to invalid pointers. |
| disableVertexArrays(gl, bindingsWithLocations); |
| } |
| |
| void drawFromBuffers (const RenderContext& context, deUint32 program, int numVertexArrays, const VertexArrayBinding* vertexArrays, const PrimitiveList& primitives, DrawUtilCallback* callback) |
| { |
| const glw::Functions& gl = context.getFunctions(); |
| std::vector<VertexArrayBinding> bindingsWithLocations; |
| |
| DE_ASSERT(isDrawCallValid(numVertexArrays, vertexArrays, primitives)); |
| DE_ASSERT(isProgramActive(context, program)); |
| |
| // Lower bindings to locations. |
| namedBindingsToProgramLocations(gl, program, vertexArrays, vertexArrays+numVertexArrays, std::inserter(bindingsWithLocations, bindingsWithLocations.begin())); |
| |
| TCU_CHECK(areVertexArrayLocationsValid(bindingsWithLocations.begin(), bindingsWithLocations.end())); |
| |
| // Create buffers for duration of draw call. |
| { |
| VertexBuffer vertexBuffer (context, (int)bindingsWithLocations.size(), (bindingsWithLocations.empty()) ? (DE_NULL) : (&bindingsWithLocations[0])); |
| |
| // Set state. |
| setVertexBufferAttributes(gl, vertexBuffer.getDescriptor()); |
| |
| if (primitives.indices) |
| { |
| IndexBuffer indexBuffer(context, primitives.indexType, primitives.numElements, primitives.indices); |
| |
| gl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, *indexBuffer); |
| |
| if (callback) |
| callback->beforeDrawCall(); |
| |
| drawIndexed(gl, primitives.type, primitives.numElements, primitives.indexType, 0); |
| |
| if (callback) |
| callback->afterDrawCall(); |
| |
| gl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); |
| } |
| else |
| { |
| if (callback) |
| callback->beforeDrawCall(); |
| |
| drawNonIndexed(gl, primitives.type, primitives.numElements); |
| |
| if (callback) |
| callback->afterDrawCall(); |
| } |
| } |
| |
| // Disable attribute arrays or otherwise someone later on might get crash thanks to invalid pointers. |
| for (std::vector<VertexArrayBinding>::const_iterator vaIter = bindingsWithLocations.begin(); vaIter != bindingsWithLocations.end(); ++vaIter) |
| gl.disableVertexAttribArray(vaIter->binding.location); |
| } |
| |
| void drawFromVAOBuffers (const RenderContext& context, deUint32 program, int numVertexArrays, const VertexArrayBinding* vertexArrays, const PrimitiveList& primitives, DrawUtilCallback* callback) |
| { |
| const glw::Functions& gl = context.getFunctions(); |
| VertexArray vao (context); |
| |
| gl.bindVertexArray(*vao); |
| drawFromBuffers(context, program, numVertexArrays, vertexArrays, primitives, callback); |
| gl.bindVertexArray(0); |
| } |
| |
| void draw (const RenderContext& context, deUint32 program, int numVertexArrays, const VertexArrayBinding* vertexArrays, const PrimitiveList& primitives, DrawUtilCallback* callback) |
| { |
| const glu::ContextType ctxType = context.getType(); |
| |
| if (isContextTypeGLCore(ctxType) || contextSupports(ctxType, ApiType::es(3,1))) |
| drawFromVAOBuffers(context, program, numVertexArrays, vertexArrays, primitives, callback); |
| else |
| { |
| DE_ASSERT(isContextTypeES(ctxType)); |
| drawFromUserPointers(context, program, numVertexArrays, vertexArrays, primitives, callback); |
| } |
| } |
| |
| } // glu |