Bug 1305190 - Fix vertex attrib elem size calculations. - r=ethlin
MozReview-Commit-ID: KXpLbIu8pRr
--- a/dom/canvas/WebGL2ContextVertices.cpp
+++ b/dom/canvas/WebGL2ContextVertices.cpp
@@ -75,27 +75,24 @@ WebGL2Context::VertexAttribIPointer(GLui
return;
}
MOZ_ASSERT(mBoundVertexArray);
mBoundVertexArray->EnsureAttrib(index);
InvalidateBufferFetching();
- WebGLVertexAttribData& vd = mBoundVertexArray->mAttribs[index];
- vd.buf = mBoundArrayBuffer;
- vd.stride = stride;
- vd.size = size;
- vd.byteOffset = offset;
- vd.type = type;
- vd.normalized = false;
- vd.integer = true;
-
MakeContextCurrent();
gl->fVertexAttribIPointer(index, size, type, stride, reinterpret_cast<void*>(offset));
+
+ WebGLVertexAttribData& vd = mBoundVertexArray->mAttribs[index];
+ const bool integerFunc = true;
+ const bool normalized = false;
+ vd.VertexAttribPointer(true, mBoundArrayBuffer, size, type, normalized, stride,
+ offset);
}
void
WebGL2Context::VertexAttribI4i(GLuint index, GLint x, GLint y, GLint z, GLint w)
{
if (IsContextLost())
return;
--- a/dom/canvas/WebGLContextBuffers.cpp
+++ b/dom/canvas/WebGLContextBuffers.cpp
@@ -519,17 +519,17 @@ WebGLContext::DeleteBuffer(WebGLBuffer*
for (auto& binding : mIndexedUniformBufferBindings) {
fnClearIfBuffer(binding.mBufferBinding);
}
}
for (int32_t i = 0; i < mGLMaxVertexAttribs; i++) {
if (mBoundVertexArray->HasAttrib(i)) {
- fnClearIfBuffer(mBoundVertexArray->mAttribs[i].buf);
+ fnClearIfBuffer(mBoundVertexArray->mAttribs[i].mBuf);
}
}
////
buffer->RequestDelete();
}
--- a/dom/canvas/WebGLContextDraw.cpp
+++ b/dom/canvas/WebGLContextDraw.cpp
@@ -813,20 +813,20 @@ WebGLContext::ValidateBufferFetching(con
uint32_t maxVertices = UINT32_MAX;
uint32_t maxInstances = UINT32_MAX;
const uint32_t attribCount = mBoundVertexArray->mAttribs.Length();
uint32_t i = 0;
for (const auto& vd : mBoundVertexArray->mAttribs) {
// If the attrib array isn't enabled, there's nothing to check;
// it's a static value.
- if (!vd.enabled)
+ if (!vd.mEnabled)
continue;
- if (vd.buf == nullptr) {
+ if (!vd.mBuf) {
ErrorInvalidOperation("%s: no VBO bound to enabled vertex attrib index %du!",
info, i);
return false;
}
++i;
}
@@ -838,62 +838,49 @@ WebGLContext::ValidateBufferFetching(con
if (attribLoc >= attribCount)
continue;
if (attribLoc == 0) {
mBufferFetch_IsAttrib0Active = true;
}
const auto& vd = mBoundVertexArray->mAttribs[attribLoc];
- if (!vd.enabled)
+ if (!vd.mEnabled)
continue;
- // the base offset
- CheckedUint32 checked_byteLength = CheckedUint32(vd.buf->ByteLength()) - vd.byteOffset;
- CheckedUint32 checked_sizeOfLastElement = CheckedUint32(vd.componentSize()) * vd.size;
-
- if (!checked_byteLength.isValid() ||
- !checked_sizeOfLastElement.isValid())
- {
- ErrorInvalidOperation("%s: Integer overflow occured while checking vertex"
- " attrib %u.",
- info, attribLoc);
- return false;
- }
-
- if (checked_byteLength.value() < checked_sizeOfLastElement.value()) {
+ const auto& bufByteLen = vd.mBuf->ByteLength();
+ if (vd.ByteOffset() > bufByteLen) {
maxVertices = 0;
maxInstances = 0;
break;
}
- CheckedUint32 checked_maxAllowedCount = ((checked_byteLength - checked_sizeOfLastElement) / vd.actualStride()) + 1;
+ size_t availBytes = bufByteLen - vd.ByteOffset();
+ if (vd.BytesPerVertex() > availBytes) {
+ maxVertices = 0;
+ maxInstances = 0;
+ break;
+ }
+ availBytes -= vd.BytesPerVertex();
+ const size_t vertCapacity = 1 + availBytes / vd.ExplicitStride();
- if (!checked_maxAllowedCount.isValid()) {
- ErrorInvalidOperation("%s: Integer overflow occured while checking vertex"
- " attrib %u.",
- info, attribLoc);
- return false;
- }
-
- if (vd.divisor == 0) {
- maxVertices = std::min(maxVertices, checked_maxAllowedCount.value());
+ if (vd.mDivisor == 0) {
+ if (vertCapacity < maxVertices) {
+ maxVertices = vertCapacity;
+ }
hasPerVertex = true;
} else {
- CheckedUint32 checked_curMaxInstances = checked_maxAllowedCount * vd.divisor;
-
- uint32_t curMaxInstances = UINT32_MAX;
- // If this isn't valid, it's because we overflowed our
- // uint32 above. Just leave this as UINT32_MAX, since
- // sizeof(uint32) becomes our limiting factor.
- if (checked_curMaxInstances.isValid()) {
- curMaxInstances = checked_curMaxInstances.value();
+ const auto curMaxInstances = CheckedInt<size_t>(vertCapacity) * vd.mDivisor;
+ // If this isn't valid, it's because we overflowed, which means we can support
+ // *too much*. Don't update maxInstances in this case.
+ if (curMaxInstances.isValid() &&
+ curMaxInstances.value() < maxInstances)
+ {
+ maxInstances = curMaxInstances.value();
}
-
- maxInstances = std::min(maxInstances, curMaxInstances);
}
}
mBufferFetchingIsVerified = true;
mBufferFetchingHasPerVertex = hasPerVertex;
mMaxFetchedVertices = maxVertices;
mMaxFetchedInstances = maxInstances;
@@ -1020,33 +1007,20 @@ WebGLContext::DoFakeVertexAttrib0(GLuint
void
WebGLContext::UndoFakeVertexAttrib0()
{
WebGLVertexAttrib0Status whatDoesAttrib0Need = WhatDoesVertexAttrib0Need();
if (MOZ_LIKELY(whatDoesAttrib0Need == WebGLVertexAttrib0Status::Default))
return;
- if (mBoundVertexArray->HasAttrib(0) && mBoundVertexArray->mAttribs[0].buf) {
+ if (mBoundVertexArray->HasAttrib(0) && mBoundVertexArray->mAttribs[0].mBuf) {
const WebGLVertexAttribData& attrib0 = mBoundVertexArray->mAttribs[0];
- gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, attrib0.buf->mGLName);
- if (attrib0.integer) {
- gl->fVertexAttribIPointer(0,
- attrib0.size,
- attrib0.type,
- attrib0.stride,
- reinterpret_cast<const GLvoid*>(attrib0.byteOffset));
- } else {
- gl->fVertexAttribPointer(0,
- attrib0.size,
- attrib0.type,
- attrib0.normalized,
- attrib0.stride,
- reinterpret_cast<const GLvoid*>(attrib0.byteOffset));
- }
+ gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, attrib0.mBuf->mGLName);
+ attrib0.DoVertexAttribPointer(gl, 0);
} else {
gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
}
gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mBoundArrayBuffer ? mBoundArrayBuffer->mGLName : 0);
}
static GLuint
--- a/dom/canvas/WebGLContextVertices.cpp
+++ b/dom/canvas/WebGLContextVertices.cpp
@@ -295,37 +295,38 @@ WebGLContext::EnableVertexAttribArray(GL
MakeContextCurrent();
InvalidateBufferFetching();
gl->fEnableVertexAttribArray(index);
MOZ_ASSERT(mBoundVertexArray);
mBoundVertexArray->EnsureAttrib(index);
- mBoundVertexArray->mAttribs[index].enabled = true;
+ mBoundVertexArray->mAttribs[index].mEnabled = true;
}
void
WebGLContext::DisableVertexAttribArray(GLuint index)
{
if (IsContextLost())
return;
if (!ValidateAttribIndex(index, "disableVertexAttribArray"))
return;
MakeContextCurrent();
InvalidateBufferFetching();
- if (index || gl->IsGLES())
+ if (index || gl->IsGLES()) {
gl->fDisableVertexAttribArray(index);
+ }
MOZ_ASSERT(mBoundVertexArray);
mBoundVertexArray->EnsureAttrib(index);
- mBoundVertexArray->mAttribs[index].enabled = false;
+ mBoundVertexArray->mAttribs[index].mEnabled = false;
}
JS::Value
WebGLContext::GetVertexAttrib(JSContext* cx, GLuint index, GLenum pname,
ErrorResult& rv)
{
if (IsContextLost())
return JS::NullValue();
@@ -335,38 +336,38 @@ WebGLContext::GetVertexAttrib(JSContext*
MOZ_ASSERT(mBoundVertexArray);
mBoundVertexArray->EnsureAttrib(index);
MakeContextCurrent();
switch (pname) {
case LOCAL_GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
- return WebGLObjectAsJSValue(cx, mBoundVertexArray->mAttribs[index].buf.get(), rv);
+ return WebGLObjectAsJSValue(cx, mBoundVertexArray->mAttribs[index].mBuf.get(), rv);
case LOCAL_GL_VERTEX_ATTRIB_ARRAY_STRIDE:
- return JS::Int32Value(mBoundVertexArray->mAttribs[index].stride);
+ return JS::Int32Value(mBoundVertexArray->mAttribs[index].Stride());
case LOCAL_GL_VERTEX_ATTRIB_ARRAY_SIZE:
- return JS::Int32Value(mBoundVertexArray->mAttribs[index].size);
+ return JS::Int32Value(mBoundVertexArray->mAttribs[index].Size());
case LOCAL_GL_VERTEX_ATTRIB_ARRAY_TYPE:
- return JS::Int32Value(mBoundVertexArray->mAttribs[index].type);
+ return JS::Int32Value(mBoundVertexArray->mAttribs[index].Type());
case LOCAL_GL_VERTEX_ATTRIB_ARRAY_INTEGER:
if (IsWebGL2())
- return JS::BooleanValue(mBoundVertexArray->mAttribs[index].integer);
+ return JS::BooleanValue(mBoundVertexArray->mAttribs[index].IntegerFunc());
break;
case LOCAL_GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
if (IsWebGL2() ||
IsExtensionEnabled(WebGLExtensionID::ANGLE_instanced_arrays))
{
- return JS::Int32Value(mBoundVertexArray->mAttribs[index].divisor);
+ return JS::Int32Value(mBoundVertexArray->mAttribs[index].mDivisor);
}
break;
case LOCAL_GL_CURRENT_VERTEX_ATTRIB:
{
JS::RootedObject obj(cx);
switch (mVertexAttribType[index]) {
case LOCAL_GL_FLOAT:
@@ -383,20 +384,20 @@ WebGLContext::GetVertexAttrib(JSContext*
}
if (!obj)
rv.Throw(NS_ERROR_OUT_OF_MEMORY);
return JS::ObjectOrNullValue(obj);
}
case LOCAL_GL_VERTEX_ATTRIB_ARRAY_ENABLED:
- return JS::BooleanValue(mBoundVertexArray->mAttribs[index].enabled);
+ return JS::BooleanValue(mBoundVertexArray->mAttribs[index].mEnabled);
case LOCAL_GL_VERTEX_ATTRIB_ARRAY_NORMALIZED:
- return JS::BooleanValue(mBoundVertexArray->mAttribs[index].normalized);
+ return JS::BooleanValue(mBoundVertexArray->mAttribs[index].Normalized());
default:
break;
}
ErrorInvalidEnumInfo("getVertexAttrib: parameter", pname);
return JS::NullValue();
}
@@ -412,17 +413,17 @@ WebGLContext::GetVertexAttribOffset(GLui
if (pname != LOCAL_GL_VERTEX_ATTRIB_ARRAY_POINTER) {
ErrorInvalidEnum("getVertexAttribOffset: bad parameter");
return 0;
}
MOZ_ASSERT(mBoundVertexArray);
mBoundVertexArray->EnsureAttrib(index);
- return mBoundVertexArray->mAttribs[index].byteOffset;
+ return mBoundVertexArray->mAttribs[index].ByteOffset();
}
void
WebGLContext::VertexAttribPointer(GLuint index, GLint size, GLenum type,
WebGLboolean normalized, GLsizei stride,
WebGLintptr byteOffset)
{
if (IsContextLost())
@@ -439,45 +440,40 @@ WebGLContext::VertexAttribPointer(GLuint
InvalidateBufferFetching();
/* XXX make work with bufferSubData & heterogeneous types
if (type != mBoundArrayBuffer->GLType())
return ErrorInvalidOperation("vertexAttribPointer: type must match bound VBO type: %d != %d", type, mBoundArrayBuffer->GLType());
*/
- WebGLVertexAttribData& vd = mBoundVertexArray->mAttribs[index];
-
- vd.buf = mBoundArrayBuffer;
- vd.stride = stride;
- vd.size = size;
- vd.byteOffset = byteOffset;
- vd.type = type;
- vd.normalized = normalized;
- vd.integer = false;
-
MakeContextCurrent();
gl->fVertexAttribPointer(index, size, type, normalized, stride,
reinterpret_cast<void*>(byteOffset));
+
+ WebGLVertexAttribData& vd = mBoundVertexArray->mAttribs[index];
+ const bool integerFunc = false;
+ vd.VertexAttribPointer(integerFunc, mBoundArrayBuffer, size, type, normalized, stride,
+ byteOffset);
}
void
WebGLContext::VertexAttribDivisor(GLuint index, GLuint divisor)
{
if (IsContextLost())
return;
if (!ValidateAttribIndex(index, "vertexAttribDivisor"))
return;
MOZ_ASSERT(mBoundVertexArray);
mBoundVertexArray->EnsureAttrib(index);
WebGLVertexAttribData& vd = mBoundVertexArray->mAttribs[index];
- vd.divisor = divisor;
+ vd.mDivisor = divisor;
InvalidateBufferFetching();
MakeContextCurrent();
gl->fVertexAttribDivisor(index, divisor);
}
--- a/dom/canvas/WebGLVertexArray.h
+++ b/dom/canvas/WebGLVertexArray.h
@@ -34,17 +34,17 @@ public:
BindVertexArrayImpl();
};
void EnsureAttrib(GLuint index);
bool HasAttrib(GLuint index) const {
return index < mAttribs.Length();
}
bool IsAttribArrayEnabled(GLuint index) const {
- return HasAttrib(index) && mAttribs[index].enabled;
+ return HasAttrib(index) && mAttribs[index].mEnabled;
}
// Implement parent classes:
void Delete();
bool IsVertexArray();
WebGLContext* GetParentObject() const {
return mContext;
--- a/dom/canvas/WebGLVertexArrayFake.cpp
+++ b/dom/canvas/WebGLVertexArrayFake.cpp
@@ -24,41 +24,36 @@ WebGLVertexArrayFake::BindVertexArrayImp
WebGLRefPtr<WebGLVertexArray> prevVertexArray = mContext->mBoundVertexArray;
mContext->mBoundVertexArray = this;
WebGLRefPtr<WebGLBuffer> prevBuffer = mContext->mBoundArrayBuffer;
mContext->BindBuffer(LOCAL_GL_ELEMENT_ARRAY_BUFFER, mElementArrayBuffer);
- for (size_t i = 0; i < mAttribs.Length(); ++i) {
- const WebGLVertexAttribData& vd = mAttribs[i];
+ size_t i = 0;
+ for (const auto& vd : mAttribs) {
+ mContext->BindBuffer(LOCAL_GL_ARRAY_BUFFER, vd.mBuf);
+ vd.DoVertexAttribPointer(gl, i);
- mContext->BindBuffer(LOCAL_GL_ARRAY_BUFFER, vd.buf);
-
- if (vd.integer) {
- gl->fVertexAttribIPointer(i, vd.size, vd.type, vd.stride,
- reinterpret_cast<const GLvoid*>(vd.byteOffset));
+ if (vd.mEnabled) {
+ gl->fEnableVertexAttribArray(i);
} else {
- gl->fVertexAttribPointer(i, vd.size, vd.type, vd.normalized, vd.stride,
- reinterpret_cast<const GLvoid*>(vd.byteOffset));
+ gl->fDisableVertexAttribArray(i);
}
-
- if (vd.enabled)
- gl->fEnableVertexAttribArray(i);
- else
- gl->fDisableVertexAttribArray(i);
+ ++i;
}
size_t len = prevVertexArray->mAttribs.Length();
- for (size_t i = mAttribs.Length(); i < len; ++i) {
- const WebGLVertexAttribData& vd = prevVertexArray->mAttribs[i];
+ for (; i < len; ++i) {
+ const auto& vd = prevVertexArray->mAttribs[i];
- if (vd.enabled)
+ if (vd.mEnabled) {
gl->fDisableVertexAttribArray(i);
+ }
}
mContext->BindBuffer(LOCAL_GL_ARRAY_BUFFER, prevBuffer);
mIsVAO = true;
}
void
WebGLVertexArrayFake::DeleteImpl()
new file mode 100644
--- /dev/null
+++ b/dom/canvas/WebGLVertexAttribData.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebGLVertexAttribData.h"
+
+#include "GLContext.h"
+
+namespace mozilla {
+
+static uint8_t
+CalcBytesPerVertex(GLenum type, uint8_t size)
+{
+ uint8_t bytesPerType;
+ switch (type) {
+ case LOCAL_GL_INT_2_10_10_10_REV:
+ case LOCAL_GL_UNSIGNED_INT_2_10_10_10_REV:
+ return 4;
+
+ case LOCAL_GL_BYTE:
+ case LOCAL_GL_UNSIGNED_BYTE:
+ bytesPerType = 1;
+ break;
+
+ case LOCAL_GL_HALF_FLOAT:
+ case LOCAL_GL_SHORT:
+ case LOCAL_GL_UNSIGNED_SHORT:
+ bytesPerType = 2;
+ break;
+
+ case LOCAL_GL_FIXED: // GLES 3.0.4 p9: 32-bit signed, with 16 fractional bits.
+ case LOCAL_GL_FLOAT:
+ case LOCAL_GL_INT:
+ case LOCAL_GL_UNSIGNED_INT:
+ bytesPerType = 4;
+ break;
+
+ default:
+ MOZ_CRASH("Bad `type`.");
+ }
+
+ return bytesPerType * size;
+}
+
+void
+WebGLVertexAttribData::VertexAttribPointer(bool integerFunc, WebGLBuffer* buf,
+ uint8_t size, GLenum type, bool normalized,
+ uint32_t stride, uint64_t byteOffset)
+{
+ mIntegerFunc = integerFunc;
+ mBuf = buf;
+ mType = type;
+ mSize = size;
+ mBytesPerVertex = CalcBytesPerVertex(mType, mSize);
+ mNormalized = normalized;
+ mStride = stride;
+ mExplicitStride = (mStride ? mStride : mBytesPerVertex);
+ mByteOffset = byteOffset;
+}
+
+void
+WebGLVertexAttribData::DoVertexAttribPointer(gl::GLContext* gl, GLuint index) const
+{
+ if (mIntegerFunc) {
+ gl->fVertexAttribIPointer(index, mSize, mType, mStride,
+ (const void*)mByteOffset);
+ } else {
+ gl->fVertexAttribPointer(index, mSize, mType, mNormalized, mStride,
+ (const void*)mByteOffset);
+ }
+}
+
+} // namespace mozilla
--- a/dom/canvas/WebGLVertexAttribData.h
+++ b/dom/canvas/WebGLVertexAttribData.h
@@ -8,82 +8,69 @@
#include "GLDefs.h"
#include "WebGLObjectModel.h"
namespace mozilla {
class WebGLBuffer;
-struct WebGLVertexAttribData
+class WebGLVertexAttribData final
{
+public:
+ uint32_t mDivisor;
+ bool mEnabled;
+
+private:
+ bool mIntegerFunc;
+public:
+ WebGLRefPtr<WebGLBuffer> mBuf;
+private:
+ GLenum mType;
+ uint8_t mSize; // num of mType vals per vert
+ uint8_t mBytesPerVertex;
+ bool mNormalized;
+ uint32_t mStride; // bytes
+ uint32_t mExplicitStride;
+ uint64_t mByteOffset;
+
+public:
+
+#define GETTER(X) const decltype(m##X)& X() const { return m##X; }
+
+ GETTER(IntegerFunc)
+ GETTER(Type)
+ GETTER(Size)
+ GETTER(BytesPerVertex)
+ GETTER(Normalized)
+ GETTER(Stride)
+ GETTER(ExplicitStride)
+ GETTER(ByteOffset)
+
+#undef GETTER
+
// note that these initial values are what GL initializes vertex attribs to
WebGLVertexAttribData()
- : buf(0)
- , stride(0)
- , size(4)
- , divisor(0) // OpenGL ES 3.0 specs paragraphe 6.2 p240
- , byteOffset(0)
- , type(LOCAL_GL_FLOAT)
- , enabled(false)
- , normalized(false)
- , integer(false)
- {}
-
- WebGLRefPtr<WebGLBuffer> buf;
- GLuint stride;
- GLuint size;
- GLuint divisor;
- GLuint byteOffset;
- GLenum type;
- bool enabled;
- bool normalized;
- bool integer;
-
- GLuint componentSize() const {
- switch(type) {
- case LOCAL_GL_BYTE:
- case LOCAL_GL_UNSIGNED_BYTE:
- return 1;
-
- case LOCAL_GL_SHORT:
- case LOCAL_GL_UNSIGNED_SHORT:
- case LOCAL_GL_HALF_FLOAT:
- case LOCAL_GL_HALF_FLOAT_OES:
- return 2;
-
- case LOCAL_GL_INT:
- case LOCAL_GL_UNSIGNED_INT:
- case LOCAL_GL_FLOAT:
- return 4;
-
- default:
- MOZ_ASSERT(false, "Should never get here!");
- return 0;
- }
+ : mDivisor(0)
+ , mEnabled(false)
+ {
+ VertexAttribPointer(false, nullptr, 4, LOCAL_GL_FLOAT, false, 0, 0);
}
- GLuint actualStride() const {
- if (stride)
- return stride;
+ void VertexAttribPointer(bool integerFunc, WebGLBuffer* buf, uint8_t size,
+ GLenum type, bool normalized, uint32_t stride,
+ uint64_t byteOffset);
- return size * componentSize();
- }
+ void DoVertexAttribPointer(gl::GLContext* gl, GLuint index) const;
};
} // namespace mozilla
inline void
-ImplCycleCollectionUnlink(mozilla::WebGLVertexAttribData& field)
-{
- field.buf = nullptr;
-}
-
-inline void
ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& callback,
mozilla::WebGLVertexAttribData& field,
const char* name,
uint32_t flags = 0)
{
- CycleCollectionNoteChild(callback, field.buf.get(), name, flags);
+ CycleCollectionNoteChild(callback, field.mBuf.get(), name, flags);
}
#endif // WEBGL_VERTEX_ATTRIB_DATA_H_
--- a/dom/canvas/moz.build
+++ b/dom/canvas/moz.build
@@ -152,16 +152,17 @@ UNIFIED_SOURCES += [
'WebGLTimerQuery.cpp',
'WebGLTransformFeedback.cpp',
'WebGLUniformLocation.cpp',
'WebGLValidateStrings.cpp',
'WebGLVertexArray.cpp',
'WebGLVertexArrayFake.cpp',
'WebGLVertexArrayGL.cpp',
'WebGLVertexArrayObject.cpp',
+ 'WebGLVertexAttribData.cpp',
]
SOURCES += [
'MurmurHash3.cpp',
]
# Suppress warnings from third-party code.
if CONFIG['CLANG_CXX']: