mirror of
https://github.com/hrydgard/ppsspp.git
synced 2024-11-23 13:30:02 +00:00
Android: Add both a lost and restore phase.
Lost = delete, restore = create. Let's stick to never overlapping. May help #8912.
This commit is contained in:
parent
29f5763d32
commit
92d930887f
@ -534,6 +534,10 @@ void GPU_DX9::DeviceLost() {
|
|||||||
framebufferManager_.DeviceLost();
|
framebufferManager_.DeviceLost();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GPU_DX9::DeviceRestore() {
|
||||||
|
// Nothing needed.
|
||||||
|
}
|
||||||
|
|
||||||
void GPU_DX9::InitClear() {
|
void GPU_DX9::InitClear() {
|
||||||
ScheduleEvent(GPU_EVENT_INIT_CLEAR);
|
ScheduleEvent(GPU_EVENT_INIT_CLEAR);
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ public:
|
|||||||
bool PerformStencilUpload(u32 dest, int size) override;
|
bool PerformStencilUpload(u32 dest, int size) override;
|
||||||
void ClearCacheNextFrame() override;
|
void ClearCacheNextFrame() override;
|
||||||
void DeviceLost() override; // Only happens on Android. Drop all textures and shaders.
|
void DeviceLost() override; // Only happens on Android. Drop all textures and shaders.
|
||||||
|
void DeviceRestore() override;
|
||||||
|
|
||||||
void DumpNextFrame() override;
|
void DumpNextFrame() override;
|
||||||
void DoState(PointerWrap &p) override;
|
void DoState(PointerWrap &p) override;
|
||||||
|
@ -207,14 +207,18 @@ void DrawEngineGLES::DestroyDeviceObjects() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawEngineGLES::GLRestore() {
|
void DrawEngineGLES::GLLost() {
|
||||||
ILOG("TransformDrawEngine::GLRestore()");
|
ILOG("TransformDrawEngine::GLLost()");
|
||||||
// The objects have already been deleted.
|
// The objects have already been deleted.
|
||||||
bufferNameCache_.clear();
|
bufferNameCache_.clear();
|
||||||
bufferNameInfo_.clear();
|
bufferNameInfo_.clear();
|
||||||
freeSizedBuffers_.clear();
|
freeSizedBuffers_.clear();
|
||||||
bufferNameCacheSize_ = 0;
|
bufferNameCacheSize_ = 0;
|
||||||
ClearTrackedVertexArrays();
|
ClearTrackedVertexArrays();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawEngineGLES::GLRestore() {
|
||||||
|
ILOG("TransformDrawEngine::GLRestore()");
|
||||||
InitDeviceObjects();
|
InitDeviceObjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +129,7 @@ public:
|
|||||||
void RestoreVAO();
|
void RestoreVAO();
|
||||||
void InitDeviceObjects();
|
void InitDeviceObjects();
|
||||||
void DestroyDeviceObjects();
|
void DestroyDeviceObjects();
|
||||||
|
void GLLost() override;
|
||||||
void GLRestore() override;
|
void GLRestore() override;
|
||||||
void Resized();
|
void Resized();
|
||||||
|
|
||||||
|
@ -651,6 +651,10 @@ void GPU_GLES::DeviceLost() {
|
|||||||
fragmentTestCache_.Clear(false);
|
fragmentTestCache_.Clear(false);
|
||||||
depalShaderCache_.Clear();
|
depalShaderCache_.Clear();
|
||||||
framebufferManager_.DeviceLost();
|
framebufferManager_.DeviceLost();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GPU_GLES::DeviceRestore() {
|
||||||
|
ILOG("GPU_GLES: DeviceRestore");
|
||||||
|
|
||||||
UpdateVsyncInterval(true);
|
UpdateVsyncInterval(true);
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ public:
|
|||||||
bool PerformStencilUpload(u32 dest, int size) override;
|
bool PerformStencilUpload(u32 dest, int size) override;
|
||||||
void ClearCacheNextFrame() override;
|
void ClearCacheNextFrame() override;
|
||||||
void DeviceLost() override; // Only happens on Android. Drop all textures and shaders.
|
void DeviceLost() override; // Only happens on Android. Drop all textures and shaders.
|
||||||
|
void DeviceRestore() override;
|
||||||
|
|
||||||
void DumpNextFrame() override;
|
void DumpNextFrame() override;
|
||||||
void DoState(PointerWrap &p) override;
|
void DoState(PointerWrap &p) override;
|
||||||
|
@ -268,6 +268,7 @@ public:
|
|||||||
virtual void EnableInterrupts(bool enable) = 0;
|
virtual void EnableInterrupts(bool enable) = 0;
|
||||||
|
|
||||||
virtual void DeviceLost() = 0;
|
virtual void DeviceLost() = 0;
|
||||||
|
virtual void DeviceRestore() = 0;
|
||||||
virtual void ReapplyGfxState() = 0;
|
virtual void ReapplyGfxState() = 0;
|
||||||
virtual void SyncThread(bool force = false) = 0;
|
virtual void SyncThread(bool force = false) = 0;
|
||||||
virtual void SyncBeginFrame() = 0;
|
virtual void SyncBeginFrame() = 0;
|
||||||
|
@ -43,6 +43,7 @@ public:
|
|||||||
void ClearCacheNextFrame() override {}
|
void ClearCacheNextFrame() override {}
|
||||||
|
|
||||||
void DeviceLost() override {}
|
void DeviceLost() override {}
|
||||||
|
void DeviceRestore() override {}
|
||||||
void DumpNextFrame() override {}
|
void DumpNextFrame() override {}
|
||||||
|
|
||||||
void Resized() override {}
|
void Resized() override {}
|
||||||
|
@ -78,6 +78,10 @@ void SoftGPU::DeviceLost() {
|
|||||||
// Handled by thin3d.
|
// Handled by thin3d.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SoftGPU::DeviceRestore() {
|
||||||
|
// Handled by thin3d.
|
||||||
|
}
|
||||||
|
|
||||||
SoftGPU::~SoftGPU() {
|
SoftGPU::~SoftGPU() {
|
||||||
vformat->Release();
|
vformat->Release();
|
||||||
vformat = nullptr;
|
vformat = nullptr;
|
||||||
|
@ -69,6 +69,7 @@ public:
|
|||||||
void ClearCacheNextFrame() override {}
|
void ClearCacheNextFrame() override {}
|
||||||
|
|
||||||
void DeviceLost() override;
|
void DeviceLost() override;
|
||||||
|
void DeviceRestore() override;
|
||||||
void DumpNextFrame() override {}
|
void DumpNextFrame() override {}
|
||||||
|
|
||||||
void Resized() override {}
|
void Resized() override {}
|
||||||
|
@ -1947,6 +1947,10 @@ void GPU_Vulkan::DeviceLost() {
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GPU_Vulkan::DeviceRestore() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
void GPU_Vulkan::GetStats(char *buffer, size_t bufsize) {
|
void GPU_Vulkan::GetStats(char *buffer, size_t bufsize) {
|
||||||
const DrawEngineVulkanStats &drawStats = drawEngine_.GetStats();
|
const DrawEngineVulkanStats &drawStats = drawEngine_.GetStats();
|
||||||
float vertexAverageCycles = gpuStats.numVertsSubmitted > 0 ? (float)gpuStats.vertexGPUCycles / (float)gpuStats.numVertsSubmitted : 0.0f;
|
float vertexAverageCycles = gpuStats.numVertsSubmitted > 0 ? (float)gpuStats.vertexGPUCycles / (float)gpuStats.numVertsSubmitted : 0.0f;
|
||||||
|
@ -62,6 +62,7 @@ public:
|
|||||||
bool PerformStencilUpload(u32 dest, int size) override;
|
bool PerformStencilUpload(u32 dest, int size) override;
|
||||||
void ClearCacheNextFrame() override;
|
void ClearCacheNextFrame() override;
|
||||||
void DeviceLost() override; // Only happens on Android. Drop all textures and shaders.
|
void DeviceLost() override; // Only happens on Android. Drop all textures and shaders.
|
||||||
|
void DeviceRestore() override;
|
||||||
|
|
||||||
void DumpNextFrame() override;
|
void DumpNextFrame() override;
|
||||||
void DoState(PointerWrap &p) override;
|
void DoState(PointerWrap &p) override;
|
||||||
|
@ -1097,6 +1097,12 @@ void EmuScreen::deviceLost() {
|
|||||||
ILOG("EmuScreen::deviceLost()");
|
ILOG("EmuScreen::deviceLost()");
|
||||||
if (gpu)
|
if (gpu)
|
||||||
gpu->DeviceLost();
|
gpu->DeviceLost();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmuScreen::deviceRestore() {
|
||||||
|
ILOG("EmuScreen::deviceRestore()");
|
||||||
|
if (gpu)
|
||||||
|
gpu->DeviceRestore();
|
||||||
|
|
||||||
RecreateViews();
|
RecreateViews();
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ public:
|
|||||||
void update(InputState &input) override;
|
void update(InputState &input) override;
|
||||||
void render() override;
|
void render() override;
|
||||||
void deviceLost() override;
|
void deviceLost() override;
|
||||||
|
void deviceRestore() override;
|
||||||
void dialogFinished(const Screen *dialog, DialogResult result) override;
|
void dialogFinished(const Screen *dialog, DialogResult result) override;
|
||||||
void sendMessage(const char *msg, const char *value) override;
|
void sendMessage(const char *msg, const char *value) override;
|
||||||
|
|
||||||
|
@ -808,11 +808,16 @@ void NativeUpdate(InputState &input) {
|
|||||||
screenManager->update(input);
|
screenManager->update(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NativeDeviceRestore() {
|
void NativeDeviceLost() {
|
||||||
if (g_gameInfoCache)
|
if (g_gameInfoCache)
|
||||||
g_gameInfoCache->Clear();
|
g_gameInfoCache->Clear();
|
||||||
screenManager->deviceLost();
|
screenManager->deviceLost();
|
||||||
|
gl_lost();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NativeDeviceRestore() {
|
||||||
|
NativeDeviceLost();
|
||||||
|
screenManager->deviceRestore();
|
||||||
if (GetGPUBackend() == GPUBackend::OPENGL) {
|
if (GetGPUBackend() == GPUBackend::OPENGL) {
|
||||||
gl_restore();
|
gl_restore();
|
||||||
}
|
}
|
||||||
|
@ -697,6 +697,7 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env,
|
|||||||
extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayShutdown(JNIEnv *env, jobject obj) {
|
extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayShutdown(JNIEnv *env, jobject obj) {
|
||||||
ILOG("NativeApp.displayShutdown()");
|
ILOG("NativeApp.displayShutdown()");
|
||||||
if (renderer_inited) {
|
if (renderer_inited) {
|
||||||
|
NativeDeviceLost();
|
||||||
NativeShutdownGraphics();
|
NativeShutdownGraphics();
|
||||||
renderer_inited = false;
|
renderer_inited = false;
|
||||||
NativeMessageReceived("recreateviews", "");
|
NativeMessageReceived("recreateviews", "");
|
||||||
|
@ -57,6 +57,24 @@ void gl_restore() {
|
|||||||
inRestore = false;
|
inRestore = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void gl_lost() {
|
||||||
|
inLost = true;
|
||||||
|
if (!holders) {
|
||||||
|
WLOG("GL resource holder not initialized, cannot process restore request");
|
||||||
|
inLost = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We should really do this when we get the context back, not during gl_lost...
|
||||||
|
ILOG("gl_lost() clearing %i items:", (int)holders->size());
|
||||||
|
for (size_t i = 0; i < holders->size(); i++) {
|
||||||
|
ILOG("gl_lost(%i / %i, %p, %08x)", (int)(i + 1), (int)holders->size(), (*holders)[i], *((uint32_t *)((*holders)[i])));
|
||||||
|
(*holders)[i]->GLLost();
|
||||||
|
}
|
||||||
|
ILOG("gl_lost() completed on %i items:", (int)holders->size());
|
||||||
|
inLost = false;
|
||||||
|
}
|
||||||
|
|
||||||
void gl_lost_manager_init() {
|
void gl_lost_manager_init() {
|
||||||
if (holders) {
|
if (holders) {
|
||||||
FLOG("Double GL lost manager init");
|
FLOG("Double GL lost manager init");
|
||||||
|
@ -7,6 +7,7 @@ class GfxResourceHolder {
|
|||||||
public:
|
public:
|
||||||
virtual ~GfxResourceHolder() {}
|
virtual ~GfxResourceHolder() {}
|
||||||
virtual void GLRestore() = 0;
|
virtual void GLRestore() = 0;
|
||||||
|
virtual void GLLost() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
void gl_lost_manager_init();
|
void gl_lost_manager_init();
|
||||||
@ -15,5 +16,8 @@ void gl_lost_manager_shutdown();
|
|||||||
void register_gl_resource_holder(GfxResourceHolder *holder);
|
void register_gl_resource_holder(GfxResourceHolder *holder);
|
||||||
void unregister_gl_resource_holder(GfxResourceHolder *holder);
|
void unregister_gl_resource_holder(GfxResourceHolder *holder);
|
||||||
|
|
||||||
|
// Notifies all objects it's time to forget / delete things.
|
||||||
|
void gl_lost();
|
||||||
|
|
||||||
// Notifies all objects that it's time to be restored.
|
// Notifies all objects that it's time to be restored.
|
||||||
void gl_restore();
|
void gl_restore();
|
||||||
|
@ -220,7 +220,7 @@ bool glsl_recompile(GLSLProgram *program, std::string *error_message) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLSLProgram::GLRestore() {
|
void GLSLProgram::GLLost() {
|
||||||
// Quoth http://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer.html;
|
// Quoth http://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer.html;
|
||||||
// "Note that when the EGL context is lost, all OpenGL resources associated with that context will be automatically deleted.
|
// "Note that when the EGL context is lost, all OpenGL resources associated with that context will be automatically deleted.
|
||||||
// You do not need to call the corresponding "glDelete" methods such as glDeleteTextures to manually delete these lost resources."
|
// You do not need to call the corresponding "glDelete" methods such as glDeleteTextures to manually delete these lost resources."
|
||||||
@ -228,9 +228,12 @@ void GLSLProgram::GLRestore() {
|
|||||||
// glDeleteShader(this->vsh_);
|
// glDeleteShader(this->vsh_);
|
||||||
// glDeleteShader(this->fsh_);
|
// glDeleteShader(this->fsh_);
|
||||||
// glDeleteProgram(this->program_);
|
// glDeleteProgram(this->program_);
|
||||||
this->program_ = 0;
|
program_ = 0;
|
||||||
this->vsh_ = 0;
|
vsh_ = 0;
|
||||||
this->fsh_ = 0;
|
fsh_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLSLProgram::GLRestore() {
|
||||||
ILOG("Restoring GLSL program %s/%s",
|
ILOG("Restoring GLSL program %s/%s",
|
||||||
strlen(this->vshader_filename) > 0 ? this->vshader_filename : "(mem)",
|
strlen(this->vshader_filename) > 0 ? this->vshader_filename : "(mem)",
|
||||||
strlen(this->fshader_filename) > 0 ? this->fshader_filename : "(mem)");
|
strlen(this->fshader_filename) > 0 ? this->fshader_filename : "(mem)");
|
||||||
|
@ -43,6 +43,7 @@ struct GLSLProgram : public GfxResourceHolder {
|
|||||||
GLuint fsh_;
|
GLuint fsh_;
|
||||||
GLuint program_;
|
GLuint program_;
|
||||||
|
|
||||||
|
void GLLost() override;
|
||||||
void GLRestore() override;
|
void GLRestore() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -207,8 +207,12 @@ public:
|
|||||||
glBindBuffer(target_, buffer_);
|
glBindBuffer(target_, buffer_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLLost() override {
|
||||||
|
buffer_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void GLRestore() override {
|
void GLRestore() override {
|
||||||
ILOG("Recreating vertex buffer after glLost");
|
ILOG("Recreating vertex buffer after gl_restore");
|
||||||
knownSize_ = 0; // Will cause a new glBufferData call. Should genBuffers again though?
|
knownSize_ = 0; // Will cause a new glBufferData call. Should genBuffers again though?
|
||||||
glGenBuffers(1, &buffer_);
|
glGenBuffers(1, &buffer_);
|
||||||
}
|
}
|
||||||
@ -235,6 +239,10 @@ public:
|
|||||||
}
|
}
|
||||||
const std::string &GetSource() const { return source_; }
|
const std::string &GetSource() const { return source_; }
|
||||||
|
|
||||||
|
void Unset() {
|
||||||
|
shader_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
~Thin3DGLShader() {
|
~Thin3DGLShader() {
|
||||||
glDeleteShader(shader_);
|
glDeleteShader(shader_);
|
||||||
}
|
}
|
||||||
@ -257,7 +265,7 @@ bool Thin3DGLShader::Compile(const char *source) {
|
|||||||
source = temp.c_str();
|
source = temp.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
glShaderSource(shader_, 1, &source, 0);
|
glShaderSource(shader_, 1, &source, nullptr);
|
||||||
glCompileShader(shader_);
|
glCompileShader(shader_);
|
||||||
GLint success = 0;
|
GLint success = 0;
|
||||||
glGetShaderiv(shader_, GL_COMPILE_STATUS, &success);
|
glGetShaderiv(shader_, GL_COMPILE_STATUS, &success);
|
||||||
@ -283,6 +291,7 @@ public:
|
|||||||
void Unapply();
|
void Unapply();
|
||||||
void Compile();
|
void Compile();
|
||||||
void GLRestore() override;
|
void GLRestore() override;
|
||||||
|
void GLLost() override;
|
||||||
bool RequiresBuffer() override {
|
bool RequiresBuffer() override {
|
||||||
return id_ != 0;
|
return id_ != 0;
|
||||||
}
|
}
|
||||||
@ -323,6 +332,12 @@ public:
|
|||||||
void SetVector(const char *name, float *value, int n) override;
|
void SetVector(const char *name, float *value, int n) override;
|
||||||
void SetMatrix4x4(const char *name, const float value[16]) override;
|
void SetMatrix4x4(const char *name, const float value[16]) override;
|
||||||
|
|
||||||
|
void GLLost() override {
|
||||||
|
program_ = 0;
|
||||||
|
vshader->Unset();
|
||||||
|
fshader->Unset();
|
||||||
|
}
|
||||||
|
|
||||||
void GLRestore() override {
|
void GLRestore() override {
|
||||||
vshader->Compile(vshader->GetSource().c_str());
|
vshader->Compile(vshader->GetSource().c_str());
|
||||||
fshader->Compile(fshader->GetSource().c_str());
|
fshader->Compile(fshader->GetSource().c_str());
|
||||||
@ -550,15 +565,19 @@ public:
|
|||||||
glBindTexture(target_, tex_);
|
glBindTexture(target_, tex_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLRestore() override {
|
void GLLost() override {
|
||||||
// We can assume that the texture is gone.
|
// We can assume that the texture is gone.
|
||||||
tex_ = 0;
|
tex_ = 0;
|
||||||
generatedMips_ = false;
|
generatedMips_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLRestore() override {
|
||||||
if (!filename_.empty()) {
|
if (!filename_.empty()) {
|
||||||
if (LoadFromFile(filename_.c_str())) {
|
if (LoadFromFile(filename_.c_str())) {
|
||||||
ILOG("Reloaded lost texture %s", filename_.c_str());
|
ILOG("Reloaded lost texture %s", filename_.c_str());
|
||||||
} else {
|
} else {
|
||||||
ELOG("Failed to reload lost texture %s", filename_.c_str());
|
ELOG("Failed to reload lost texture %s", filename_.c_str());
|
||||||
|
tex_ = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
WLOG("Texture %p cannot be restored - has no filename", this);
|
WLOG("Texture %p cannot be restored - has no filename", this);
|
||||||
@ -663,6 +682,10 @@ void Thin3DGLVertexFormat::Compile() {
|
|||||||
lastBase_ = -1;
|
lastBase_ = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Thin3DGLVertexFormat::GLLost() {
|
||||||
|
id_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void Thin3DGLVertexFormat::GLRestore() {
|
void Thin3DGLVertexFormat::GLRestore() {
|
||||||
Compile();
|
Compile();
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,14 @@ void ScreenManager::deviceLost() {
|
|||||||
// TODO: Change this when it becomes necessary.
|
// TODO: Change this when it becomes necessary.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScreenManager::deviceRestore() {
|
||||||
|
for (size_t i = 0; i < stack_.size(); i++) {
|
||||||
|
stack_[i].screen->deviceRestore();
|
||||||
|
}
|
||||||
|
// Dialogs too? Nah, they should only use the standard UI texture anyway.
|
||||||
|
// TODO: Change this when it becomes necessary.
|
||||||
|
}
|
||||||
|
|
||||||
Screen *ScreenManager::topScreen() const {
|
Screen *ScreenManager::topScreen() const {
|
||||||
if (!stack_.empty())
|
if (!stack_.empty())
|
||||||
return stack_.back().screen;
|
return stack_.back().screen;
|
||||||
|
@ -50,6 +50,7 @@ public:
|
|||||||
virtual void render() {}
|
virtual void render() {}
|
||||||
virtual void postRender() {}
|
virtual void postRender() {}
|
||||||
virtual void deviceLost() {}
|
virtual void deviceLost() {}
|
||||||
|
virtual void deviceRestore() {}
|
||||||
virtual void resized() {}
|
virtual void resized() {}
|
||||||
virtual void dialogFinished(const Screen *dialog, DialogResult result) {}
|
virtual void dialogFinished(const Screen *dialog, DialogResult result) {}
|
||||||
virtual bool touch(const TouchInput &touch) { return false; }
|
virtual bool touch(const TouchInput &touch) { return false; }
|
||||||
@ -103,6 +104,7 @@ public:
|
|||||||
void render();
|
void render();
|
||||||
void resized();
|
void resized();
|
||||||
void deviceLost();
|
void deviceLost();
|
||||||
|
void deviceRestore();
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
// Push a dialog box in front. Currently 1-level only.
|
// Push a dialog box in front. Currently 1-level only.
|
||||||
|
Loading…
Reference in New Issue
Block a user