Load to VAO function
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &modelVertexVBO);
glGenBuffers(1, &sphereTransformVBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, modelVertexVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * (sphereModel->numVertices * 3), &(sphereModel->vertices[0]), GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), NULL);
glBindBuffer(GL_ARRAY_BUFFER, sphereTransformVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * (maxSphereStorage * 4 * 4), NULL, GL_STATIC_DRAW);
glVertexAttribPointer(1, 4 * 4, GL_FLOAT, GL_FALSE, 4 * 4 * sizeof(GLfloat), NULL);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glVertexAttribDivisor(sphereTransformVBO, 1);
glBindVertexArray(0);
Geometry drawing function:
glBindVertexArray(VAO);
glDrawArraysInstanced(sphereModel->mode, 0, sphereModel->numVertices, sphereCount);
When I try running this code it crashes with the following crash note:
Exception thrown at 0x0000000068F4EDB4 (nvoglv64.dll) in Engine.exe: 0xC0000005: Access violation reading location 0x0000000000000000.
When I remove the second VBO it works for some reason.
glVertexAttribPointer(0, 4 * 4, GL_FLOAT, GL_FALSE, 4 * 4 * sizeof(GLfloat), NULL);
Your crash is the result of a simple copy-and-paste bug. You use attribute 0 here, which means you never called glVertexAttribPointer for attribute 1. Therefore, it uses the default attribute state, thus leading to a crash.
However, I strongly suspect that you are attempting to pass a 4x4 matrix as a single attribute. That won't work; OpenGL will give you a GL_INVALID_VALUE error if you try to set the attribute's size to be more than 4.
Matrices are treated as arrays of (column) vectors. And each vector takes up a separate attribute index. So if you want to pass a matrix, you will have to use 4 attribute indices (starting with the one provided by your shader). And each one will have to have the divisor set for it as well.
Why are you remapping the Vertex Attribute pointer to your second vbo?
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), NULL);
...
glVertexAttribPointer(0, 4 * 4, GL_FLOAT, GL_FALSE, 4 * 4 * sizeof(GLfloat), NULL);
refer https://www.opengl.org/sdk/docs/man/html/glVertexAttribPointer.xhtml for more info on glVertexAttribPointer()
I am guessing your draw call
glDrawArraysInstanced(sphereModel->mode, 0, sphereModel->numVertices, sphereCount);
Note the '0' index in both cases
is exceeding the size of your second VBO and hence the error. Is this intentional that you want to remap your vertex attribute pointer to the second VBO? This overwrites the first mapping, in other words your first VBO is not being used.
I suggest using GLuint attribute1; to store the index and map accordingly to avoid such problems in future. Using numbers for attribute index directly like 0 is an easy way to make mistakes like this.
Related
Here is a sample code :-
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBindVertexArray(VAO);
...
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100,
&translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
... ... ...
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1); //<--- who own this setting?
//^ mostly copied from https://learnopengl.com/Advanced-OpenGL/Instancing
Who own the glVertexAttribDivisor setting? (VAO / instanceVBO / global state)
A comment in https://gamedev.stackexchange.com/questions/99236/what-state-is-stored-in-an-opengl-vertex-array-object-vao-and-how-do-i-use-the#comment174555_99238 suggests that it is stored in VBO.
However, the comment contradicts (?) to the above code which calls glVertexAttribDivisor(2, 1) after unbinds glBindBuffer(GL_ARRAY_BUFFER, 0); .
I would be appreciate if you are also kind to provide reference that I can read more about :
which settings/states owned by which one of Opengl's thingy (VAO/VBO/etc).
The vertex array divisor (VERTEX BINDING DIVISOR) is stored in the Vertex Array Objects state vector, separately for each vertex attribute (like enable state, offset, stride etc.).
The states which are stored in the VAO are listed in the specification in Table 23.3: Vertex Array Object State.
VAOs are specified in OpenGL 4.6 API Core Profile Specification - 10.3.1 Vertex Array Objects.
See also Vertex Specification - Instanced arrays
I write some vertex data inside a shader to SSBO. Then I want to use the data written to the SSBO as VBOs. These will be used for the next draw call. How can this be done?
Here is, how I do it now, but it still segfaults:
int new_vertex_count = …;
int new_index_count = …;
int* new_indices = …;
GLuint ssbo[3];
glGenBuffers(3, ssbo);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo[1]);
glBufferData(GL_SHADER_STORAGE_BUFFER, new_vertex_count * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo[1]);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo[2]);
glBufferData(GL_SHADER_STORAGE_BUFFER, new_vertex_count * 2 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, ssbo[2]);
glBindVertexArray(vao); //bind the original VAO
glPatchParameteri(GL_PATCH_VERTICES, 16);
glEnable(GL_RASTERIZER_DISCARD); //disable displaying
glDrawElements(GL_PATCHES, index_count, GL_UNSIGNED_INT, 0); //don't draw, just
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); //sync writing
glFinish();
glDisable(GL_RASTERIZER_DISCARD); //reanable displaying for next draw call
glBindVertexArray(0); //unbind original VAO in order to use new VBOs
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ssbo[0]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, index_count * sizeof(uint), indices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, ssbo[1]); //bind SSBO as VBO, is this even possible?
//or should I use new VBOs and copy? How would I copy then?
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, ssbo[2]);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glDrawElements(GL_PATCHES, new_index_count, new_indices, GL_UNSIGNED_INT, 0); //here the real drawing
There is no such thing as an "SSBO" or a "VBO". There are only buffer objects. Storage blocks and vertex arrays are uses for buffer objects, but a particular buffer is not inherently linked to a particular use. There's nothing stopping you from writing to a buffer through a storage block, then reading from it in a rendering operation.
So long as you follow the rules of incoherent memory access for those writes, of course. Writes through a storage block are not available for reading operations unless you explicitly make them available. You would use glMemoryBarrier for this. And the way a memory barrier works is that the enumerator specifies the operations you want to be able to see the results of whatever was written.
You want the written data to be read as vertex attribute arrays. So you use:
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);
I'm currently learning OpenGL in my free time and lately I have been facing an "error" I don't understand.
The thing is, I have no errors, only nothing appear on my screen. I'm using OpenGL with SFML.
Here is my code. Here is my method:
void CreateObjet(GLuint& vao, GLuint& vbo, GLuint& ebo, GLuint& textureLocation)
//I create my arrays here.. Don't worry they are fine.
CreateTexture(textureLocation);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glGenVertexArrays (1, &vao);
glBindVertexArray (vao); //On travaille dans le VAO
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData (GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), NULL);
glEnableVertexAttribArray(0);
glBufferData (GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
glVertexAttribPointer (1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBufferData (GL_ARRAY_BUFFER, sizeof(texCoords), texCoords, GL_STATIC_DRAW);
glVertexAttribPointer (2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6* sizeof(GLfloat)));
glEnableVertexAttribArray(2);
//EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indiceFinal), indiceFinal, GL_STATIC_DRAW);
glBindVertexArray(0);
I know my problem is not with my shaders because I receive no errors in my console with GlshaderRiv().
I would like to know if I'm doing the order properly.
I Create a VBO and a EBO
I Create a VAO
I bind the current VAO to modify it
I bind the current VBO inside the VAO
I bind my first array (Vertex Position vector3f) in my VBO and put them in the first pointer with the correct offset and stride.
I bind my second array (Color Position vector3f) in my VBO and put them in the first pointer with the correct offset and stride.
I bind my third array (Texture Position vector2f) in my VBO and put them in the first pointer with the correct offset and stride.
I bind a EBO within the VAO
I bind the EBO with my element position (Vector 3u).
I unbind the VAO from the memory because my drawing loop is quite later in the code and so, I don't want to use memory space for nothing. Don't worry, Before I draw I put glBindVertexArray(&vao);
That definitely does not look right. You're writing the values for all attributes to the same buffer, with each one overwriting the previous one:
glBufferData (GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), NULL);
glEnableVertexAttribArray(0);
glBufferData (GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
...
When you make the second glBufferData() call, it will overwrite the points data that you previously stored in the buffer with the colors data.
The misunderstanding is probably about what glVertexAttribPointer() does. It specifies which buffer the given attribute is sourced from, as well as the data layout (component count, type, etc). But it does not create a copy of the buffer data, or anything like that. The attribute data you want to use must still be stored in the buffer at the time of the draw call.
To fix this, you either have to use a different buffer for each attribute, or arrange the attribute data so that the values for all 3 attributes can be stored in the same buffer. The arguments of your glVertexAttribPointer() calls actually suggest that you were intending to store all attribute values interleaved in the same buffer. To get this working, you have to arrange the attribute values accordingly, and then store them in the buffer with a single glBufferData() call.
The memory arrangement you will need for this will have all the attribute values for the first vertex in sequence, followed by the values for the second vertex, etc. With pi the position of vertex i, ci the color, and ti the texture coordinates, the correct memory layout is:
p0x p0y p0z c0r c0g c0b t0s t0t
p1x p1y p1z c1r c1g c1b t1s t1t
p2x p2y p2z c2r c2g c2b t2s t2t
...
It seems like glBufferSubData is overwriting or somehow mangling data between my glDrawArrays calls. I'm working in Windows 7 64bit, with that latest drivers for my Nvidia GeForce GT520M CUDA 1GB.
I have 2 models, each with an animation. The models have 1 mesh, and that mesh is stored in the same VAO. They also have 1 animation each, and the bone transformations to be used for rendering the mesh is stored in the same VBO.
My workflow looks like this:
calculate bone transformation matrices for a model
load bone transformation matrices into opengl using glBufferSubData, then bind the buffer
render the models mesh using glDrawArrays
For one model, this works (at least, mostly - sometimes I get weird gaps in between the vertices).
However, for more than one model, it looks like bone transformation matrix data is getting mixed up between the rendering calls to the meshes.
Single Model Animated Windows
Two Models Animated Windows
I load my bone transformation data like so:
void Animation::bind()
{
glBindBuffer(GL_UNIFORM_BUFFER, bufferId_);
glBufferSubData(GL_UNIFORM_BUFFER, 0, currentTransforms_.size() * sizeof(glm::mat4), ¤tTransforms_[0]);
bindPoint_ = openGlDevice_->bindBuffer( bufferId_ );
}
And I render my mesh like so:
void Mesh::render()
{
glBindVertexArray(vaoId_);
glDrawArrays(GL_TRIANGLES, 0, vertices_.size());
glBindVertexArray(0);
}
If I add a call to glFinish() after my call to render(), it works just fine! This seems to indicate to me that, for some reason, the transformation matrix data for one animation is 'bleeding' over to the next animation.
How could this happen? I am under the impression that if I called glBufferSubData while that buffer was in use (i.e. for a glDrawArrays for example), then it would block. Is this not the case?
It might be worth mentioning that this same code works just fine in Linux.
Note: Related to a previous post, which I deleted.
Mesh Loading Code:
void Mesh::load()
{
LOG_DEBUG( "loading mesh '" + name_ +"' into video memory." );
// create our vao
glGenVertexArrays(1, &vaoId_);
glBindVertexArray(vaoId_);
// create our vbos
glGenBuffers(5, &vboIds_[0]);
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[0]);
glBufferData(GL_ARRAY_BUFFER, vertices_.size() * sizeof(glm::vec3), &vertices_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[1]);
glBufferData(GL_ARRAY_BUFFER, textureCoordinates_.size() * sizeof(glm::vec2), &textureCoordinates_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[2]);
glBufferData(GL_ARRAY_BUFFER, normals_.size() * sizeof(glm::vec3), &normals_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[3]);
glBufferData(GL_ARRAY_BUFFER, colors_.size() * sizeof(glm::vec4), &colors_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 0, 0);
if (bones_.size() == 0)
{
bones_.resize( vertices_.size() );
for (auto& b : bones_)
{
b.weights = glm::vec4(0.25f);
}
}
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[4]);
glBufferData(GL_ARRAY_BUFFER, bones_.size() * sizeof(VertexBoneData), &bones_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(4);
glVertexAttribIPointer(4, 4, GL_INT, sizeof(VertexBoneData), (const GLvoid*)0);
glEnableVertexAttribArray(5);
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(VertexBoneData), (const GLvoid*)(sizeof(glm::ivec4)));
glBindVertexArray(0);
}
Animation UBO Setup:
void Animation::setupAnimationUbo()
{
bufferId_ = openGlDevice_->createBufferObject(GL_UNIFORM_BUFFER, Constants::MAX_NUMBER_OF_BONES_PER_MESH * sizeof(glm::mat4), ¤tTransforms_[0]);
}
where Constants::MAX_NUMBER_OF_BONES_PER_MESH is set to 100.
In OpenGlDevice:
GLuint OpenGlDevice::createBufferObject(GLenum target, glmd::uint32 totalSize, const void* dataPointer)
{
GLuint bufferId = 0;
glGenBuffers(1, &bufferId);
glBindBuffer(target, bufferId);
glBufferData(target, totalSize, dataPointer, GL_DYNAMIC_DRAW);
glBindBuffer(target, 0);
bufferIds_.push_back(bufferId);
return bufferId;
}
Those usage flags are mostly correct for this scenario, though you might consider trying GL_STREAM_DRAW.
Your driver appears to be failing to implicitly synchronize for some reason, so you might want to try a technique that eliminates the need for synchronization in the first place. I would suggest Buffer Orphaning: call glBufferData (...) with NULL for the data pointer prior to sending data. This will allow commands that are currently using the UBO to continue using the original data store without forcing synchronization, since you will allocate a new data store before sending new data. When the earlier mentioned commands finish the original data store will be orphaned and the GL implementation will free it.
In newer OpenGL implementations you can use glInvalidateBuffer[Sub]Data (...) to hint the driver into doing what was discussed above. Likewise, you can use glMapBufferRange (...) with appropriate flags to control all of this behavior more explicitly. Unmapping will implicitly flush and synchronize access to a buffer object unless told otherwise, this might get your driver to do its job if you do not want to mess around with synchronization-free buffer update logic.
Most of what I mentioned is discussed in more detail here.
It seems like glBufferSubData is overwriting or somehow mangling data between my glDrawArrays calls. I'm working in Windows 7 64bit, with that latest drivers for my Nvidia GeForce GT520M CUDA 1GB.
I have 2 models, each with an animation. The models have 1 mesh, and that mesh is stored in the same VAO. They also have 1 animation each, and the bone transformations to be used for rendering the mesh is stored in the same VBO.
My workflow looks like this:
calculate bone transformation matrices for a model
load bone transformation matrices into opengl using glBufferSubData, then bind the buffer
render the models mesh using glDrawArrays
For one model, this works (at least, mostly - sometimes I get weird gaps in between the vertices).
However, for more than one model, it looks like bone transformation matrix data is getting mixed up between the rendering calls to the meshes.
Single Model Animated Windows
Two Models Animated Windows
I load my bone transformation data like so:
void Animation::bind()
{
glBindBuffer(GL_UNIFORM_BUFFER, bufferId_);
glBufferSubData(GL_UNIFORM_BUFFER, 0, currentTransforms_.size() * sizeof(glm::mat4), ¤tTransforms_[0]);
bindPoint_ = openGlDevice_->bindBuffer( bufferId_ );
}
And I render my mesh like so:
void Mesh::render()
{
glBindVertexArray(vaoId_);
glDrawArrays(GL_TRIANGLES, 0, vertices_.size());
glBindVertexArray(0);
}
If I add a call to glFinish() after my call to render(), it works just fine! This seems to indicate to me that, for some reason, the transformation matrix data for one animation is 'bleeding' over to the next animation.
How could this happen? I am under the impression that if I called glBufferSubData while that buffer was in use (i.e. for a glDrawArrays for example), then it would block. Is this not the case?
It might be worth mentioning that this same code works just fine in Linux.
Note: Related to a previous post, which I deleted.
Mesh Loading Code:
void Mesh::load()
{
LOG_DEBUG( "loading mesh '" + name_ +"' into video memory." );
// create our vao
glGenVertexArrays(1, &vaoId_);
glBindVertexArray(vaoId_);
// create our vbos
glGenBuffers(5, &vboIds_[0]);
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[0]);
glBufferData(GL_ARRAY_BUFFER, vertices_.size() * sizeof(glm::vec3), &vertices_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[1]);
glBufferData(GL_ARRAY_BUFFER, textureCoordinates_.size() * sizeof(glm::vec2), &textureCoordinates_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[2]);
glBufferData(GL_ARRAY_BUFFER, normals_.size() * sizeof(glm::vec3), &normals_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[3]);
glBufferData(GL_ARRAY_BUFFER, colors_.size() * sizeof(glm::vec4), &colors_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 0, 0);
if (bones_.size() == 0)
{
bones_.resize( vertices_.size() );
for (auto& b : bones_)
{
b.weights = glm::vec4(0.25f);
}
}
glBindBuffer(GL_ARRAY_BUFFER, vboIds_[4]);
glBufferData(GL_ARRAY_BUFFER, bones_.size() * sizeof(VertexBoneData), &bones_[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(4);
glVertexAttribIPointer(4, 4, GL_INT, sizeof(VertexBoneData), (const GLvoid*)0);
glEnableVertexAttribArray(5);
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(VertexBoneData), (const GLvoid*)(sizeof(glm::ivec4)));
glBindVertexArray(0);
}
Animation UBO Setup:
void Animation::setupAnimationUbo()
{
bufferId_ = openGlDevice_->createBufferObject(GL_UNIFORM_BUFFER, Constants::MAX_NUMBER_OF_BONES_PER_MESH * sizeof(glm::mat4), ¤tTransforms_[0]);
}
where Constants::MAX_NUMBER_OF_BONES_PER_MESH is set to 100.
In OpenGlDevice:
GLuint OpenGlDevice::createBufferObject(GLenum target, glmd::uint32 totalSize, const void* dataPointer)
{
GLuint bufferId = 0;
glGenBuffers(1, &bufferId);
glBindBuffer(target, bufferId);
glBufferData(target, totalSize, dataPointer, GL_DYNAMIC_DRAW);
glBindBuffer(target, 0);
bufferIds_.push_back(bufferId);
return bufferId;
}
Those usage flags are mostly correct for this scenario, though you might consider trying GL_STREAM_DRAW.
Your driver appears to be failing to implicitly synchronize for some reason, so you might want to try a technique that eliminates the need for synchronization in the first place. I would suggest Buffer Orphaning: call glBufferData (...) with NULL for the data pointer prior to sending data. This will allow commands that are currently using the UBO to continue using the original data store without forcing synchronization, since you will allocate a new data store before sending new data. When the earlier mentioned commands finish the original data store will be orphaned and the GL implementation will free it.
In newer OpenGL implementations you can use glInvalidateBuffer[Sub]Data (...) to hint the driver into doing what was discussed above. Likewise, you can use glMapBufferRange (...) with appropriate flags to control all of this behavior more explicitly. Unmapping will implicitly flush and synchronize access to a buffer object unless told otherwise, this might get your driver to do its job if you do not want to mess around with synchronization-free buffer update logic.
Most of what I mentioned is discussed in more detail here.