diff --git a/renderdoc/driver/gl/gl_driver.h b/renderdoc/driver/gl/gl_driver.h index a8054d44f..ed8e4b0cd 100644 --- a/renderdoc/driver/gl/gl_driver.h +++ b/renderdoc/driver/gl/gl_driver.h @@ -155,6 +155,24 @@ class WrappedOpenGL set m_HighTrafficResources; + // we store two separate sets of maps, since for an explicit glMemoryBarrier + // we need to flush both types of maps, but for implicit sync points we only + // want to consider coherent maps, and since that happens often we want it to + // be as efficient as possible. + set m_CoherentMaps; + set m_PersistentMaps; + + // this function iterates over all the maps, checking for any changes between + // the shadow pointers, and propogates that to 'real' GL + void PersistentMapMemoryBarrier(const set &maps); + + // this function is called at any point that could possibly pick up a change + // in a coherent persistent mapped buffer, to propogate changes across. In most + // cases hopefully m_CoherentMaps will be empty so this will amount to an inlined + // check and jump + inline void CoherentMapImplicitBarrier() + { if(!m_CoherentMaps.empty()) PersistentMapMemoryBarrier(m_CoherentMaps); } + vector m_FrameRecord; const FetchDrawcall *GetDrawcall(const FetchDrawcall *draw, uint32_t eventID); @@ -1140,6 +1158,8 @@ class WrappedOpenGL void Common_glTextureParameterIivEXT(GLResourceRecord *record, GLenum target, GLenum pname, const GLint *params); void Common_glTextureParameterIuivEXT(GLResourceRecord *record, GLenum target, GLenum pname, const GLuint *params); + void Common_glNamedBufferStorageEXT(ResourceId id, GLsizeiptr size, const void *data, GLbitfield flags); + IMPLEMENT_FUNCTION_SERIALISED(GLenum, glCheckNamedFramebufferStatusEXT(GLuint framebuffer, GLenum target)); IMPLEMENT_FUNCTION_SERIALISED(void, glCompressedTextureImage1DEXT(GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void *bits)); IMPLEMENT_FUNCTION_SERIALISED(void, glCompressedTextureImage2DEXT(GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void *bits)); diff --git a/renderdoc/driver/gl/gl_resources.h b/renderdoc/driver/gl/gl_resources.h index 7302f45ca..6542be6b4 100644 --- a/renderdoc/driver/gl/gl_resources.h +++ b/renderdoc/driver/gl/gl_resources.h @@ -138,6 +138,7 @@ struct GLResourceRecord : public ResourceRecord usage(eGL_NONE) { RDCEraseEl(ShadowPtr); + RDCEraseEl(Map); } ~GLResourceRecord() @@ -161,6 +162,9 @@ struct GLResourceRecord : public ResourceRecord MapStatus status; bool invalidate; byte *ptr; + + byte *persistentPtr; + int64_t persistentMaps; // counter indicating how many coherent maps are 'live' } Map; void VerifyDataType(GLenum target) diff --git a/renderdoc/driver/gl/wrappers/gl_buffer_funcs.cpp b/renderdoc/driver/gl/wrappers/gl_buffer_funcs.cpp index 2c5817b72..83715bc9a 100644 --- a/renderdoc/driver/gl/wrappers/gl_buffer_funcs.cpp +++ b/renderdoc/driver/gl/wrappers/gl_buffer_funcs.cpp @@ -343,17 +343,15 @@ bool WrappedOpenGL::Serialise_glNamedBufferStorageEXT(GLuint buffer, GLsizeiptr return true; } -void WrappedOpenGL::glNamedBufferStorageEXT(GLuint buffer, GLsizeiptr size, const void *data, GLbitfield flags) +void WrappedOpenGL::Common_glNamedBufferStorageEXT(ResourceId id, GLsizeiptr size, const void *data, GLbitfield flags) { - m_Real.glNamedBufferStorageEXT(buffer, size, data, flags); - if(m_State >= WRITING) { - GLResourceRecord *record = GetResourceManager()->GetResourceRecord(BufferRes(GetCtx(), buffer)); + GLResourceRecord *record = GetResourceManager()->GetResourceRecord(id); RDCASSERT(record); SCOPED_SERIALISE_CONTEXT(BUFFERSTORAGE); - Serialise_glNamedBufferStorageEXT(buffer, size, data, flags); + Serialise_glNamedBufferStorageEXT(record->Resource.name, size, data, flags); // for satisfying GL_MIN_MAP_BUFFER_ALIGNMENT scope.SetAlignment(64); @@ -365,13 +363,35 @@ void WrappedOpenGL::glNamedBufferStorageEXT(GLuint buffer, GLsizeiptr size, cons record->SetDataPtr(chunk->GetData()); record->Length = (int32_t)size; } + + // We immediately map the whole range with appropriate flags, to be copied into whenever we + // need to propogate changes. Note: Coherent buffers are not mapped coherent, but this is + // because the user code isn't writing into them anyway and we're inserting invisible sync + // points - so there's no need for it to be coherently mapped (and there's no requirement + // that a buffer declared as coherent must ALWAYS be mapped as coherent). + if(flags & GL_MAP_PERSISTENT_BIT) + { + record->Map.persistentPtr = (byte *)m_Real.glMapNamedBufferRangeEXT(record->Resource.name, 0, size, + GL_MAP_WRITE_BIT|GL_MAP_FLUSH_EXPLICIT_BIT|GL_MAP_PERSISTENT_BIT); + RDCASSERT(record->Map.persistentPtr); + + // persistent maps always need both sets of shadow storage, so allocate up front. + record->AllocShadowStorage(size, 64); + } } else { - m_Buffers[GetResourceManager()->GetID(BufferRes(GetCtx(), buffer))].size = size; + m_Buffers[id].size = size; } } +void WrappedOpenGL::glNamedBufferStorageEXT(GLuint buffer, GLsizeiptr size, const void *data, GLbitfield flags) +{ + m_Real.glNamedBufferStorageEXT(buffer, size, data, flags); + + Common_glNamedBufferStorageEXT(GetResourceManager()->GetID(BufferRes(GetCtx(), buffer)), size, data, flags); +} + void WrappedOpenGL::glNamedBufferStorage(GLuint buffer, GLsizei size, const void *data, GLbitfield flags) { // only difference to EXT function is size parameter, so just upcast @@ -382,32 +402,10 @@ void WrappedOpenGL::glBufferStorage(GLenum target, GLsizeiptr size, const void * { m_Real.glBufferStorage(target, size, data, flags); - size_t idx = BufferIdx(target); - if(m_State >= WRITING) - { - GLResourceRecord *record = GetCtxData().m_BufferRecord[BufferIdx(target)]; - RDCASSERT(record); - - SCOPED_SERIALISE_CONTEXT(BUFFERSTORAGE); - Serialise_glNamedBufferStorageEXT(record->Resource.name, - size, data, flags); - - // for satisfying GL_MIN_MAP_BUFFER_ALIGNMENT - scope.SetAlignment(64); - - Chunk *chunk = scope.Get(); - - { - record->AddChunk(chunk); - record->SetDataPtr(chunk->GetData()); - record->Length = (int32_t)size; - } - } + Common_glNamedBufferStorageEXT(GetCtxData().m_BufferRecord[BufferIdx(target)]->GetResourceID(), size, data, flags); else - { RDCERR("Internal buffers should be allocated via dsa interfaces"); - } } bool WrappedOpenGL::Serialise_glNamedBufferDataEXT(GLuint buffer, GLsizeiptr size, const void *data, GLenum usage) @@ -1485,14 +1483,8 @@ void WrappedOpenGL::glInvalidateBufferSubData(GLuint buffer, GLintptr offset, GL * bit is passed to glMemoryBarrier, we manually call into glFlushMappedNamedBufferRangeEXT() with the * appropriate parameters and handling is otherwise identical. * - * The final piece of the puzzle is coherent mapped buffers. Since coherent maps have a cost (there may - * only be a limited amount of memory available to act as coherent mapped memory, in addition to it - * having a performance cost) we don't do the same as above where we map immediately on creation. - * Instead we perform real maps in tandem with user maps, but otherwise behave the same as above - the - * pointer returned to the user is a pointer to our backing store, and we keep the pointer to the 'real' - * mapped memory internal and perform the copies as above. Note we map the whole buffer even if user - * code only maps a range, to simplify the case where a second map comes along for a non-overlapping - * range (the pointer can be reused). + * The final piece of the puzzle is coherent mapped buffers. Since we must break the coherency carefully + * (see below), we map coherent buffers as non-coherent at creation time, the same as above. * * To satisfy the demands of being coherent, we need to transparently propogate any changes between the * user written data and the 'real' memory, without any call to intercept - there would be no need to @@ -1502,6 +1494,9 @@ void WrappedOpenGL::glInvalidateBufferSubData(GLuint buffer, GLintptr offset, GL * call that could depend on the results of the buffer. We then check if any write/change has happened * by comparing to the shadow storage, and if so we perform a manual flush of that changed region and * update the shadow storage for next time. + * + * This "fake coherency" is the reason we can map the buffer as non-coherent, since we will be performing + * copies and flushes manually to emulate the coherency to allow our interception in the middle. * * By definition, there will be *many* of these places where the buffer results could be used, not least * any buffer copy, any texture copy (since a texture buffer could be created), any draw or dispatch, @@ -1519,6 +1514,16 @@ void WrappedOpenGL::glInvalidateBufferSubData(GLuint buffer, GLintptr offset, GL * performing a read-back at every sync point to find every change. Which by itself may also hide race * conditions anyway. * + * + * Implementation notes: + * + * The record->Map.ptr is the *offsetted* pointer, ie. a pointer to the beginning of the mapped region, + * at record->Map.offset bytes from the start of the buffer. + * + * record->Map.persistentPtr points to the *base* of the buffer, not offsetted by any current map. + * + * Likewise the shadow storage pointers point to the base of a buffer-sized allocation each. + * ************************************************************************/ void *WrappedOpenGL::glMapNamedBufferRangeEXT(GLuint buffer, GLintptr offset, GLsizeiptr length, GLbitfield access) @@ -1543,8 +1548,14 @@ void *WrappedOpenGL::glMapNamedBufferRangeEXT(GLuint buffer, GLintptr offset, GL // if this map is writing and doesn't invalidate, or is flush explicit, map directly if(!directMap && (!invalidateMap || flushExplicitMap) && (access & GL_MAP_WRITE_BIT) && m_State != WRITING_CAPFRAME) - { directMap = true; + + // persistent maps must ALWAYS be intercepted + if(access & GL_MAP_PERSISTENT_BIT) + directMap = false; + + if(directMap) + { m_HighTrafficResources.insert(record->GetResourceID()); GetResourceManager()->MarkDirtyResource(record->GetResourceID()); } @@ -1553,9 +1564,15 @@ void *WrappedOpenGL::glMapNamedBufferRangeEXT(GLuint buffer, GLintptr offset, GL record->Map.length = length; record->Map.access = access; record->Map.invalidate = invalidateMap; - - if((access & (GL_MAP_COHERENT_BIT|GL_MAP_PERSISTENT_BIT)) != 0) - RDCUNIMPLEMENTED("haven't implemented persistant glMap calls"); + + // store a list of all persistent maps, and subset of all coherent maps + if(access & GL_MAP_PERSISTENT_BIT) + { + Atomic::Inc64(&record->Map.persistentMaps); + m_PersistentMaps.insert(record); + if(record->Map.access & GL_MAP_COHERENT_BIT) + m_CoherentMaps.insert(record); + } // if we're doing a direct map, pass onto GL and return if(directMap) @@ -1571,12 +1588,16 @@ void *WrappedOpenGL::glMapNamedBufferRangeEXT(GLuint buffer, GLintptr offset, GL { byte *ptr = record->GetDataPtr(); + if(record->Map.persistentPtr) + ptr = record->GetShadowPtr(0); + RDCASSERT(ptr); ptr += offset; m_Real.glGetNamedBufferSubDataEXT(buffer, offset, length, ptr); - + + record->Map.ptr = ptr; record->Map.status = GLResourceRecord::Mapped_Read; return ptr; @@ -1587,14 +1608,25 @@ void *WrappedOpenGL::glMapNamedBufferRangeEXT(GLuint buffer, GLintptr offset, GL RDCASSERT(ptr); { - // flush explicit maps are handled particularly: - // if we're idle, we just return the backing pointer and treat it no differently to a normal write map, - // as modified-but-unflushed ranges are "undefined", so we can easily just let them - // be modified. - // if we're capframing, we won't create a normal unmap chunk that copies over all the data with a - // given diff range. Instead we'll create a flush chunk for every glFlushMappedBufferRange. + // persistent maps get particular handling + if(access & GL_MAP_PERSISTENT_BIT) + { + // persistent pointers are always into the shadow storage, this way we can use the backing + // store for 'initial' buffer contents as with any other buffer. We also need to keep a + // comparison & modified buffer in case the application calls glMemoryBarrier(..) at any + // time. - if(m_State == WRITING_CAPFRAME) + // if we're invalidating, mark the whole range as 0xcc + if(invalidateMap) + { + memset(record->GetShadowPtr(0)+offset, 0xcc, length); + memset(record->GetShadowPtr(1)+offset, 0xcc, length); + } + + record->Map.ptr = ptr = record->GetShadowPtr(0)+offset; + record->Map.status = GLResourceRecord::Mapped_Write; + } + else if(m_State == WRITING_CAPFRAME) { byte *shadow = (byte *)record->GetShadowPtr(0); @@ -1637,7 +1669,7 @@ void *WrappedOpenGL::glMapNamedBufferRangeEXT(GLuint buffer, GLintptr offset, GL record->Map.ptr = ptr = shadow; record->Map.status = GLResourceRecord::Mapped_Write; } - else // if(m_State == WRITING_IDLE) + else if(m_State == WRITING_IDLE) { // return buffer backing store pointer, offsetted ptr += offset; @@ -1800,9 +1832,21 @@ bool WrappedOpenGL::Serialise_glUnmapNamedBufferEXT(GLuint buffer) if(DiffEnd > DiffStart) { - void *ptr = m_Real.glMapNamedBufferRangeEXT(buffer, (GLintptr)(offs+DiffStart), GLsizeiptr(DiffEnd-DiffStart), GL_MAP_WRITE_BIT); - memcpy(ptr, data, size_t(DiffEnd-DiffStart)); - m_Real.glUnmapNamedBufferEXT(buffer); + if(record && record->Map.persistentPtr) + { + // if we have a persistent mapped pointer, copy the range into the 'real' memory and + // do a flush. Note the persistent pointer is always to the base of the buffer so we + // need to account for the offset + + memcpy(record->Map.persistentPtr+offs+DiffStart, record->Map.ptr+DiffStart, DiffEnd-DiffStart); + m_Real.glFlushMappedNamedBufferRangeEXT(buffer, GLintptr(offs+DiffStart), DiffEnd-DiffStart); + } + else + { + void *ptr = m_Real.glMapNamedBufferRangeEXT(buffer, (GLintptr)(offs+DiffStart), GLsizeiptr(DiffEnd-DiffStart), GL_MAP_WRITE_BIT); + memcpy(ptr, data, size_t(DiffEnd-DiffStart)); + m_Real.glUnmapNamedBufferEXT(buffer); + } } if(m_State < WRITING) @@ -1838,33 +1882,60 @@ GLboolean WrappedOpenGL::glUnmapNamedBufferEXT(GLuint buffer) break; case GLResourceRecord::Mapped_Write: { - if(m_State == WRITING_CAPFRAME) + if(record->Map.access & GL_MAP_FLUSH_EXPLICIT_BIT) { - if(record->Map.access & GL_MAP_FLUSH_EXPLICIT_BIT) + // do nothing, any flushes that happened were handled, + // and we won't do any other updates here or make a chunk. + } + else if(m_State == WRITING_CAPFRAME) + { + SCOPED_SERIALISE_CONTEXT(UNMAP); + Serialise_glUnmapNamedBufferEXT(buffer); + m_ContextRecord->AddChunk(scope.Get()); + } + else if(m_State == WRITING_IDLE) + { + if(record->Map.persistentPtr) { - // do nothing, any flushes that happened were handled, - // and we won't do any other updates here or make a chunk. + // if we have a persistent mapped pointer, copy the range into the 'real' memory and + // do a flush. Note the persistent pointer is always to the base of the buffer so we + // need to account for the offset + + memcpy(record->Map.persistentPtr+record->Map.offset, record->Map.ptr, record->Map.length); + m_Real.glFlushMappedNamedBufferRangeEXT(buffer, record->Map.offset, record->Map.length); + + // update shadow storage + memcpy(record->GetShadowPtr(1)+record->Map.offset, record->Map.ptr, record->Map.length); + + GetResourceManager()->MarkDirtyResource(record->GetResourceID()); } else { - SCOPED_SERIALISE_CONTEXT(UNMAP); - Serialise_glUnmapNamedBufferEXT(buffer); - m_ContextRecord->AddChunk(scope.Get()); + // if we are here for WRITING_IDLE, the app wrote directly into our backing + // store memory. Just need to copy the data across to GL, no other work needed + void *ptr = m_Real.glMapNamedBufferRangeEXT(buffer, (GLintptr)record->Map.offset, GLsizeiptr(record->Map.length), GL_MAP_WRITE_BIT); + memcpy(ptr, record->Map.ptr, record->Map.length); + m_Real.glUnmapNamedBufferEXT(buffer); } } - else - { - // if we are here for WRITING_IDLE, the app wrote directly into our backing - // store memory. Just need to copy the data across to GL, no other work needed - void *ptr = m_Real.glMapNamedBufferRangeEXT(buffer, (GLintptr)record->Map.offset, GLsizeiptr(record->Map.length), GL_MAP_WRITE_BIT); - memcpy(ptr, record->Map.ptr, record->Map.length); - m_Real.glUnmapNamedBufferEXT(buffer); - } break; } } + // keep list of persistent & coherent maps up to date if we've + // made the last unmap to a buffer + if(record->Map.access & GL_MAP_PERSISTENT_BIT) + { + int64_t ref = Atomic::Dec64(&record->Map.persistentMaps); + if(ref == 0) + { + m_PersistentMaps.erase(record); + if(record->Map.access & GL_MAP_COHERENT_BIT) + m_CoherentMaps.erase(record); + } + } + record->Map.status = GLResourceRecord::Unmapped; return ret; @@ -1920,11 +1991,23 @@ bool WrappedOpenGL::Serialise_glFlushMappedNamedBufferRangeEXT(GLuint buffer, GL res = GetResourceManager()->GetLiveResource(ID); else res = GetResourceManager()->GetCurrentResource(ID); + + if(record && record->Map.persistentPtr) + { + // if we have a persistent mapped pointer, copy the range into the 'real' memory and + // do a flush. Note the persistent pointer is always to the base of the buffer so we + // need to account for the offset - // perform a map of the range and copy the data, to emulate the modified region being flushed - void *ptr = m_Real.glMapNamedBufferRangeEXT(res.name, (GLintptr)offs, (GLsizeiptr)len, GL_MAP_WRITE_BIT); - memcpy(ptr, data, (size_t)len); - m_Real.glUnmapNamedBufferEXT(res.name); + memcpy(record->Map.persistentPtr+offs, record->Map.ptr - record->Map.offset + offs, (size_t)len); + m_Real.glFlushMappedNamedBufferRangeEXT(buffer, (GLintptr)offs, (GLsizeiptr)len); + } + else + { + // perform a map of the range and copy the data, to emulate the modified region being flushed + void *ptr = m_Real.glMapNamedBufferRangeEXT(res.name, (GLintptr)offs, (GLsizeiptr)len, GL_MAP_WRITE_BIT); + memcpy(ptr, data, (size_t)len); + m_Real.glUnmapNamedBufferEXT(res.name); + } if(m_State < WRITING) SAFE_DELETE_ARRAY(data); @@ -1991,6 +2074,18 @@ void WrappedOpenGL::glFlushMappedNamedBufferRangeEXT(GLuint buffer, GLintptr off // other statuses is GLResourceRecord::Mapped_Read } } + else if(m_State == WRITING_IDLE) + { + // if this is a flush of a persistent map, we need to copy through to + // the real pointer and perform a real flush. + if(record && record->Map.persistentPtr) + { + memcpy(record->Map.persistentPtr+offset, record->Map.ptr - record->Map.offset + offset, length); + m_Real.glFlushMappedNamedBufferRangeEXT(buffer, offset, length); + + GetResourceManager()->MarkDirtyResource(record->GetResourceID()); + } + } } void WrappedOpenGL::glFlushMappedNamedBufferRange(GLuint buffer, GLintptr offset, GLsizei length) @@ -2015,6 +2110,32 @@ void WrappedOpenGL::glFlushMappedBufferRange(GLenum target, GLintptr offset, GLs return m_Real.glFlushMappedBufferRange(target, offset, length); } +void WrappedOpenGL::PersistentMapMemoryBarrier(const set &maps) +{ + // this function iterates over all the maps, checking for any changes between + // the shadow pointers, and propogates that to 'real' GL + + for(set::const_iterator it = maps.begin(); it != maps.end(); ++it) + { + GLResourceRecord *record = *it; + + RDCASSERT(record && record->Map.persistentPtr); + + size_t diffStart = 0, diffEnd = 0; + bool found = FindDiffRange(record->GetShadowPtr(0), record->GetShadowPtr(1), (size_t)record->Length, diffStart, diffEnd); + if(found) + { + // update the modified region in the 'comparison' shadow buffer for next check + memcpy(record->GetShadowPtr(1) + diffStart, record->GetShadowPtr(0) + diffStart, diffEnd - diffStart); + + // we use our own flush function so it will serialise chunks when necessary, and it + // also handles copying into the persistent mapped pointer and flushing the real GL + // buffer + glFlushMappedNamedBufferRangeEXT(record->Resource.name, GLintptr(diffStart), GLsizeiptr(diffEnd - diffStart)); + } + } +} + #pragma endregion #pragma region Transform Feedback @@ -3670,6 +3791,17 @@ void WrappedOpenGL::glDeleteBuffers(GLsizei n, const GLuint *buffers) GLResource res = BufferRes(GetCtx(), buffers[i]); if(GetResourceManager()->HasCurrentResource(res)) { + GLResourceRecord *record = GetResourceManager()->GetResourceRecord(res); + if(record) + { + // if we have a persistent pointer, make sure to unmap it + if(record->Map.persistentPtr) + m_Real.glUnmapNamedBufferEXT(res.name); + + // free any shadow storage + record->FreeShadowStorage(); + } + GetResourceManager()->MarkCleanResource(res); if(GetResourceManager()->HasResourceRecord(res)) GetResourceManager()->GetResourceRecord(res)->Delete(GetResourceManager()); diff --git a/renderdoc/driver/gl/wrappers/gl_draw_funcs.cpp b/renderdoc/driver/gl/wrappers/gl_draw_funcs.cpp index 1a4c531ea..70407578e 100644 --- a/renderdoc/driver/gl/wrappers/gl_draw_funcs.cpp +++ b/renderdoc/driver/gl/wrappers/gl_draw_funcs.cpp @@ -185,6 +185,20 @@ bool WrappedOpenGL::Serialise_glMemoryBarrier(GLbitfield barriers) void WrappedOpenGL::glMemoryBarrier(GLbitfield barriers) { + if(barriers & GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT) + { + barriers &= ~GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT; + + // perform a forced flush of all persistent mapped buffers, + // coherent or not. + PersistentMapMemoryBarrier(m_PersistentMaps); + } + + // if it was only GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT and that's been specially + // handled then just return. Otherwise, handle the rest + if(barriers == 0) + return; + m_Real.glMemoryBarrier(barriers); if(m_State == WRITING_CAPFRAME) @@ -210,6 +224,20 @@ bool WrappedOpenGL::Serialise_glMemoryBarrierByRegion(GLbitfield barriers) void WrappedOpenGL::glMemoryBarrierByRegion(GLbitfield barriers) { + if(barriers & GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT) + { + barriers &= ~GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT; + + // perform a forced flush of all persistent mapped buffers, + // coherent or not. + PersistentMapMemoryBarrier(m_PersistentMaps); + } + + // if it was only GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT and that's been specially + // handled then just return. Otherwise, handle the rest + if(barriers == 0) + return; + m_Real.glMemoryBarrierByRegion(barriers); if(m_State == WRITING_CAPFRAME)