mirror of
https://github.com/projectM-visualizer/projectm.git
synced 2026-02-04 16:05:28 +00:00
Add texture burn-in support
Also use shader cache in texture copier and a transformation matrix for flipping instead of flags.
This commit is contained in:
@ -278,6 +278,9 @@ if(CMAKE_SYSTEM_NAME STREQUAL Emscripten)
|
||||
endif()
|
||||
message(STATUS " Use system GLM: ${ENABLE_SYSTEM_GLM}")
|
||||
message(STATUS " Use system projectM-eval: ${ENABLE_SYSTEM_PROJECTM_EVAL}")
|
||||
if(ENABLE_SYSTEM_PROJECTM_EVAL)
|
||||
message(STATUS " projectM-eval version: ${projectM-Eval_VERSION}")
|
||||
endif()
|
||||
message(STATUS " Link UI with shared lib: ${ENABLE_SHARED_LINKING}")
|
||||
message(STATUS "")
|
||||
message(STATUS "Targets and applications:")
|
||||
|
||||
@ -105,7 +105,7 @@ void MilkdropPreset::RenderFrame(const libprojectM::Audio::FrameAudioData& audio
|
||||
}
|
||||
|
||||
// y-flip the previous frame and assign the flipped texture as "main"
|
||||
m_flipTexture.Draw(m_framebuffer.GetColorAttachmentTexture(m_previousFrameBuffer, 0), nullptr, true, false);
|
||||
m_flipTexture.Draw(*renderContext.shaderCache, m_framebuffer.GetColorAttachmentTexture(m_previousFrameBuffer, 0), nullptr, true, false);
|
||||
m_state.mainTexture = m_flipTexture.Texture();
|
||||
|
||||
// We now draw to the current framebuffer.
|
||||
@ -146,7 +146,7 @@ void MilkdropPreset::RenderFrame(const libprojectM::Audio::FrameAudioData& audio
|
||||
m_border.Draw(m_perFrameContext);
|
||||
|
||||
// y-flip the image for final compositing again
|
||||
m_flipTexture.Draw(m_framebuffer.GetColorAttachmentTexture(m_currentFrameBuffer, 0), nullptr, true, false);
|
||||
m_flipTexture.Draw(*renderContext.shaderCache, m_framebuffer.GetColorAttachmentTexture(m_currentFrameBuffer, 0), nullptr, true, false);
|
||||
m_state.mainTexture = m_flipTexture.Texture();
|
||||
|
||||
// We no longer need the previous frame image, use it to render the final composite.
|
||||
@ -158,7 +158,7 @@ void MilkdropPreset::RenderFrame(const libprojectM::Audio::FrameAudioData& audio
|
||||
if (!m_finalComposite.HasCompositeShader())
|
||||
{
|
||||
// Flip texture again in "previous" framebuffer as old-school effects are still upside down.
|
||||
m_flipTexture.Draw(m_framebuffer.GetColorAttachmentTexture(m_previousFrameBuffer, 0), m_framebuffer, m_previousFrameBuffer, true, false);
|
||||
m_flipTexture.Draw(*renderContext.shaderCache, m_framebuffer.GetColorAttachmentTexture(m_previousFrameBuffer, 0), m_framebuffer, m_previousFrameBuffer, true, false);
|
||||
}
|
||||
|
||||
// Swap framebuffer IDs for the next frame.
|
||||
@ -178,7 +178,7 @@ void MilkdropPreset::DrawInitialImage(const std::shared_ptr<Renderer::Texture>&
|
||||
m_framebuffer.SetSize(renderContext.viewportSizeX, renderContext.viewportSizeY);
|
||||
|
||||
// Render to previous framebuffer, as this is the image used to draw the next frame on.
|
||||
m_flipTexture.Draw(image, m_framebuffer, m_previousFrameBuffer);
|
||||
m_flipTexture.Draw(*renderContext.shaderCache, image, m_framebuffer, m_previousFrameBuffer);
|
||||
}
|
||||
|
||||
void MilkdropPreset::BindFramebuffer()
|
||||
|
||||
@ -29,8 +29,8 @@
|
||||
|
||||
#include <Renderer/CopyTexture.hpp>
|
||||
#include <Renderer/PresetTransition.hpp>
|
||||
#include <Renderer/TextureManager.hpp>
|
||||
#include <Renderer/ShaderCache.hpp>
|
||||
#include <Renderer/TextureManager.hpp>
|
||||
#include <Renderer/TransitionShaderManager.hpp>
|
||||
|
||||
#include <UserSprites/SpriteManager.hpp>
|
||||
@ -177,11 +177,11 @@ void ProjectM::RenderFrame(uint32_t targetFramebufferObject /*= 0*/)
|
||||
}
|
||||
else
|
||||
{
|
||||
m_textureCopier->Draw(m_activePreset->OutputTexture(), false, false);
|
||||
m_textureCopier->Draw(*renderContext.shaderCache, m_activePreset->OutputTexture(), false, false);
|
||||
}
|
||||
|
||||
// Draw user sprites
|
||||
m_spriteManager->Draw(audioData, renderContext, targetFramebufferObject, { m_activePreset, m_transitioningPreset });
|
||||
m_spriteManager->Draw(audioData, renderContext, targetFramebufferObject, {m_activePreset, m_transitioningPreset});
|
||||
|
||||
m_frameCount++;
|
||||
m_previousFrameVolume = audioData.vol;
|
||||
@ -312,6 +312,23 @@ auto ProjectM::UserSpriteIdentifiers() const -> std::vector<uint32_t>
|
||||
return m_spriteManager->ActiveSpriteIdentifiers();
|
||||
}
|
||||
|
||||
void ProjectM::BurnInTexture(uint32_t openGlTextureId, int left, int top, int width, int height)
|
||||
{
|
||||
if (m_activePreset)
|
||||
{
|
||||
m_activePreset->BindFramebuffer();
|
||||
m_textureCopier->Draw(*m_shaderCache, openGlTextureId, m_windowWidth, m_windowHeight, left, top, width, height);
|
||||
}
|
||||
|
||||
if (m_transitioningPreset)
|
||||
{
|
||||
m_transitioningPreset->BindFramebuffer();
|
||||
m_textureCopier->Draw(*m_shaderCache, openGlTextureId, m_windowWidth, m_windowHeight, left, top, width, height);
|
||||
}
|
||||
|
||||
Renderer::Framebuffer::Unbind();
|
||||
}
|
||||
|
||||
void ProjectM::SetPresetLocked(bool locked)
|
||||
{
|
||||
// ToDo: Add a preset switch timer separate from the display timer and reset to 0 when
|
||||
|
||||
@ -246,6 +246,16 @@ public:
|
||||
*/
|
||||
auto UserSpriteIdentifiers() const -> std::vector<uint32_t>;
|
||||
|
||||
/**
|
||||
* @brief Draws the given texture on the active preset's main texture to get a "burn-in" effect.
|
||||
* @param openGlTextureId The OpenGL texture to draw onto the active preset(s).
|
||||
* @param left Left coordinate in pixels on the destination texture.
|
||||
* @param top Top coordinate in pixels on the destination texture.
|
||||
* @param width Width of the final image on the destination texture in pixels, can be negative to flip it horizontally.
|
||||
* @param height Height of the final image on the destination texture in pixels, can be negative to flip it vertically.
|
||||
*/
|
||||
void BurnInTexture(uint32_t openGlTextureId, int left, int top, int width, int height);
|
||||
|
||||
private:
|
||||
void Initialize();
|
||||
|
||||
|
||||
@ -17,19 +17,11 @@ layout(location = 2) in vec2 tex_coord;
|
||||
|
||||
out vec2 fragment_tex_coord;
|
||||
|
||||
uniform ivec2 flip;
|
||||
uniform mat4 vertex_transformation;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
gl_Position = vec4(position, 0.0, 1.0) * vertex_transformation;
|
||||
fragment_tex_coord = tex_coord;
|
||||
if (flip.x > 0)
|
||||
{
|
||||
fragment_tex_coord.s = 1.0 - fragment_tex_coord.s;
|
||||
}
|
||||
if (flip.y > 0)
|
||||
{
|
||||
fragment_tex_coord.t = 1.0 - fragment_tex_coord.t;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
@ -53,13 +45,6 @@ CopyTexture::CopyTexture()
|
||||
{
|
||||
m_framebuffer.CreateColorAttachment(0, 0);
|
||||
|
||||
std::string vertexShader(ShaderVersion);
|
||||
std::string fragmentShader(ShaderVersion);
|
||||
vertexShader.append(CopyTextureVertexShader);
|
||||
fragmentShader.append(CopyTextureFragmentShader);
|
||||
|
||||
m_shader.CompileProgram(vertexShader, fragmentShader);
|
||||
|
||||
m_mesh.SetRenderPrimitiveType(Mesh::PrimitiveType::TriangleStrip);
|
||||
|
||||
m_mesh.SetVertexCount(4);
|
||||
@ -78,7 +63,9 @@ CopyTexture::CopyTexture()
|
||||
m_mesh.Update();
|
||||
}
|
||||
|
||||
void CopyTexture::Draw(const std::shared_ptr<class Texture>& originalTexture, bool flipVertical, bool flipHorizontal)
|
||||
void CopyTexture::Draw(ShaderCache& shaderCache,
|
||||
const std::shared_ptr<class Texture>& originalTexture,
|
||||
bool flipVertical, bool flipHorizontal)
|
||||
{
|
||||
if (originalTexture == nullptr)
|
||||
{
|
||||
@ -87,10 +74,12 @@ void CopyTexture::Draw(const std::shared_ptr<class Texture>& originalTexture, bo
|
||||
|
||||
// Just bind the texture and draw it to the currently bound buffer.
|
||||
originalTexture->Bind(0);
|
||||
Copy(flipVertical, flipHorizontal);
|
||||
Copy(shaderCache, flipVertical, flipHorizontal);
|
||||
}
|
||||
|
||||
void CopyTexture::Draw(const std::shared_ptr<class Texture>& originalTexture, const std::shared_ptr<class Texture>& targetTexture,
|
||||
void CopyTexture::Draw(ShaderCache& shaderCache,
|
||||
const std::shared_ptr<class Texture>& originalTexture,
|
||||
const std::shared_ptr<class Texture>& targetTexture,
|
||||
bool flipVertical, bool flipHorizontal)
|
||||
{
|
||||
if (originalTexture == nullptr ||
|
||||
@ -128,7 +117,7 @@ void CopyTexture::Draw(const std::shared_ptr<class Texture>& originalTexture, co
|
||||
m_framebuffer.GetAttachment(0, TextureAttachment::AttachmentType::Color, 0)->Texture(targetTexture);
|
||||
}
|
||||
|
||||
Copy(flipVertical, flipHorizontal);
|
||||
Copy(shaderCache, flipVertical, flipHorizontal);
|
||||
|
||||
// Rebind our internal texture.
|
||||
if (targetTexture)
|
||||
@ -139,7 +128,9 @@ void CopyTexture::Draw(const std::shared_ptr<class Texture>& originalTexture, co
|
||||
Framebuffer::Unbind();
|
||||
}
|
||||
|
||||
void CopyTexture::Draw(const std::shared_ptr<class Texture>& originalTexture, Framebuffer& framebuffer, int framebufferIndex,
|
||||
void CopyTexture::Draw(ShaderCache& shaderCache,
|
||||
const std::shared_ptr<class Texture>& originalTexture,
|
||||
Framebuffer& framebuffer, int framebufferIndex,
|
||||
bool flipVertical, bool flipHorizontal)
|
||||
{
|
||||
if (originalTexture == nullptr //
|
||||
@ -162,7 +153,7 @@ void CopyTexture::Draw(const std::shared_ptr<class Texture>& originalTexture, Fr
|
||||
// Draw from unflipped texture
|
||||
originalTexture->Bind(0);
|
||||
|
||||
Copy(flipVertical, flipHorizontal);
|
||||
Copy(shaderCache, flipVertical, flipHorizontal);
|
||||
|
||||
// Swap texture attachments
|
||||
auto tempAttachment = framebuffer.GetAttachment(framebufferIndex, TextureAttachment::AttachmentType::Color, 0);
|
||||
@ -174,6 +165,74 @@ void CopyTexture::Draw(const std::shared_ptr<class Texture>& originalTexture, Fr
|
||||
Framebuffer::Unbind();
|
||||
}
|
||||
|
||||
void CopyTexture::Draw(ShaderCache& shaderCache,
|
||||
const std::shared_ptr<struct Texture>& originalTexture,
|
||||
const std::shared_ptr<struct Texture>& targetTexture,
|
||||
int left, int top, int width, int height)
|
||||
{
|
||||
if (originalTexture == nullptr ||
|
||||
originalTexture->Empty() ||
|
||||
targetTexture == nullptr ||
|
||||
targetTexture->Empty() ||
|
||||
originalTexture == targetTexture)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateTextureSize(targetTexture->Width(), targetTexture->Height());
|
||||
|
||||
if (m_width == 0 || m_height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<class Texture> internalTexture;
|
||||
|
||||
m_framebuffer.Bind(0);
|
||||
|
||||
// Draw from original texture
|
||||
originalTexture->Bind(0);
|
||||
internalTexture = m_framebuffer.GetColorAttachmentTexture(0, 0);
|
||||
m_framebuffer.GetAttachment(0, TextureAttachment::AttachmentType::Color, 0)->Texture(targetTexture);
|
||||
|
||||
Copy(shaderCache, left, top, width, height);
|
||||
|
||||
// Rebind our internal texture.
|
||||
m_framebuffer.GetAttachment(0, TextureAttachment::AttachmentType::Color, 0)->Texture(internalTexture);
|
||||
|
||||
Framebuffer::Unbind();
|
||||
}
|
||||
|
||||
void CopyTexture::Draw(ShaderCache& shaderCache,
|
||||
GLuint originalTexture,
|
||||
int viewportWidth, int viewportHeight,
|
||||
int left, int top, int width, int height)
|
||||
{
|
||||
if (originalTexture == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewportWidth == 0 || viewportHeight == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int oldWidth = m_width;
|
||||
int oldHeight = m_height;
|
||||
|
||||
m_width = viewportWidth;
|
||||
m_height = viewportHeight;
|
||||
|
||||
// Draw from original texture
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, originalTexture);
|
||||
Copy(shaderCache, left, top, width, height);
|
||||
|
||||
m_width = oldWidth;
|
||||
m_height = oldHeight;
|
||||
}
|
||||
|
||||
auto CopyTexture::Texture() -> std::shared_ptr<class Texture>
|
||||
{
|
||||
return m_framebuffer.GetColorAttachmentTexture(0, 0);
|
||||
@ -193,11 +252,18 @@ void CopyTexture::UpdateTextureSize(int width, int height)
|
||||
m_framebuffer.SetSize(m_width, m_height);
|
||||
}
|
||||
|
||||
void CopyTexture::Copy(bool flipVertical, bool flipHorizontal)
|
||||
void CopyTexture::Copy(ShaderCache& shaderCache,
|
||||
bool flipVertical, bool flipHorizontal)
|
||||
{
|
||||
m_shader.Bind();
|
||||
m_shader.SetUniformInt("texture_sampler", 0);
|
||||
m_shader.SetUniformInt2("flip", {flipHorizontal ? 1 : 0, flipVertical ? 1 : 0});
|
||||
glm::mat4x4 flipMatrix(1.0);
|
||||
|
||||
flipMatrix[0][0] = flipHorizontal ? -1.0 : 1.0;
|
||||
flipMatrix[1][1] = flipVertical ? -1.0 : 1.0;
|
||||
|
||||
std::shared_ptr<Shader> shader = BindShader(shaderCache);
|
||||
|
||||
shader->SetUniformInt("texture_sampler", 0);
|
||||
shader->SetUniformMat4x4("vertex_transformation", flipMatrix);
|
||||
|
||||
m_sampler.Bind(0);
|
||||
|
||||
@ -209,5 +275,57 @@ void CopyTexture::Copy(bool flipVertical, bool flipHorizontal)
|
||||
Shader::Unbind();
|
||||
}
|
||||
|
||||
void CopyTexture::Copy(ShaderCache& shaderCache,
|
||||
int left, int top, int width, int height)
|
||||
{
|
||||
glm::mat4x4 translationMatrix(1.0);
|
||||
translationMatrix[0][0] = static_cast<float>(width) / static_cast<float>(m_width);
|
||||
translationMatrix[1][1] = static_cast<float>(height) / static_cast<float>(m_height);
|
||||
|
||||
translationMatrix[3][0] = static_cast<float>(left) / static_cast<float>(m_width);
|
||||
translationMatrix[3][1] = static_cast<float>(top) / static_cast<float>(m_height);
|
||||
|
||||
std::shared_ptr<Shader> shader = BindShader(shaderCache);
|
||||
|
||||
shader->SetUniformInt("texture_sampler", 0);
|
||||
shader->SetUniformMat4x4("vertex_transformation", translationMatrix);
|
||||
|
||||
m_sampler.Bind(0);
|
||||
|
||||
m_mesh.Draw();
|
||||
|
||||
Mesh::Unbind();
|
||||
Sampler::Unbind(0);
|
||||
Shader::Unbind();
|
||||
}
|
||||
|
||||
std::shared_ptr<Shader> CopyTexture::BindShader(ShaderCache& shaderCache)
|
||||
{
|
||||
auto shader = m_shader.lock();
|
||||
|
||||
if (!shader)
|
||||
{
|
||||
shader = shaderCache.Get("copy_texture");
|
||||
}
|
||||
|
||||
if (!shader)
|
||||
{
|
||||
std::string vertexShader(ShaderVersion);
|
||||
std::string fragmentShader(ShaderVersion);
|
||||
vertexShader.append(CopyTextureVertexShader);
|
||||
fragmentShader.append(CopyTextureFragmentShader);
|
||||
|
||||
shader = std::make_shared<Shader>();
|
||||
shader->CompileProgram(vertexShader, fragmentShader);
|
||||
|
||||
m_shader = shader;
|
||||
shaderCache.Insert("copy_texture", shader);
|
||||
}
|
||||
|
||||
shader->Bind();
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
} // namespace Renderer
|
||||
} // namespace libprojectM
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
#include "Renderer/Framebuffer.hpp"
|
||||
#include "Renderer/Mesh.hpp"
|
||||
#include "Renderer/Shader.hpp"
|
||||
#include "Renderer/ShaderCache.hpp"
|
||||
|
||||
namespace libprojectM {
|
||||
namespace Renderer {
|
||||
@ -20,37 +20,76 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Copies the original texture into the currently bound framebuffer.
|
||||
* @param shaderCache The global shader cache instance.
|
||||
* @param originalTexture The texture to be copied.
|
||||
* @param flipVertical Flip image on the y-axis when copying.
|
||||
* @param flipHorizontal Flip image on the x-axis when copying.
|
||||
*/
|
||||
void Draw(const std::shared_ptr<class Texture>& originalTexture,
|
||||
void Draw(ShaderCache& shaderCache,
|
||||
const std::shared_ptr<Texture>& originalTexture,
|
||||
bool flipVertical = false, bool flipHorizontal = false);
|
||||
|
||||
/**
|
||||
* @brief Copies the original texture either into the object's internal framebuffer or a given target texture.
|
||||
* The original and target textures must not be the same.
|
||||
* @param shaderCache The global shader cache instance.
|
||||
* @param originalTexture The texture to be copied.
|
||||
* @param targetTexture Optional target texture to draw onto.
|
||||
* @param flipVertical Flip image on the y-axis when copying.
|
||||
* @param flipHorizontal Flip image on the x-axis when copying.
|
||||
*/
|
||||
void Draw(const std::shared_ptr<class Texture>& originalTexture, const std::shared_ptr<class Texture>& targetTexture = {},
|
||||
void Draw(ShaderCache& shaderCache,
|
||||
const std::shared_ptr<Texture>& originalTexture,
|
||||
const std::shared_ptr<Texture>& targetTexture = {},
|
||||
bool flipVertical = false, bool flipHorizontal = false);
|
||||
|
||||
/**
|
||||
* @brief Copies the texture bound the given framebuffer's first color attachment.
|
||||
* This is done by drawing into a second framebuffer, then swapping the textures, so the original texture
|
||||
* can be the current color attachment of targetFramebuffer.
|
||||
* @param shaderCache The global shader cache instance.
|
||||
* @param originalTexture The texture to be copied.
|
||||
* @param targetFramebuffer Optional target texture to draw onto.
|
||||
* @param framebufferIndex The index of the framebuffer to use.
|
||||
* @param flipVertical Flip image on the y-axis when copying.
|
||||
* @param flipHorizontal Flip image on the x-axis when copying.
|
||||
*/
|
||||
void Draw(const std::shared_ptr<class Texture>& originalTexture, Framebuffer& framebuffer, int framebufferIndex,
|
||||
void Draw(ShaderCache& shaderCache,
|
||||
const std::shared_ptr<Texture>& originalTexture,
|
||||
Framebuffer& framebuffer, int framebufferIndex,
|
||||
bool flipVertical = false, bool flipHorizontal = false);
|
||||
|
||||
/**
|
||||
* @brief Draws the original texture onto the specified target texture, using the provided screen coordinates to position it.
|
||||
* @param shaderCache The global shader cache instance.
|
||||
* @param originalTexture The texture to be copied.
|
||||
* @param targetTexture The target texture to draw onto.
|
||||
* @param left Left offset on the target texture in screen coordinates.
|
||||
* @param top Top offset on the target texture in screen coordinates.
|
||||
* @param width Width on the target texture in screen coordinates. Use a negative value to flip vertically.
|
||||
* @param height Height on the target texture in screen coordinates. Use a negative value to flip horizontally.
|
||||
*/
|
||||
void Draw(ShaderCache& shaderCache,
|
||||
const std::shared_ptr<Texture>& originalTexture,
|
||||
const std::shared_ptr<Texture>& targetTexture,
|
||||
int left, int top, int width, int height);
|
||||
|
||||
/**
|
||||
* @brief Draws a raw GL texture into the currently bound framebuffer, using the provided screen coordinates to position it.
|
||||
* @param shaderCache The global shader cache instance.
|
||||
* @param originalTexture The texture ID to be copied.
|
||||
* @param viewportWidth The target surface width.
|
||||
* @param viewportHeight The target surface height.
|
||||
* @param left Left offset on the target texture in screen coordinates.
|
||||
* @param top Top offset on the target texture in screen coordinates.
|
||||
* @param width Width on the target texture in screen coordinates. Use a negative value to flip vertically.
|
||||
* @param height Height on the target texture in screen coordinates. Use a negative value to flip horizontally.
|
||||
*/
|
||||
void Draw(ShaderCache& shaderCache,
|
||||
GLuint originalTexture,
|
||||
int viewportWidth, int viewportHeight,
|
||||
int left, int top, int width, int height);
|
||||
|
||||
/**
|
||||
* @brief Returns the flipped texture.
|
||||
*
|
||||
@ -64,15 +103,20 @@ private:
|
||||
*/
|
||||
void UpdateTextureSize(int width, int height);
|
||||
|
||||
void Copy(bool flipVertical, bool flipHorizontal);
|
||||
void Copy(ShaderCache& shaderCache,
|
||||
bool flipVertical, bool flipHorizontal);
|
||||
|
||||
void Copy(ShaderCache& shaderCache,
|
||||
int left, int top, int width, int height);
|
||||
|
||||
Mesh m_mesh;
|
||||
Shader m_shader; //!< Simple textured shader
|
||||
std::weak_ptr<Shader> m_shader; //!< Simple textured shader
|
||||
Framebuffer m_framebuffer{1}; //!< Framebuffer for drawing the flipped texture
|
||||
Sampler m_sampler{GL_CLAMP_TO_EDGE, GL_NEAREST}; //!< Texture sampler settings
|
||||
|
||||
int m_width{}; //!< Last known framebuffer/texture width
|
||||
int m_height{}; //!< Last known framebuffer/texture height
|
||||
std::shared_ptr<Shader> BindShader(ShaderCache& shaderCache);
|
||||
};
|
||||
|
||||
} // namespace Renderer
|
||||
|
||||
Reference in New Issue
Block a user