clutter/stage-view: Remove unused double buffered shadow fb
It was for a failed expriment that tried to mmap() dmabuf memory and find damaged regions to decrease the amount that was eventually used to write to an onscreen, but mmap:ing is only fast enough on intel, and it's only relevant on various server GPUs. For it to be achievable, we need to render to system memory in a way that we don't need to copy it out of OpenGL, but that's currently not possible. Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/3443>
This commit is contained in:
parent
d8ff97ebf6
commit
7152779ae0
@ -74,12 +74,6 @@ typedef struct _ClutterStageViewPrivate
|
|||||||
|
|
||||||
gboolean use_shadowfb;
|
gboolean use_shadowfb;
|
||||||
struct {
|
struct {
|
||||||
struct {
|
|
||||||
CoglDmaBufHandle *handles[2];
|
|
||||||
int current_idx;
|
|
||||||
ClutterDamageHistory *damage_history;
|
|
||||||
} dma_buf;
|
|
||||||
|
|
||||||
CoglOffscreen *framebuffer;
|
CoglOffscreen *framebuffer;
|
||||||
} shadow;
|
} shadow;
|
||||||
|
|
||||||
@ -312,68 +306,6 @@ paint_transformed_framebuffer (ClutterStageView *view,
|
|||||||
cogl_framebuffer_pop_matrix (dst_framebuffer);
|
cogl_framebuffer_pop_matrix (dst_framebuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
|
||||||
is_shadowfb_double_buffered (ClutterStageView *view)
|
|
||||||
{
|
|
||||||
ClutterStageViewPrivate *priv =
|
|
||||||
clutter_stage_view_get_instance_private (view);
|
|
||||||
|
|
||||||
return priv->shadow.dma_buf.handles[0] && priv->shadow.dma_buf.handles[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
init_dma_buf_shadowfbs (ClutterStageView *view,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
GError **error)
|
|
||||||
{
|
|
||||||
ClutterStageViewPrivate *priv =
|
|
||||||
clutter_stage_view_get_instance_private (view);
|
|
||||||
CoglContext *cogl_context = cogl_framebuffer_get_context (priv->framebuffer);
|
|
||||||
CoglRenderer *cogl_renderer = cogl_context_get_renderer (cogl_context);
|
|
||||||
CoglFramebuffer *initial_shadowfb;
|
|
||||||
|
|
||||||
if (!cogl_clutter_winsys_has_feature (COGL_WINSYS_FEATURE_BUFFER_AGE))
|
|
||||||
{
|
|
||||||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
|
|
||||||
"Buffer age not supported");
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!COGL_IS_ONSCREEN (priv->framebuffer))
|
|
||||||
{
|
|
||||||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
|
|
||||||
"Tried to use shadow buffer without onscreen");
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
priv->shadow.dma_buf.handles[0] = cogl_renderer_create_dma_buf (cogl_renderer,
|
|
||||||
COGL_PIXEL_FORMAT_BGRX_8888,
|
|
||||||
width, height,
|
|
||||||
error);
|
|
||||||
if (!priv->shadow.dma_buf.handles[0])
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
priv->shadow.dma_buf.handles[1] = cogl_renderer_create_dma_buf (cogl_renderer,
|
|
||||||
COGL_PIXEL_FORMAT_BGRX_8888,
|
|
||||||
width, height,
|
|
||||||
error);
|
|
||||||
if (!priv->shadow.dma_buf.handles[1])
|
|
||||||
{
|
|
||||||
g_clear_pointer (&priv->shadow.dma_buf.handles[0],
|
|
||||||
cogl_dma_buf_handle_free);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
priv->shadow.dma_buf.damage_history = clutter_damage_history_new ();
|
|
||||||
|
|
||||||
initial_shadowfb =
|
|
||||||
cogl_dma_buf_handle_get_framebuffer (priv->shadow.dma_buf.handles[0]);
|
|
||||||
priv->shadow.framebuffer = COGL_OFFSCREEN (g_object_ref (initial_shadowfb));
|
|
||||||
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static CoglOffscreen *
|
static CoglOffscreen *
|
||||||
create_offscreen_framebuffer (ClutterStageView *view,
|
create_offscreen_framebuffer (ClutterStageView *view,
|
||||||
int width,
|
int width,
|
||||||
@ -407,24 +339,6 @@ create_offscreen_framebuffer (ClutterStageView *view,
|
|||||||
return framebuffer;
|
return framebuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
|
||||||
init_fallback_shadowfb (ClutterStageView *view,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
GError **error)
|
|
||||||
{
|
|
||||||
ClutterStageViewPrivate *priv =
|
|
||||||
clutter_stage_view_get_instance_private (view);
|
|
||||||
CoglOffscreen *offscreen;
|
|
||||||
|
|
||||||
offscreen = create_offscreen_framebuffer (view, width, height, error);
|
|
||||||
if (!offscreen)
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
priv->shadow.framebuffer = offscreen;
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
init_shadowfb (ClutterStageView *view)
|
init_shadowfb (ClutterStageView *view)
|
||||||
{
|
{
|
||||||
@ -433,33 +347,20 @@ init_shadowfb (ClutterStageView *view)
|
|||||||
g_autoptr (GError) error = NULL;
|
g_autoptr (GError) error = NULL;
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
|
CoglOffscreen *offscreen;
|
||||||
|
|
||||||
width = cogl_framebuffer_get_width (priv->framebuffer);
|
width = cogl_framebuffer_get_width (priv->framebuffer);
|
||||||
height = cogl_framebuffer_get_height (priv->framebuffer);
|
height = cogl_framebuffer_get_height (priv->framebuffer);
|
||||||
|
|
||||||
if (g_strcmp0 (g_getenv ("MUTTER_DEBUG_ENABLE_DOUBLE_SHADOWFB"), "1") == 0)
|
offscreen = create_offscreen_framebuffer (view, width, height, &error);
|
||||||
|
if (!offscreen)
|
||||||
{
|
{
|
||||||
if (init_dma_buf_shadowfbs (view, width, height, &error))
|
g_warning ("Failed to create shadow framebuffer: %s", error->message);
|
||||||
{
|
return;
|
||||||
g_message ("Initialized double buffered shadow fb for %s",
|
|
||||||
priv->name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_warning ("Failed to initialize double buffered shadow fb for %s: %s",
|
|
||||||
priv->name, error->message);
|
|
||||||
g_clear_error (&error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!init_fallback_shadowfb (view, width, height, &error))
|
priv->shadow.framebuffer = offscreen;
|
||||||
{
|
return;
|
||||||
g_warning ("Failed to initialize single buffered shadow fb for %s: %s",
|
|
||||||
priv->name, error->message);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
g_message ("Initialized single buffered shadow fb for %s", priv->name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -495,180 +396,13 @@ clutter_stage_view_after_paint (ClutterStageView *view,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
|
||||||
is_tile_dirty (MtkRectangle *tile,
|
|
||||||
uint8_t *current_data,
|
|
||||||
uint8_t *prev_data,
|
|
||||||
int bpp,
|
|
||||||
int stride)
|
|
||||||
{
|
|
||||||
int y;
|
|
||||||
|
|
||||||
for (y = tile->y; y < tile->y + tile->height; y++)
|
|
||||||
{
|
|
||||||
if (memcmp (prev_data + y * stride + tile->x * bpp,
|
|
||||||
current_data + y * stride + tile->x * bpp,
|
|
||||||
tile->width * bpp) != 0)
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
flip_dma_buf_idx (int idx)
|
|
||||||
{
|
|
||||||
return (idx + 1) % 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
static MtkRegion *
|
|
||||||
find_damaged_tiles (ClutterStageView *view,
|
|
||||||
const MtkRegion *damage_region,
|
|
||||||
GError **error)
|
|
||||||
{
|
|
||||||
ClutterStageViewPrivate *priv =
|
|
||||||
clutter_stage_view_get_instance_private (view);
|
|
||||||
MtkRegion *tile_damage_region;
|
|
||||||
MtkRectangle damage_extents;
|
|
||||||
MtkRectangle fb_rect;
|
|
||||||
int prev_dma_buf_idx;
|
|
||||||
CoglDmaBufHandle *prev_dma_buf_handle;
|
|
||||||
uint8_t *prev_data;
|
|
||||||
int current_dma_buf_idx;
|
|
||||||
CoglDmaBufHandle *current_dma_buf_handle;
|
|
||||||
uint8_t *current_data;
|
|
||||||
int width, height, stride, bpp;
|
|
||||||
int tile_x_min, tile_x_max;
|
|
||||||
int tile_y_min, tile_y_max;
|
|
||||||
int tile_x, tile_y;
|
|
||||||
const int tile_size = 16;
|
|
||||||
|
|
||||||
prev_dma_buf_idx = flip_dma_buf_idx (priv->shadow.dma_buf.current_idx);
|
|
||||||
prev_dma_buf_handle = priv->shadow.dma_buf.handles[prev_dma_buf_idx];
|
|
||||||
|
|
||||||
current_dma_buf_idx = priv->shadow.dma_buf.current_idx;
|
|
||||||
current_dma_buf_handle = priv->shadow.dma_buf.handles[current_dma_buf_idx];
|
|
||||||
|
|
||||||
width = cogl_dma_buf_handle_get_width (current_dma_buf_handle);
|
|
||||||
height = cogl_dma_buf_handle_get_height (current_dma_buf_handle);
|
|
||||||
stride = cogl_dma_buf_handle_get_stride (current_dma_buf_handle);
|
|
||||||
bpp = cogl_dma_buf_handle_get_bpp (current_dma_buf_handle);
|
|
||||||
|
|
||||||
cogl_framebuffer_finish (COGL_FRAMEBUFFER (priv->shadow.framebuffer));
|
|
||||||
|
|
||||||
if (!cogl_dma_buf_handle_sync_read_start (prev_dma_buf_handle, error))
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
if (!cogl_dma_buf_handle_sync_read_start (current_dma_buf_handle, error))
|
|
||||||
goto err_sync_read_current;
|
|
||||||
|
|
||||||
prev_data = cogl_dma_buf_handle_mmap (prev_dma_buf_handle, error);
|
|
||||||
if (!prev_data)
|
|
||||||
goto err_mmap_prev;
|
|
||||||
current_data = cogl_dma_buf_handle_mmap (current_dma_buf_handle, error);
|
|
||||||
if (!current_data)
|
|
||||||
goto err_mmap_current;
|
|
||||||
|
|
||||||
fb_rect = (MtkRectangle) {
|
|
||||||
.width = width,
|
|
||||||
.height = height,
|
|
||||||
};
|
|
||||||
|
|
||||||
damage_extents = mtk_region_get_extents (damage_region);
|
|
||||||
|
|
||||||
tile_x_min = damage_extents.x / tile_size;
|
|
||||||
tile_x_max = ((damage_extents.x + damage_extents.width + tile_size - 1) /
|
|
||||||
tile_size);
|
|
||||||
tile_y_min = damage_extents.y / tile_size;
|
|
||||||
tile_y_max = ((damage_extents.y + damage_extents.height + tile_size - 1) /
|
|
||||||
tile_size);
|
|
||||||
|
|
||||||
tile_damage_region = mtk_region_create ();
|
|
||||||
|
|
||||||
for (tile_y = tile_y_min; tile_y <= tile_y_max; tile_y++)
|
|
||||||
{
|
|
||||||
for (tile_x = tile_x_min; tile_x <= tile_x_max; tile_x++)
|
|
||||||
{
|
|
||||||
MtkRectangle tile = {
|
|
||||||
.x = tile_x * tile_size,
|
|
||||||
.y = tile_y * tile_size,
|
|
||||||
.width = tile_size,
|
|
||||||
.height = tile_size,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mtk_region_contains_rectangle (damage_region, &tile) ==
|
|
||||||
MTK_REGION_OVERLAP_OUT)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
mtk_rectangle_intersect (&tile, &fb_rect, &tile);
|
|
||||||
|
|
||||||
if (is_tile_dirty (&tile, current_data, prev_data, bpp, stride))
|
|
||||||
mtk_region_union_rectangle (tile_damage_region, &tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cogl_dma_buf_handle_sync_read_end (prev_dma_buf_handle, error))
|
|
||||||
{
|
|
||||||
g_warning ("Failed to end DMA buffer read synchronization: %s",
|
|
||||||
(*error)->message);
|
|
||||||
g_clear_error (error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cogl_dma_buf_handle_sync_read_end (current_dma_buf_handle, error))
|
|
||||||
{
|
|
||||||
g_warning ("Failed to end DMA buffer read synchronization: %s",
|
|
||||||
(*error)->message);
|
|
||||||
g_clear_error (error);
|
|
||||||
}
|
|
||||||
|
|
||||||
cogl_dma_buf_handle_munmap (prev_dma_buf_handle, prev_data, NULL);
|
|
||||||
cogl_dma_buf_handle_munmap (current_dma_buf_handle, current_data, NULL);
|
|
||||||
|
|
||||||
mtk_region_intersect (tile_damage_region, damage_region);
|
|
||||||
|
|
||||||
return tile_damage_region;
|
|
||||||
|
|
||||||
err_mmap_current:
|
|
||||||
cogl_dma_buf_handle_munmap (prev_dma_buf_handle, prev_data, NULL);
|
|
||||||
|
|
||||||
err_mmap_prev:
|
|
||||||
cogl_dma_buf_handle_sync_read_end (current_dma_buf_handle, NULL);
|
|
||||||
|
|
||||||
err_sync_read_current:
|
|
||||||
cogl_dma_buf_handle_sync_read_end (prev_dma_buf_handle, NULL);
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
swap_dma_buf_framebuffer (ClutterStageView *view)
|
|
||||||
{
|
|
||||||
ClutterStageViewPrivate *priv =
|
|
||||||
clutter_stage_view_get_instance_private (view);
|
|
||||||
int next_idx;
|
|
||||||
CoglDmaBufHandle *next_dma_buf_handle;
|
|
||||||
CoglFramebuffer *next_framebuffer;
|
|
||||||
|
|
||||||
next_idx = ((priv->shadow.dma_buf.current_idx + 1) %
|
|
||||||
G_N_ELEMENTS (priv->shadow.dma_buf.handles));
|
|
||||||
priv->shadow.dma_buf.current_idx = next_idx;
|
|
||||||
|
|
||||||
next_dma_buf_handle = priv->shadow.dma_buf.handles[next_idx];
|
|
||||||
next_framebuffer =
|
|
||||||
cogl_dma_buf_handle_get_framebuffer (next_dma_buf_handle);
|
|
||||||
g_clear_object (&priv->shadow.framebuffer);
|
|
||||||
priv->shadow.framebuffer = COGL_OFFSCREEN (g_object_ref (next_framebuffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
copy_shadowfb_to_onscreen (ClutterStageView *view,
|
copy_shadowfb_to_onscreen (ClutterStageView *view,
|
||||||
const MtkRegion *swap_region)
|
const MtkRegion *swap_region)
|
||||||
{
|
{
|
||||||
ClutterStageViewPrivate *priv =
|
ClutterStageViewPrivate *priv =
|
||||||
clutter_stage_view_get_instance_private (view);
|
clutter_stage_view_get_instance_private (view);
|
||||||
ClutterDamageHistory *damage_history = priv->shadow.dma_buf.damage_history;
|
|
||||||
g_autoptr (MtkRegion) damage_region = NULL;
|
g_autoptr (MtkRegion) damage_region = NULL;
|
||||||
int age;
|
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
if (mtk_region_is_empty (swap_region))
|
if (mtk_region_is_empty (swap_region))
|
||||||
@ -684,59 +418,6 @@ copy_shadowfb_to_onscreen (ClutterStageView *view,
|
|||||||
damage_region = mtk_region_copy (swap_region);
|
damage_region = mtk_region_copy (swap_region);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_shadowfb_double_buffered (view))
|
|
||||||
{
|
|
||||||
CoglOnscreen *onscreen = COGL_ONSCREEN (priv->framebuffer);
|
|
||||||
g_autoptr (MtkRegion) changed_region = NULL;
|
|
||||||
|
|
||||||
if (cogl_onscreen_get_frame_counter (onscreen) >= 1)
|
|
||||||
{
|
|
||||||
g_autoptr (GError) error = NULL;
|
|
||||||
|
|
||||||
changed_region = find_damaged_tiles (view, damage_region, &error);
|
|
||||||
if (!changed_region)
|
|
||||||
{
|
|
||||||
int other_dma_buf_idx;
|
|
||||||
|
|
||||||
g_warning ("Disabling actual damage detection: %s",
|
|
||||||
error->message);
|
|
||||||
|
|
||||||
other_dma_buf_idx =
|
|
||||||
flip_dma_buf_idx (priv->shadow.dma_buf.current_idx);
|
|
||||||
g_clear_pointer (&priv->shadow.dma_buf.handles[other_dma_buf_idx],
|
|
||||||
cogl_dma_buf_handle_free);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
changed_region = mtk_region_copy (damage_region);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed_region)
|
|
||||||
{
|
|
||||||
int buffer_age;
|
|
||||||
|
|
||||||
clutter_damage_history_record (damage_history, changed_region);
|
|
||||||
|
|
||||||
buffer_age = cogl_onscreen_get_buffer_age (onscreen);
|
|
||||||
if (clutter_damage_history_is_age_valid (damage_history, buffer_age))
|
|
||||||
{
|
|
||||||
for (age = 1; age <= buffer_age; age++)
|
|
||||||
{
|
|
||||||
const MtkRegion *old_damage;
|
|
||||||
|
|
||||||
old_damage = clutter_damage_history_lookup (damage_history, age);
|
|
||||||
mtk_region_union (changed_region, old_damage);
|
|
||||||
}
|
|
||||||
|
|
||||||
g_clear_pointer (&damage_region, mtk_region_unref);
|
|
||||||
damage_region = g_steal_pointer (&changed_region);
|
|
||||||
}
|
|
||||||
|
|
||||||
clutter_damage_history_step (damage_history);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < mtk_region_num_rectangles (damage_region); i++)
|
for (i = 0; i < mtk_region_num_rectangles (damage_region); i++)
|
||||||
{
|
{
|
||||||
CoglFramebuffer *shadowfb = COGL_FRAMEBUFFER (priv->shadow.framebuffer);
|
CoglFramebuffer *shadowfb = COGL_FRAMEBUFFER (priv->shadow.framebuffer);
|
||||||
@ -756,9 +437,6 @@ copy_shadowfb_to_onscreen (ClutterStageView *view,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_shadowfb_double_buffered (view))
|
|
||||||
swap_dma_buf_framebuffer (view);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -798,23 +476,7 @@ clutter_stage_view_foreach_front_buffer (ClutterStageView *view,
|
|||||||
}
|
}
|
||||||
else if (priv->shadow.framebuffer)
|
else if (priv->shadow.framebuffer)
|
||||||
{
|
{
|
||||||
if (is_shadowfb_double_buffered (view))
|
callback (COGL_FRAMEBUFFER (priv->shadow.framebuffer), user_data);
|
||||||
{
|
|
||||||
int i;
|
|
||||||
|
|
||||||
for (i = 0; i < G_N_ELEMENTS (priv->shadow.dma_buf.handles); i++)
|
|
||||||
{
|
|
||||||
CoglDmaBufHandle *handle = priv->shadow.dma_buf.handles[i];
|
|
||||||
CoglFramebuffer *framebuffer =
|
|
||||||
cogl_dma_buf_handle_get_framebuffer (handle);
|
|
||||||
|
|
||||||
callback (framebuffer, user_data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback (COGL_FRAMEBUFFER (priv->shadow.framebuffer), user_data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -1463,20 +1125,12 @@ clutter_stage_view_dispose (GObject *object)
|
|||||||
ClutterStageView *view = CLUTTER_STAGE_VIEW (object);
|
ClutterStageView *view = CLUTTER_STAGE_VIEW (object);
|
||||||
ClutterStageViewPrivate *priv =
|
ClutterStageViewPrivate *priv =
|
||||||
clutter_stage_view_get_instance_private (view);
|
clutter_stage_view_get_instance_private (view);
|
||||||
int i;
|
|
||||||
|
|
||||||
g_signal_emit (view, stage_view_signals[DESTROY], 0);
|
g_signal_emit (view, stage_view_signals[DESTROY], 0);
|
||||||
|
|
||||||
g_clear_pointer (&priv->name, g_free);
|
g_clear_pointer (&priv->name, g_free);
|
||||||
|
|
||||||
g_clear_object (&priv->shadow.framebuffer);
|
g_clear_object (&priv->shadow.framebuffer);
|
||||||
for (i = 0; i < G_N_ELEMENTS (priv->shadow.dma_buf.handles); i++)
|
|
||||||
{
|
|
||||||
g_clear_pointer (&priv->shadow.dma_buf.handles[i],
|
|
||||||
cogl_dma_buf_handle_free);
|
|
||||||
}
|
|
||||||
g_clear_pointer (&priv->shadow.dma_buf.damage_history,
|
|
||||||
clutter_damage_history_free);
|
|
||||||
|
|
||||||
g_clear_object (&priv->offscreen);
|
g_clear_object (&priv->offscreen);
|
||||||
g_clear_object (&priv->offscreen_pipeline);
|
g_clear_object (&priv->offscreen_pipeline);
|
||||||
|
Loading…
Reference in New Issue
Block a user