mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 05:38:31 +09:00
[Fix] (MG_Impl/Framebuffer, MG_State/Framebuffer, MG_Backend/DirectGLES): redone fbo attachment management. remap GL -> ES attachment names
This commit is contained in:
@@ -137,26 +137,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
namespace FramebufferImpl {
|
namespace FramebufferImpl {
|
||||||
void SyncCurrentFBO() {
|
void SyncCurrentFBO() {
|
||||||
auto currentFBO = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read};
|
||||||
if (!currentFBO) {
|
|
||||||
MGLOG_E("No FBO is currently bound, cannot sync current FBO.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentFBO == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
|
MG_State::GLState::FramebufferObject* lastUpdatedFBO = nullptr;
|
||||||
// Default FBO, nothing to sync
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& backendFBOIt = g_backendFramebufferObjects.find(currentFBO);
|
for (auto target: fboTargets) {
|
||||||
SharedPtr<BackendFramebufferObject> backendFBOObject;
|
auto currentFBO =
|
||||||
if (backendFBOIt == g_backendFramebufferObjects.end()) {
|
MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
|
||||||
backendFBOObject = MakeShared<BackendFramebufferObject>();
|
|
||||||
g_backendFramebufferObjects[currentFBO] = backendFBOObject;
|
if (!currentFBO) {
|
||||||
} else {
|
MGLOG_E("No FBO is currently bound, cannot sync current FBO.");
|
||||||
backendFBOObject = backendFBOIt->second;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentFBO ==
|
||||||
|
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
|
||||||
|
// Default FBO, nothing to sync
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &backendFBOIt = g_backendFramebufferObjects.find(currentFBO);
|
||||||
|
SharedPtr<BackendFramebufferObject> backendFBOObject;
|
||||||
|
if (backendFBOIt == g_backendFramebufferObjects.end()) {
|
||||||
|
backendFBOObject = MakeShared<BackendFramebufferObject>();
|
||||||
|
g_backendFramebufferObjects[currentFBO] = backendFBOObject;
|
||||||
|
} else {
|
||||||
|
backendFBOObject = backendFBOIt->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentFBO.get() == lastUpdatedFBO) {
|
||||||
|
MGLOG_I("Draw FBO and read FBO are the same, skipping sync.");
|
||||||
|
} else {
|
||||||
|
backendFBOObject->SyncToBackend(currentFBO, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
backendFBOObject->Bind(target);
|
||||||
|
|
||||||
|
lastUpdatedFBO = currentFBO.get();
|
||||||
}
|
}
|
||||||
backendFBOObject->SyncToBackend(currentFBO);
|
|
||||||
}
|
}
|
||||||
} // namespace FramebufferImpl
|
} // namespace FramebufferImpl
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include "DirectGLES.h"
|
#include "DirectGLES.h"
|
||||||
|
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
|
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
|
||||||
@@ -383,62 +384,127 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_External::GLES::glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
|
MG_External::GLES::glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendFramebufferObject::SyncToBackend(SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
|
void BackendFramebufferObject::SyncToBackend(SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {
|
||||||
if (!stateFBOObject) {
|
if (!stateFBOObject) {
|
||||||
MGLOG_E("State FBO object is null, cannot sync to backend.");
|
MGLOG_E("State FBO object is null, cannot sync to backend.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MGLOG_D("Syncing FBO object with ID: %u to backend for state: %p", m_backendFBOId, stateFBOObject.get());
|
MGLOG_D("Syncing FBO object with ID: %u to backend for state: %p, as %s FBO", m_backendFBOId, stateFBOObject.get(),
|
||||||
|
(asTarget == FramebufferTarget::Draw ? "DRAW" : "READ"));
|
||||||
|
|
||||||
BackendFramebufferBindingProtector backendFBOBindingProtector(GL_FRAMEBUFFER);
|
GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget);
|
||||||
// TODO: do i really need to bind here?
|
BackendFramebufferBindingProtector backendFBOBindingProtector(glFBOTarget);
|
||||||
Bind(FramebufferTarget::Read);
|
|
||||||
Bind(FramebufferTarget::Draw);
|
|
||||||
|
|
||||||
// TODO: add dirty check
|
Bind(asTarget);
|
||||||
// Sync all attachments
|
|
||||||
const auto& attachments = stateFBOObject->GetAllAttachments();
|
if (asTarget == FramebufferTarget::Draw) {
|
||||||
for (SizeT index = 0; index < attachments.size(); ++index) {
|
int nBuffers = 0;
|
||||||
FramebufferAttachmentType attachmentType = static_cast<FramebufferAttachmentType>(index);
|
// Attach attachment to FBO, realize `glDrawBuffers`
|
||||||
const auto& attachment = attachments[index];
|
if (stateFBOObject->DrawBuffersIsDirty()) {
|
||||||
if (!attachment.IsComplete()) {
|
std::fill(m_frontendBuffers,
|
||||||
continue;
|
m_frontendBuffers + MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS,
|
||||||
|
GL_NONE);
|
||||||
|
std::fill(m_backendBuffers,
|
||||||
|
m_backendBuffers + MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS,
|
||||||
|
GL_NONE);
|
||||||
|
auto &stateDrawBuffers = stateFBOObject->GetDrawBuffers();
|
||||||
|
for (GLint i = 0; i < stateDrawBuffers.size(); ++i) {
|
||||||
|
m_frontendBuffers[i] =
|
||||||
|
MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(stateDrawBuffers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
MGLOG_D("%s: mapping draw buffers gl -> es:", __func__);
|
||||||
|
for (int i = 0;
|
||||||
|
i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
||||||
|
if (m_frontendBuffers[i] == GL_NONE) {
|
||||||
|
MGLOG_D("(m_frontendBuffers[%d] = GL_NONE), skipped", i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_backendBuffers[nBuffers] = GL_COLOR_ATTACHMENT0 + nBuffers;
|
||||||
|
m_compactedFrontendBuffers[nBuffers] = m_frontendBuffers[i];
|
||||||
|
MGLOG_D("(m_frontendBuffers[%d] = %s) -> %s",
|
||||||
|
i, MG_Util::ConvertGLEnumToString(m_frontendBuffers[i]).c_str(),
|
||||||
|
MG_Util::ConvertGLEnumToString(m_backendBuffers[nBuffers]).c_str());
|
||||||
|
nBuffers++;
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_External::GLES::glDrawBuffers(nBuffers, m_backendBuffers);
|
||||||
|
|
||||||
|
stateFBOObject->ClearDrawBuffersDirtyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.IsTexture()) {
|
// Sync all attachments
|
||||||
const auto& textureObject = attachment.GetTexture();
|
const auto& attachments = stateFBOObject->GetAllAttachments();
|
||||||
const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject);
|
for (SizeT i = 0; i < nBuffers; ++i) {
|
||||||
if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) {
|
FramebufferAttachmentType frontendAttachmentType =
|
||||||
MGLOG_E("No backend texture found for FBO attachment, cannot bind texture.");
|
MG_Util::ConvertGLEnumToFramebufferAttachmentType(m_compactedFrontendBuffers[i]);
|
||||||
|
GLenum glBackendAttachmentType = m_backendBuffers[i];
|
||||||
|
|
||||||
|
const auto& attachment = attachments[(SizeT)frontendAttachmentType];
|
||||||
|
if (!attachment.IsComplete()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto& backendTextureObject = backendTextureIt->second;
|
|
||||||
backendTextureObject->Bind(MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()));
|
|
||||||
MG_External::GLES::glFramebufferTexture2D(
|
|
||||||
GL_FRAMEBUFFER, MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(attachmentType),
|
|
||||||
MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()),
|
|
||||||
backendTextureObject->GetBackendTextureId(), static_cast<GLint>(attachment.GetTextureLevel()));
|
|
||||||
} else if (attachment.IsRenderbuffer()) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateFBOObject->DrawBuffersIsDirty()) {
|
if (attachment.IsTexture()) {
|
||||||
static GLenum drawbufs[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS];
|
const auto& textureObject = attachment.GetTexture();
|
||||||
std::fill(drawbufs, drawbufs + MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE);
|
const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject);
|
||||||
auto& stateDrawBuffers = stateFBOObject->GetDrawBuffers();
|
if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) {
|
||||||
GLint i = 0;
|
MGLOG_E("No backend texture found for FBO attachment, cannot bind texture.");
|
||||||
for (; i < stateDrawBuffers.size(); ++i) {
|
continue;
|
||||||
if (stateDrawBuffers[i] == FramebufferAttachmentType::None) {
|
}
|
||||||
break;
|
const auto& backendTextureObject = backendTextureIt->second;
|
||||||
|
auto glTextureTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget());
|
||||||
|
backendTextureObject->Bind(glTextureTarget);
|
||||||
|
MG_External::GLES::glFramebufferTexture2D(
|
||||||
|
glFBOTarget, glBackendAttachmentType, glTextureTarget,
|
||||||
|
backendTextureObject->GetBackendTextureId(), static_cast<GLint>(attachment.GetTextureLevel()));
|
||||||
|
} else if (attachment.IsRenderbuffer()) {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
drawbufs[i] = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(stateDrawBuffers[i]);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Attach attachment to FBO, realize `glReadBuffer`
|
||||||
|
// TODO: do we actually need "virtualization" here? I assume not?
|
||||||
|
GLenum frontendAtt = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(stateFBOObject->GetReadBuffer());
|
||||||
|
GLenum backendAtt = GL_NONE;
|
||||||
|
// Attach attachment to FBO, fast path
|
||||||
|
for (SizeT i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
||||||
|
if (m_compactedFrontendBuffers[i] == frontendAtt)
|
||||||
|
backendAtt = m_backendBuffers[i];
|
||||||
|
}
|
||||||
|
if (backendAtt != GL_NONE) {
|
||||||
|
MG_External::GLES::glReadBuffer(backendAtt);
|
||||||
|
} else {
|
||||||
|
// Maybe we should properly do `glFramebufferTexture2D` here
|
||||||
|
// don't need remap / virtualization
|
||||||
|
MG_External::GLES::glReadBuffer(frontendAtt);
|
||||||
|
const auto &attachments = stateFBOObject->GetAllAttachments();
|
||||||
|
const auto &attachment = attachments[(SizeT) frontendAtt];
|
||||||
|
if (!attachment.IsComplete()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MG_External::GLES::glDrawBuffers(i, drawbufs);
|
if (attachment.IsTexture()) {
|
||||||
|
const auto &textureObject = attachment.GetTexture();
|
||||||
stateFBOObject->ClearDrawBuffersDirtyState();
|
const auto &backendTextureIt =
|
||||||
|
TextureImpl::g_backendTextureObjects.find(textureObject);
|
||||||
|
if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) {
|
||||||
|
MGLOG_E("No backend texture found for FBO attachment, cannot bind texture.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &backendTextureObject = backendTextureIt->second;
|
||||||
|
auto glTextureTarget = MG_Util::ConvertTextureTargetToGLEnum(
|
||||||
|
textureObject->GetTarget());
|
||||||
|
backendTextureObject->Bind(glTextureTarget);
|
||||||
|
MG_External::GLES::glFramebufferTexture2D(
|
||||||
|
glFBOTarget, frontendAtt, glTextureTarget,
|
||||||
|
backendTextureObject->GetBackendTextureId(),
|
||||||
|
static_cast<GLint>(attachment.GetTextureLevel()));
|
||||||
|
} else if (attachment.IsRenderbuffer()) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,12 +83,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
class BackendFramebufferObject {
|
class BackendFramebufferObject {
|
||||||
public:
|
public:
|
||||||
BackendFramebufferObject();
|
BackendFramebufferObject();
|
||||||
void SyncToBackend(SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
|
void SyncToBackend(SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget);
|
||||||
Uint GetBackendFramebufferId() { return m_backendFBOId; }
|
Uint GetBackendFramebufferId() { return m_backendFBOId; }
|
||||||
void Bind(FramebufferTarget target);
|
void Bind(FramebufferTarget target);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendFBOId = 0;
|
Uint m_backendFBOId = 0;
|
||||||
|
|
||||||
|
/* this will save buffers in its original form,
|
||||||
|
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
||||||
|
i.e. it could be like [COLOR_ATTACHMENT0, COLOR_ATTACHMENT5, NONE, COLOR_ATTACHMENT4]
|
||||||
|
Probably useful to re-link shader output according to this.
|
||||||
|
aka. realizing `glBindFragDataLocation`
|
||||||
|
*/
|
||||||
|
GLenum m_frontendBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
||||||
|
/* this will save buffers in its compacted GL form,
|
||||||
|
not consecutive is not allowed
|
||||||
|
i.e. it could be like [COLOR_ATTACHMENT0, COLOR_ATTACHMENT5, COLOR_ATTACHMENT4]
|
||||||
|
(no GL_NONE among those)
|
||||||
|
*/
|
||||||
|
GLenum m_compactedFrontendBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
||||||
|
/* this will save buffers in stricter ES rules
|
||||||
|
reversion, absence or not consecutive are not allowed, according to ES spec
|
||||||
|
i.e. it could be like [COLOR_ATTACHMENT0, COLOR_ATTACHMENT1, NONE, NONE, ...]
|
||||||
|
this array could be provided as data directly to ES `glDrawBuffers` function
|
||||||
|
*/
|
||||||
|
GLenum m_backendBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
||||||
};
|
};
|
||||||
|
|
||||||
extern UnorderedMap<SharedPtr<MG_State::GLState::FramebufferObject>, SharedPtr<BackendFramebufferObject>>
|
extern UnorderedMap<SharedPtr<MG_State::GLState::FramebufferObject>, SharedPtr<BackendFramebufferObject>>
|
||||||
|
|||||||
@@ -67,23 +67,24 @@ namespace MobileGL {
|
|||||||
m_externalIndex(externalIndex) {
|
m_externalIndex(externalIndex) {
|
||||||
m_attachments.fill(FramebufferAttachment(false));
|
m_attachments.fill(FramebufferAttachment(false));
|
||||||
m_drawBuffers.fill(FramebufferAttachmentType::None);
|
m_drawBuffers.fill(FramebufferAttachmentType::None);
|
||||||
|
m_drawBuffers[0] = FramebufferAttachmentType::Color0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FramebufferObject::AttachTexture(FramebufferAttachmentType type, SharedPtr<ITextureObject> texture,
|
void FramebufferObject::AttachTexture(FramebufferAttachmentType type, SharedPtr<ITextureObject> texture,
|
||||||
int level) {
|
int level) {
|
||||||
m_attachments[static_cast<SizeT>(type)] = FramebufferAttachment(std::move(texture), level);
|
m_attachments[static_cast<SizeT>(type)] = FramebufferAttachment(std::move(texture), level);
|
||||||
// m_drawBuffersDirty = true;
|
m_drawBuffersDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FramebufferObject::AttachRenderbuffer(FramebufferAttachmentType type,
|
void FramebufferObject::AttachRenderbuffer(FramebufferAttachmentType type,
|
||||||
std::shared_ptr<RenderbufferObjectStub> renderbuffer) {
|
std::shared_ptr<RenderbufferObjectStub> renderbuffer) {
|
||||||
m_attachments[static_cast<SizeT>(type)] = FramebufferAttachment(renderbuffer);
|
m_attachments[static_cast<SizeT>(type)] = FramebufferAttachment(renderbuffer);
|
||||||
// m_drawBuffersDirty = true;
|
m_drawBuffersDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FramebufferObject::Detach(FramebufferAttachmentType type) {
|
void FramebufferObject::Detach(FramebufferAttachmentType type) {
|
||||||
m_attachments[static_cast<SizeT>(type)] = FramebufferAttachment(false);
|
m_attachments[static_cast<SizeT>(type)] = FramebufferAttachment(false);
|
||||||
// m_drawBuffersDirty = true;
|
m_drawBuffersDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FramebufferAttachment& FramebufferObject::GetAttachment(FramebufferAttachmentType type) const {
|
const FramebufferAttachment& FramebufferObject::GetAttachment(FramebufferAttachmentType type) const {
|
||||||
|
|||||||
@@ -103,11 +103,12 @@ namespace MobileGL {
|
|||||||
static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)>&
|
static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)>&
|
||||||
GetAllAttachments() const;
|
GetAllAttachments() const;
|
||||||
Bool CheckCompleteness() const;
|
Bool CheckCompleteness() const;
|
||||||
// void SetDrawBuffers(const Vector<FramebufferAttachmentType>& buffers);
|
// aka. `buffer` as in glDrawBuffers/glReadBuffers
|
||||||
void SetDrawBuffer(Uint index, FramebufferAttachmentType buffer);
|
void SetDrawBuffer(Uint index, FramebufferAttachmentType buffer);
|
||||||
bool DrawBuffersIsDirty() const { return m_drawBuffersDirty; }
|
bool DrawBuffersIsDirty() const { return m_drawBuffersDirty; }
|
||||||
void ClearDrawBuffersDirtyState() { m_drawBuffersDirty = false; }
|
void ClearDrawBuffersDirtyState() { m_drawBuffersDirty = false; }
|
||||||
const Array<FramebufferAttachmentType, MAX_DRAW_BUFFERS>& GetDrawBuffers() const;
|
const Array<FramebufferAttachmentType, MAX_DRAW_BUFFERS>& GetDrawBuffers() const;
|
||||||
|
FramebufferAttachmentType GetReadBuffer() const { return m_readBuffer; }
|
||||||
Uint GetExternalIndex() const;
|
Uint GetExternalIndex() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -117,6 +118,7 @@ namespace MobileGL {
|
|||||||
m_attachments;
|
m_attachments;
|
||||||
Bool m_drawBuffersDirty = false;
|
Bool m_drawBuffersDirty = false;
|
||||||
Array<FramebufferAttachmentType, MAX_DRAW_BUFFERS> m_drawBuffers;
|
Array<FramebufferAttachmentType, MAX_DRAW_BUFFERS> m_drawBuffers;
|
||||||
|
FramebufferAttachmentType m_readBuffer = FramebufferAttachmentType::None;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace MobileGL {
|
|||||||
case FramebufferAttachmentType::Stencil:
|
case FramebufferAttachmentType::Stencil:
|
||||||
return GL_STENCIL_ATTACHMENT;
|
return GL_STENCIL_ATTACHMENT;
|
||||||
default:
|
default:
|
||||||
return GL_COLOR_ATTACHMENT0;
|
return GL_NONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user