mirror of
https://github.com/panda3d/panda3d.git
synced 2025-09-28 15:53:55 -04:00
glgsg: Implement partial updates of texture arrays / cube maps
Also, simplify the cube map upload mechanism somewhat. Fixes #1168
This commit is contained in:
parent
494631ac54
commit
bfe595f4dc
@ -1084,10 +1084,12 @@ reset() {
|
||||
if (is_at_least_gl_version(4, 4) || has_extension("GL_ARB_clear_texture")) {
|
||||
_glClearTexImage = (PFNGLCLEARTEXIMAGEPROC)
|
||||
get_extension_func("glClearTexImage");
|
||||
_glClearTexSubImage = (PFNGLCLEARTEXSUBIMAGEPROC)
|
||||
get_extension_func("glClearTexSubImage");
|
||||
|
||||
if (_glClearTexImage == nullptr) {
|
||||
if (_glClearTexImage == nullptr || _glClearTexSubImage == nullptr) {
|
||||
GLCAT.warning()
|
||||
<< "GL_ARB_clear_texture advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n";
|
||||
<< "GL_ARB_clear_texture advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
|
||||
} else {
|
||||
_supports_clear_texture = true;
|
||||
}
|
||||
@ -13458,59 +13460,9 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) {
|
||||
}
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
if (texture_type == Texture::TT_cube_map) {
|
||||
// A cube map must load six different 2-d images (which are stored as the
|
||||
// six pages of the system ram image).
|
||||
if (!_supports_cube_map) {
|
||||
report_my_gl_errors();
|
||||
return false;
|
||||
}
|
||||
nassertr(target == GL_TEXTURE_CUBE_MAP, false);
|
||||
|
||||
success = success && upload_texture_image
|
||||
(gtc, needs_reload, uses_mipmaps, mipmap_bias,
|
||||
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_CUBE_MAP_POSITIVE_X,
|
||||
internal_format, external_format, component_type,
|
||||
true, 0, image_compression);
|
||||
|
||||
success = success && upload_texture_image
|
||||
(gtc, needs_reload, uses_mipmaps, mipmap_bias,
|
||||
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
|
||||
internal_format, external_format, component_type,
|
||||
true, 1, image_compression);
|
||||
|
||||
success = success && upload_texture_image
|
||||
(gtc, needs_reload, uses_mipmaps, mipmap_bias,
|
||||
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
|
||||
internal_format, external_format, component_type,
|
||||
true, 2, image_compression);
|
||||
|
||||
success = success && upload_texture_image
|
||||
(gtc, needs_reload, uses_mipmaps, mipmap_bias,
|
||||
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
|
||||
internal_format, external_format, component_type,
|
||||
true, 3, image_compression);
|
||||
|
||||
success = success && upload_texture_image
|
||||
(gtc, needs_reload, uses_mipmaps, mipmap_bias,
|
||||
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
|
||||
internal_format, external_format, component_type,
|
||||
true, 4, image_compression);
|
||||
|
||||
success = success && upload_texture_image
|
||||
(gtc, needs_reload, uses_mipmaps, mipmap_bias,
|
||||
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
|
||||
internal_format, external_format, component_type,
|
||||
true, 5, image_compression);
|
||||
|
||||
} else {
|
||||
// Any other kind of texture can be loaded all at once.
|
||||
success = upload_texture_image
|
||||
bool success = upload_texture_image
|
||||
(gtc, needs_reload, uses_mipmaps, mipmap_bias, target,
|
||||
target, internal_format, external_format,
|
||||
component_type, false, 0, image_compression);
|
||||
}
|
||||
internal_format, external_format, component_type, image_compression);
|
||||
|
||||
if (gtc->_generate_mipmaps && _glGenerateMipmap != nullptr &&
|
||||
!image.is_null()) {
|
||||
@ -13571,21 +13523,13 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) {
|
||||
/**
|
||||
* Loads a texture image, or one page of a cube map image, from system RAM to
|
||||
* texture memory.
|
||||
*
|
||||
* texture_target is normally the same thing as page_target; both represent
|
||||
* the GL target onto which the texture image is loaded, e.g. GL_TEXTURE_1D,
|
||||
* GL_TEXTURE_2D, etc. The only time they may differ is in the case of cube
|
||||
* mapping, in which case texture_target will be target for the overall
|
||||
* texture, e.g. GL_TEXTURE_CUBE_MAP, and page_target will be the target for
|
||||
* this particular page, e.g. GL_TEXTURE_CUBE_MAP_POSITIVE_X.
|
||||
*/
|
||||
bool CLP(GraphicsStateGuardian)::
|
||||
upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
bool uses_mipmaps, int mipmap_bias,
|
||||
GLenum texture_target, GLenum page_target,
|
||||
GLenum texture_target,
|
||||
GLint internal_format,
|
||||
GLint external_format, GLenum component_type,
|
||||
bool one_page_only, int z,
|
||||
Texture::CompressionMode image_compression) {
|
||||
// Make sure the error stack is cleared out before we begin.
|
||||
clear_my_gl_errors();
|
||||
@ -13635,29 +13579,32 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
// saving on texture memory fragmentation.
|
||||
|
||||
if (GLCAT.is_debug()) {
|
||||
SparseArray pages = gtc->get_image_modified_pages(0);
|
||||
if (num_ram_mipmap_levels == 0) {
|
||||
if (tex->has_clear_color()) {
|
||||
GLCAT.debug()
|
||||
<< "clearing texture " << tex->get_name() << ", "
|
||||
<< width << " x " << height << " x " << depth << ", z = " << z
|
||||
<< width << " x " << height << " x " << depth << ", pages " << pages
|
||||
<< ", uses_mipmaps = " << uses_mipmaps << ", clear_color = "
|
||||
<< tex->get_clear_color() << "\n";
|
||||
} else {
|
||||
GLCAT.debug()
|
||||
<< "not loading NULL image for texture " << tex->get_name()
|
||||
<< ", " << width << " x " << height << " x " << depth
|
||||
<< ", z = " << z << ", uses_mipmaps = " << uses_mipmaps << "\n";
|
||||
<< ", pages " << pages << ", uses_mipmaps = " << uses_mipmaps << "\n";
|
||||
}
|
||||
} else {
|
||||
GLCAT.debug()
|
||||
<< "updating image data of texture " << tex->get_name()
|
||||
<< ", " << width << " x " << height << " x " << depth
|
||||
<< ", z = " << z << ", mipmaps " << num_ram_mipmap_levels
|
||||
<< ", pages " << pages << ", mipmaps " << num_ram_mipmap_levels
|
||||
<< ", uses_mipmaps = " << uses_mipmaps << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
for (int n = mipmap_bias; n < num_levels; ++n) {
|
||||
SparseArray pages = gtc->get_image_modified_pages(n);
|
||||
|
||||
// we grab the mipmap pointer first, if it is NULL we grab the normal
|
||||
// mipmap image pointer which is a PTA_uchar
|
||||
const unsigned char *image_ptr = (unsigned char*)tex->get_ram_mipmap_pointer(n);
|
||||
@ -13684,8 +13631,17 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
// function.
|
||||
vector_uchar clear_data = tex->get_clear_data();
|
||||
|
||||
if (pages.has_all_of(0, depth)) {
|
||||
_glClearTexImage(gtc->_index, n - mipmap_bias, external_format,
|
||||
component_type, (void *)&clear_data[0]);
|
||||
}
|
||||
else for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) {
|
||||
int begin = pages.get_subrange_begin(sri);
|
||||
int num_pages = pages.get_subrange_end(sri) - begin;
|
||||
_glClearTexSubImage(gtc->_index, n - mipmap_bias, 0, 0, begin,
|
||||
width, height, num_pages, external_format,
|
||||
component_type, (void *)&clear_data[0]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
@ -13714,14 +13670,11 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
}
|
||||
|
||||
PTA_uchar bgr_image;
|
||||
size_t view_size = tex->get_ram_mipmap_view_size(n);
|
||||
size_t page_size = tex->get_ram_mipmap_page_size(n);
|
||||
if (image_ptr != nullptr) {
|
||||
const unsigned char *orig_image_ptr = image_ptr;
|
||||
size_t view_size = tex->get_ram_mipmap_view_size(n);
|
||||
image_ptr += view_size * gtc->get_view();
|
||||
if (one_page_only) {
|
||||
view_size = tex->get_ram_mipmap_page_size(n);
|
||||
image_ptr += view_size * z;
|
||||
}
|
||||
nassertr(image_ptr >= orig_image_ptr && image_ptr + view_size <= orig_image_ptr + tex->get_ram_mipmap_image_size(n), false);
|
||||
|
||||
if (image_compression == Texture::CM_off) {
|
||||
@ -13734,23 +13687,29 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
|
||||
int width = tex->get_expected_mipmap_x_size(n);
|
||||
int height = tex->get_expected_mipmap_y_size(n);
|
||||
#ifndef OPENGLES_1
|
||||
int depth = tex->get_expected_mipmap_z_size(n);
|
||||
#endif
|
||||
|
||||
#ifdef DO_PSTATS
|
||||
_data_transferred_pcollector.add_level(view_size);
|
||||
_data_transferred_pcollector.add_level(page_size * pages.get_num_on_bits());
|
||||
#endif
|
||||
switch (texture_target) {
|
||||
#ifndef OPENGLES_1
|
||||
case GL_TEXTURE_3D:
|
||||
if (_supports_3d_texture) {
|
||||
for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) {
|
||||
int begin = pages.get_subrange_begin(sri);
|
||||
int num_pages = pages.get_subrange_end(sri) - begin;
|
||||
const unsigned char *page_ptr = image_ptr + page_size * begin;
|
||||
|
||||
if (image_compression == Texture::CM_off) {
|
||||
_glTexSubImage3D(page_target, n - mipmap_bias, 0, 0, 0, width, height, depth,
|
||||
external_format, component_type, image_ptr);
|
||||
_glTexSubImage3D(texture_target, n - mipmap_bias,
|
||||
0, 0, begin, width, height, num_pages,
|
||||
external_format, component_type, page_ptr);
|
||||
} else {
|
||||
_glCompressedTexSubImage3D(page_target, n - mipmap_bias, 0, 0, 0, width, height, depth,
|
||||
external_format, view_size, image_ptr);
|
||||
_glCompressedTexSubImage3D(texture_target, n - mipmap_bias,
|
||||
0, 0, begin, width, height, num_pages,
|
||||
external_format,
|
||||
page_size * num_pages, page_ptr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
report_my_gl_errors();
|
||||
@ -13762,11 +13721,11 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
#ifndef OPENGLES
|
||||
case GL_TEXTURE_1D:
|
||||
if (image_compression == Texture::CM_off) {
|
||||
glTexSubImage1D(page_target, n - mipmap_bias, 0, width,
|
||||
glTexSubImage1D(texture_target, n - mipmap_bias, 0, width,
|
||||
external_format, component_type, image_ptr);
|
||||
} else {
|
||||
_glCompressedTexSubImage1D(page_target, n - mipmap_bias, 0, width,
|
||||
external_format, view_size, image_ptr);
|
||||
_glCompressedTexSubImage1D(texture_target, n - mipmap_bias, 0, width,
|
||||
external_format, page_size, image_ptr);
|
||||
}
|
||||
break;
|
||||
#endif // OPENGLES
|
||||
@ -13775,12 +13734,21 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
if (_supports_2d_texture_array) {
|
||||
for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) {
|
||||
int begin = pages.get_subrange_begin(sri);
|
||||
int num_pages = pages.get_subrange_end(sri) - begin;
|
||||
const unsigned char *page_ptr = image_ptr + page_size * begin;
|
||||
|
||||
if (image_compression == Texture::CM_off) {
|
||||
_glTexSubImage3D(page_target, n - mipmap_bias, 0, 0, 0, width, height, depth,
|
||||
external_format, component_type, image_ptr);
|
||||
_glTexSubImage3D(texture_target, n - mipmap_bias,
|
||||
0, 0, begin, width, height, num_pages,
|
||||
external_format, component_type, page_ptr);
|
||||
} else {
|
||||
_glCompressedTexSubImage3D(page_target, n - mipmap_bias, 0, 0, 0, width, height, depth,
|
||||
external_format, view_size, image_ptr);
|
||||
_glCompressedTexSubImage3D(texture_target, n - mipmap_bias,
|
||||
0, 0, begin, width, height, num_pages,
|
||||
external_format,
|
||||
page_size * num_pages, page_ptr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
report_my_gl_errors();
|
||||
@ -13792,7 +13760,7 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
#ifndef OPENGLES
|
||||
case GL_TEXTURE_BUFFER:
|
||||
if (_supports_buffer_texture) {
|
||||
_glBufferSubData(GL_TEXTURE_BUFFER, 0, view_size, image_ptr);
|
||||
_glBufferSubData(GL_TEXTURE_BUFFER, 0, page_size, image_ptr);
|
||||
} else {
|
||||
report_my_gl_errors();
|
||||
return false;
|
||||
@ -13800,6 +13768,34 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
break;
|
||||
#endif // OPENGLES
|
||||
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
if (_supports_cube_map) {
|
||||
// This is the only texture type that must be specified using separate
|
||||
// per-page calls.
|
||||
if (n == 0) {
|
||||
height = tex->get_y_size() - tex->get_pad_y_size();
|
||||
}
|
||||
for (int z = 0; z < 6; ++z) {
|
||||
if (pages.get_bit(z)) {
|
||||
GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z;
|
||||
const unsigned char *page_ptr = image_ptr + page_size * z;
|
||||
|
||||
if (image_compression == Texture::CM_off) {
|
||||
glTexSubImage2D(page_target, n - mipmap_bias, 0, 0, width, height,
|
||||
external_format, component_type, page_ptr);
|
||||
} else {
|
||||
_glCompressedTexSubImage2D(page_target, n - mipmap_bias,
|
||||
0, 0, width, height,
|
||||
external_format, page_size, page_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
report_my_gl_errors();
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (image_compression == Texture::CM_off) {
|
||||
if (n == 0) {
|
||||
@ -13807,11 +13803,11 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
// TexSubImage2D doesn't accept a row-stride parameter.
|
||||
height = tex->get_y_size() - tex->get_pad_y_size();
|
||||
}
|
||||
glTexSubImage2D(page_target, n - mipmap_bias, 0, 0, width, height,
|
||||
glTexSubImage2D(texture_target, n - mipmap_bias, 0, 0, width, height,
|
||||
external_format, component_type, image_ptr);
|
||||
} else {
|
||||
_glCompressedTexSubImage2D(page_target, n - mipmap_bias, 0, 0, width, height,
|
||||
external_format, view_size, image_ptr);
|
||||
_glCompressedTexSubImage2D(texture_target, n - mipmap_bias, 0, 0, width, height,
|
||||
external_format, page_size, image_ptr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -13835,7 +13831,7 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
if (GLCAT.is_debug()) {
|
||||
GLCAT.debug()
|
||||
<< "loading new texture object for " << tex->get_name() << ", " << width
|
||||
<< " x " << height << " x " << depth << ", z = " << z << ", mipmaps "
|
||||
<< " x " << height << " x " << depth << ", mipmaps "
|
||||
<< num_ram_mipmap_levels << ", uses_mipmaps = " << uses_mipmaps << "\n";
|
||||
}
|
||||
|
||||
@ -13900,10 +13896,6 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
if (image_ptr != nullptr) {
|
||||
const unsigned char *orig_image_ptr = image_ptr;
|
||||
image_ptr += view_size * gtc->get_view();
|
||||
if (one_page_only) {
|
||||
view_size = tex->get_ram_mipmap_page_size(n);
|
||||
image_ptr += view_size * z;
|
||||
}
|
||||
nassertr(image_ptr >= orig_image_ptr && image_ptr + view_size <= orig_image_ptr + tex->get_ram_mipmap_image_size(n), false);
|
||||
|
||||
if (image_compression == Texture::CM_off) {
|
||||
@ -13927,12 +13919,11 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
#ifndef OPENGLES // 1-d textures not supported by OpenGL ES. Fall through.
|
||||
case GL_TEXTURE_1D:
|
||||
if (image_compression == Texture::CM_off) {
|
||||
glTexImage1D(page_target, n - mipmap_bias, internal_format,
|
||||
width, 0,
|
||||
external_format, component_type, image_ptr);
|
||||
glTexImage1D(texture_target, n - mipmap_bias, internal_format,
|
||||
width, 0, external_format, component_type, image_ptr);
|
||||
} else {
|
||||
_glCompressedTexImage1D(page_target, n - mipmap_bias, external_format, width,
|
||||
0, view_size, image_ptr);
|
||||
_glCompressedTexImage1D(texture_target, n - mipmap_bias, external_format,
|
||||
width, 0, view_size, image_ptr);
|
||||
}
|
||||
break;
|
||||
#endif // OPENGLES // OpenGL ES will fall through.
|
||||
@ -13941,13 +13932,12 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
case GL_TEXTURE_3D:
|
||||
if (_supports_3d_texture) {
|
||||
if (image_compression == Texture::CM_off) {
|
||||
_glTexImage3D(page_target, n - mipmap_bias, internal_format,
|
||||
_glTexImage3D(texture_target, n - mipmap_bias, internal_format,
|
||||
width, height, depth, 0,
|
||||
external_format, component_type, image_ptr);
|
||||
} else {
|
||||
_glCompressedTexImage3D(page_target, n - mipmap_bias, external_format, width,
|
||||
height, depth,
|
||||
0, view_size, image_ptr);
|
||||
_glCompressedTexImage3D(texture_target, n - mipmap_bias, external_format,
|
||||
width, height, depth, 0, view_size, image_ptr);
|
||||
}
|
||||
} else {
|
||||
report_my_gl_errors();
|
||||
@ -13961,13 +13951,12 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
if (_supports_2d_texture_array) {
|
||||
if (image_compression == Texture::CM_off) {
|
||||
_glTexImage3D(page_target, n - mipmap_bias, internal_format,
|
||||
_glTexImage3D(texture_target, n - mipmap_bias, internal_format,
|
||||
width, height, depth, 0,
|
||||
external_format, component_type, image_ptr);
|
||||
} else {
|
||||
_glCompressedTexImage3D(page_target, n - mipmap_bias, external_format, width,
|
||||
height, depth,
|
||||
0, view_size, image_ptr);
|
||||
_glCompressedTexImage3D(texture_target, n - mipmap_bias, external_format,
|
||||
width, height, depth, 0, view_size, image_ptr);
|
||||
}
|
||||
} else {
|
||||
report_my_gl_errors();
|
||||
@ -13988,13 +13977,37 @@ upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
break;
|
||||
#endif // OPENGLES
|
||||
|
||||
default:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
if (_supports_cube_map) {
|
||||
// This is the only texture type that must be specified using separate
|
||||
// per-page calls.
|
||||
size_t page_size = tex->get_ram_mipmap_page_size(n);
|
||||
for (int z = 0; z < 6; ++z) {
|
||||
GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z;
|
||||
const unsigned char *page_ptr = image_ptr + page_size * z;
|
||||
|
||||
if (image_compression == Texture::CM_off) {
|
||||
glTexImage2D(page_target, n - mipmap_bias, internal_format,
|
||||
width, height, 0,
|
||||
external_format, component_type, image_ptr);
|
||||
external_format, component_type, page_ptr);
|
||||
} else {
|
||||
_glCompressedTexImage2D(page_target, n - mipmap_bias, external_format,
|
||||
width, height, 0, page_size, page_ptr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
report_my_gl_errors();
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (image_compression == Texture::CM_off) {
|
||||
glTexImage2D(texture_target, n - mipmap_bias, internal_format,
|
||||
width, height, 0,
|
||||
external_format, component_type, image_ptr);
|
||||
} else {
|
||||
_glCompressedTexImage2D(texture_target, n - mipmap_bias, external_format,
|
||||
width, height, 0, view_size, image_ptr);
|
||||
}
|
||||
}
|
||||
|
@ -616,10 +616,9 @@ protected:
|
||||
bool upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps);
|
||||
bool upload_texture_image(CLP(TextureContext) *gtc, bool needs_reload,
|
||||
bool uses_mipmaps, int mipmap_bias,
|
||||
GLenum texture_target, GLenum page_target,
|
||||
GLenum texture_target,
|
||||
GLint internal_format, GLint external_format,
|
||||
GLenum component_type,
|
||||
bool one_page_only, int z,
|
||||
Texture::CompressionMode image_compression);
|
||||
void generate_mipmaps(CLP(TextureContext) *gtc);
|
||||
bool upload_simple_texture(CLP(TextureContext) *gtc);
|
||||
@ -808,6 +807,7 @@ public:
|
||||
bool _supports_clear_texture;
|
||||
#ifndef OPENGLES
|
||||
PFNGLCLEARTEXIMAGEPROC _glClearTexImage;
|
||||
PFNGLCLEARTEXSUBIMAGEPROC _glClearTexSubImage;
|
||||
#endif
|
||||
|
||||
bool _supports_clear_buffer;
|
||||
|
Loading…
x
Reference in New Issue
Block a user