Commit 876a8b0b authored by Yunqing Wang's avatar Yunqing Wang
Browse files

Reuse neighbor's warped motion parameters

If a block's motion_mode is WARPED_CAUSAL and its mode is NEARESTMV, search
its immediate above and left neighbors to get the set of neighbor blocks
using WARPED_CAUSAL motion mode, pick the one with largest block size, and
use that neighbor's warped motion parameters directly for the current block.
If none of the neighbors uses WARPED_CAUSAL motion mode, we estimate the
current block's warped motion parameters.

Before this patch, for every block, we estimate its warped motion parameters.
With this patch, we reduce the number of blocks doing parameter estimation.
Here are results by testing on clips with camera motions.
                    WARPED_CAUSAL blocks   blocks reusing parameters
station2_240p(30f):     3857                    1678
netflix_arieal(30f):     692                     223

No noticable changes in coding gain. Borg test result showed a PSNR
change of +0.006% on cam_lowres set, and -0.014% on lowres set.

Change-Id: If12387ad0ca8a1996ea4c3f1bedcb269ebf78c6c
parent 49404055
......@@ -1911,6 +1911,8 @@ int sortSamples(int *pts_mv, MV *mv, int *pts, int *pts_inref, int len) {
pts_mvd[i] =
abs(pts_mv[2 * i] - mv->col) + abs(pts_mv[2 * i + 1] - mv->row);
// TODO(yunqingwang): len is capped at 8 after this. Thus, modify this to find
// the best 8 samples instead of sorting all samples.
for (i = 1; i <= len - 1; ++i) {
for (j = 0; j < i; ++j) {
if (pts_mvd[j] > pts_mvd[i]) {
......@@ -1939,7 +1941,6 @@ int sortSamples(int *pts_mv, MV *mv, int *pts, int *pts_inref, int len) {
}
}
}
len = AOMMIN(len, LEAST_SQUARES_SAMPLES_MAX);
for (i = len - 1; i >= 1; i--) {
......@@ -1951,7 +1952,7 @@ int sortSamples(int *pts_mv, MV *mv, int *pts, int *pts_inref, int len) {
// Note: Samples returned are at 1/8-pel precision
int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
int *pts, int *pts_inref, int *pts_mv) {
int *pts, int *pts_inref, int *pts_mv, int *pts_wm) {
MB_MODE_INFO *const mbmi0 = &(xd->mi[0]->mbmi);
int ref_frame = mbmi0->ref_frame[0];
int up_available = xd->up_available;
......@@ -1981,6 +1982,11 @@ int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
if (mbmi->ref_frame[0] == ref_frame && mbmi->ref_frame[1] == NONE_FRAME) {
record_samples(mbmi, pts, pts_inref, pts_mv, global_offset_r,
global_offset_c, 0, -1, col_offset, 1);
pts_wm[0] = mi_row_offset * xd->mi_stride;
pts_wm[1] = (mbmi->motion_mode == WARPED_CAUSAL)
? (n8_w * mi_size_high[mbmi->sb_type])
: 0;
pts_wm += 2;
pts += 2;
pts_inref += 2;
pts_mv += 2;
......@@ -2000,6 +2006,11 @@ int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
mbmi->ref_frame[1] == NONE_FRAME) {
record_samples(mbmi, pts, pts_inref, pts_mv, global_offset_r,
global_offset_c, 0, -1, i, 1);
pts_wm[0] = mi_col_offset + mi_row_offset * xd->mi_stride;
pts_wm[1] = (mbmi->motion_mode == WARPED_CAUSAL)
? (n8_w * mi_size_high[mbmi->sb_type])
: 0;
pts_wm += 2;
pts += 2;
pts_inref += 2;
pts_mv += 2;
......@@ -2028,6 +2039,11 @@ int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
if (mbmi->ref_frame[0] == ref_frame && mbmi->ref_frame[1] == NONE_FRAME) {
record_samples(mbmi, pts, pts_inref, pts_mv, global_offset_r,
global_offset_c, row_offset, 1, 0, -1);
pts_wm[0] = mi_col_offset;
pts_wm[1] = (mbmi->motion_mode == WARPED_CAUSAL)
? (n8_h * mi_size_wide[mbmi->sb_type])
: 0;
pts_wm += 2;
pts += 2;
pts_inref += 2;
pts_mv += 2;
......@@ -2047,6 +2063,11 @@ int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
mbmi->ref_frame[1] == NONE_FRAME) {
record_samples(mbmi, pts, pts_inref, pts_mv, global_offset_r,
global_offset_c, i, 1, 0, -1);
pts_wm[0] = mi_col_offset + mi_row_offset * xd->mi_stride;
pts_wm[1] = (mbmi->motion_mode == WARPED_CAUSAL)
? (n8_h * mi_size_wide[mbmi->sb_type])
: 0;
pts_wm += 2;
pts += 2;
pts_inref += 2;
pts_mv += 2;
......@@ -2069,6 +2090,12 @@ int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
if (mbmi->ref_frame[0] == ref_frame && mbmi->ref_frame[1] == NONE_FRAME) {
record_samples(mbmi, pts, pts_inref, pts_mv, global_offset_r,
global_offset_c, 0, -1, 0, -1);
pts_wm[0] = mi_col_offset + mi_row_offset * xd->mi_stride;
pts_wm[1] =
(mbmi->motion_mode == WARPED_CAUSAL)
? (mi_size_wide[mbmi->sb_type] * mi_size_high[mbmi->sb_type])
: 0;
pts_wm += 2;
pts += 2;
pts_inref += 2;
pts_mv += 2;
......@@ -2093,6 +2120,11 @@ int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
if (mbmi->ref_frame[0] == ref_frame && mbmi->ref_frame[1] == NONE_FRAME) {
record_samples(mbmi, pts, pts_inref, pts_mv, global_offset_r,
global_offset_c, 0, -1, xd->n8_w, 1);
pts_wm[0] = mi_col_offset + mi_row_offset * xd->mi_stride;
pts_wm[1] =
(mbmi->motion_mode == WARPED_CAUSAL)
? (mi_size_wide[mbmi->sb_type] * mi_size_high[mbmi->sb_type])
: 0;
np++;
if (np >= SAMPLES_MAX) return SAMPLES_MAX;
}
......
......@@ -410,7 +410,7 @@ void av1_update_mv_context(const AV1_COMMON *cm, const MACROBLOCKD *xd,
#if CONFIG_EXT_WARPED_MOTION
int sortSamples(int *pts_mv, MV *mv, int *pts, int *pts_inref, int len);
int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
int *pts, int *pts_inref, int *pts_mv);
int *pts, int *pts_inref, int *pts_mv, int *pts_wm);
#else
int findSamples(const AV1_COMMON *cm, MACROBLOCKD *xd, int mi_row, int mi_col,
int *pts, int *pts_inref);
......
......@@ -1870,7 +1870,7 @@ static void read_inter_block_mode_info(AV1Decoder *const pbi,
int mode_ctx = 0;
int pts[SAMPLES_ARRAY_SIZE], pts_inref[SAMPLES_ARRAY_SIZE];
#if CONFIG_EXT_WARPED_MOTION
int pts_mv[SAMPLES_ARRAY_SIZE];
int pts_mv[SAMPLES_ARRAY_SIZE], pts_wm[SAMPLES_ARRAY_SIZE];
#endif // CONFIG_EXT_WARPED_MOTION
FRAME_CONTEXT *ec_ctx = xd->tile_ctx;
......@@ -2154,7 +2154,7 @@ static void read_inter_block_mode_info(AV1Decoder *const pbi,
if (mbmi->sb_type >= BLOCK_8X8 && !has_second_ref(mbmi))
#if CONFIG_EXT_WARPED_MOTION
mbmi->num_proj_ref[0] =
findSamples(cm, xd, mi_row, mi_col, pts, pts_inref, pts_mv);
findSamples(cm, xd, mi_row, mi_col, pts, pts_inref, pts_mv, pts_wm);
#else
mbmi->num_proj_ref[0] = findSamples(cm, xd, mi_row, mi_col, pts, pts_inref);
#endif // CONFIG_EXT_WARPED_MOTION
......@@ -2207,19 +2207,42 @@ static void read_inter_block_mode_info(AV1Decoder *const pbi,
mbmi->wm_params[0].wmtype = DEFAULT_WMTYPE;
#if CONFIG_EXT_WARPED_MOTION
if (mbmi->num_proj_ref[0] > 1)
mbmi->num_proj_ref[0] = sortSamples(pts_mv, &mbmi->mv[0].as_mv, pts,
pts_inref, mbmi->num_proj_ref[0]);
// Find a warped neighbor.
int cand;
int best_cand = -1;
int best_weight = 0;
assert(mbmi->mode >= NEARESTMV && mbmi->mode <= NEWMV);
if (mbmi->mode == NEARESTMV) {
for (cand = 0; cand < mbmi->num_proj_ref[0]; cand++) {
if (pts_wm[cand * 2 + 1] > best_weight) {
best_weight = pts_wm[cand * 2 + 1];
best_cand = cand;
}
}
}
if (best_cand != -1) {
MODE_INFO *best_mi = xd->mi[pts_wm[2 * best_cand]];
assert(best_mi->mbmi.motion_mode == WARPED_CAUSAL);
mbmi->wm_params[0] = best_mi->mbmi.wm_params[0];
} else {
if (mbmi->num_proj_ref[0] > 1)
mbmi->num_proj_ref[0] = sortSamples(pts_mv, &mbmi->mv[0].as_mv, pts,
pts_inref, mbmi->num_proj_ref[0]);
#endif // CONFIG_EXT_WARPED_MOTION
if (find_projection(mbmi->num_proj_ref[0], pts, pts_inref, bsize,
mbmi->mv[0].as_mv.row, mbmi->mv[0].as_mv.col,
&mbmi->wm_params[0], mi_row, mi_col)) {
if (find_projection(mbmi->num_proj_ref[0], pts, pts_inref, bsize,
mbmi->mv[0].as_mv.row, mbmi->mv[0].as_mv.col,
&mbmi->wm_params[0], mi_row, mi_col)) {
#if WARPED_MOTION_DEBUG
printf("Warning: unexpected warped model from aomenc\n");
printf("Warning: unexpected warped model from aomenc\n");
#endif
mbmi->wm_params[0].invalid = 1;
mbmi->wm_params[0].invalid = 1;
}
#if CONFIG_EXT_WARPED_MOTION
}
#endif // CONFIG_EXT_WARPED_MOTION
}
#if DEC_MISMATCH_DEBUG
......
......@@ -7287,6 +7287,39 @@ static InterpFilters condition_interp_filters_on_mv(
}
#endif
#if CONFIG_EXT_WARPED_MOTION
static int handle_zero_mv(const AV1_COMMON *const cm, MACROBLOCK *const x,
BLOCK_SIZE bsize, int mi_col, int mi_row) {
MACROBLOCKD *xd = &x->e_mbd;
MODE_INFO *mi = xd->mi[0];
MB_MODE_INFO *mbmi = &mi->mbmi;
int skip = 0;
// Handle the special case of 0 MV.
if (mbmi->ref_frame[0] > INTRA_FRAME && mbmi->ref_frame[1] <= INTRA_FRAME) {
int8_t ref_frame_type = av1_ref_frame_type(mbmi->ref_frame);
int16_t mode_ctx = x->mbmi_ext->mode_context[ref_frame_type];
if (mode_ctx & (1 << ALL_ZERO_FLAG_OFFSET)) {
int_mv zeromv;
const MV_REFERENCE_FRAME ref = mbmi->ref_frame[0];
zeromv.as_int = gm_get_motion_vector(&cm->global_motion[ref],
cm->allow_high_precision_mv, bsize,
mi_col, mi_row, 0
#if CONFIG_AMVR
,
cm->cur_frame_mv_precision_level
#endif
)
.as_int;
if (mbmi->mv[0].as_int == zeromv.as_int && mbmi->mode != GLOBALMV) {
skip = 1;
}
}
}
return skip;
}
#endif // CONFIG_EXT_WARPED_MOTION
// TODO(afergs): Refactor the MBMI references in here - there's four
// TODO(afergs): Refactor optional args - add them to a struct or remove
static int64_t motion_mode_rd(
......@@ -7323,7 +7356,7 @@ static int64_t motion_mode_rd(
#if CONFIG_EXT_WARPED_MOTION
int pts0[SAMPLES_ARRAY_SIZE], pts_inref0[SAMPLES_ARRAY_SIZE];
int pts_mv0[SAMPLES_ARRAY_SIZE];
int pts_mv0[SAMPLES_ARRAY_SIZE], pts_wm[SAMPLES_ARRAY_SIZE];
int total_samples;
#else
int pts[SAMPLES_ARRAY_SIZE], pts_inref[SAMPLES_ARRAY_SIZE];
......@@ -7335,7 +7368,7 @@ static int64_t motion_mode_rd(
aom_clear_system_state();
#if CONFIG_EXT_WARPED_MOTION
mbmi->num_proj_ref[0] =
findSamples(cm, xd, mi_row, mi_col, pts0, pts_inref0, pts_mv0);
findSamples(cm, xd, mi_row, mi_col, pts0, pts_inref0, pts_mv0, pts_wm);
total_samples = mbmi->num_proj_ref[0];
#else
mbmi->num_proj_ref[0] = findSamples(cm, xd, mi_row, mi_col, pts, pts_inref);
......@@ -7395,77 +7428,112 @@ static int64_t motion_mode_rd(
av1_unswitchable_filter(cm->interp_filter));
#if CONFIG_EXT_WARPED_MOTION
memcpy(pts, pts0, total_samples * 2 * sizeof(*pts0));
memcpy(pts_inref, pts_inref0, total_samples * 2 * sizeof(*pts_inref0));
// Rank the samples by motion vector difference
if (mbmi->num_proj_ref[0] > 1) {
mbmi->num_proj_ref[0] = sortSamples(pts_mv0, &mbmi->mv[0].as_mv, pts,
pts_inref, mbmi->num_proj_ref[0]);
best_bmc_mbmi->num_proj_ref[0] = mbmi->num_proj_ref[0];
// Find a warped neighbor.
int cand;
int best_cand = -1;
int best_weight = 0;
assert(this_mode >= NEARESTMV && this_mode <= NEWMV);
if (this_mode == NEARESTMV) {
for (cand = 0; cand < mbmi->num_proj_ref[0]; cand++) {
if (pts_wm[cand * 2 + 1] > best_weight) {
best_weight = pts_wm[cand * 2 + 1];
best_cand = cand;
}
}
}
if (best_cand != -1) {
MODE_INFO *best_mi = xd->mi[pts_wm[2 * best_cand]];
assert(best_mi->mbmi.motion_mode == WARPED_CAUSAL);
mbmi->wm_params[0] = best_mi->mbmi.wm_params[0];
// Handle the special case of 0 MV.
if (handle_zero_mv(cm, x, bsize, mi_col, mi_row)) continue;
av1_build_inter_predictors_sb(cm, xd, mi_row, mi_col, NULL, bsize);
model_rd_for_sb(cpi, bsize, x, xd, 0, MAX_MB_PLANE - 1, &tmp_rate,
&tmp_dist, skip_txfm_sb, skip_sse_sb);
} else {
memcpy(pts, pts0, total_samples * 2 * sizeof(*pts0));
memcpy(pts_inref, pts_inref0, total_samples * 2 * sizeof(*pts_inref0));
// Rank the samples by motion vector difference
if (mbmi->num_proj_ref[0] > 1) {
mbmi->num_proj_ref[0] = sortSamples(pts_mv0, &mbmi->mv[0].as_mv, pts,
pts_inref, mbmi->num_proj_ref[0]);
best_bmc_mbmi->num_proj_ref[0] = mbmi->num_proj_ref[0];
}
#endif // CONFIG_EXT_WARPED_MOTION
if (!find_projection(mbmi->num_proj_ref[0], pts, pts_inref, bsize,
mbmi->mv[0].as_mv.row, mbmi->mv[0].as_mv.col,
&mbmi->wm_params[0], mi_row, mi_col)) {
// Refine MV for NEWMV mode
if (!is_comp_pred && have_newmv_in_inter_mode(this_mode)) {
int tmp_rate_mv = 0;
const int_mv mv0 = mbmi->mv[0];
WarpedMotionParams wm_params0 = mbmi->wm_params[0];
if (!find_projection(mbmi->num_proj_ref[0], pts, pts_inref, bsize,
mbmi->mv[0].as_mv.row, mbmi->mv[0].as_mv.col,
&mbmi->wm_params[0], mi_row, mi_col)) {
// Refine MV for NEWMV mode
if (!is_comp_pred && have_newmv_in_inter_mode(this_mode)) {
int tmp_rate_mv = 0;
const int_mv mv0 = mbmi->mv[0];
WarpedMotionParams wm_params0 = mbmi->wm_params[0];
#if CONFIG_EXT_WARPED_MOTION
int num_proj_ref0 = mbmi->num_proj_ref[0];
int num_proj_ref0 = mbmi->num_proj_ref[0];
// Refine MV in a small range.
av1_refine_warped_mv(cpi, x, bsize, mi_row, mi_col, pts0, pts_inref0,
pts_mv0, total_samples);
// Refine MV in a small range.
av1_refine_warped_mv(cpi, x, bsize, mi_row, mi_col, pts0,
pts_inref0, pts_mv0, total_samples);
#else
// Refine MV in a small range.
av1_refine_warped_mv(cpi, x, bsize, mi_row, mi_col, pts, pts_inref);
#endif // CONFIG_EXT_WARPED_MOTION
// Keep the refined MV and WM parameters.
if (mv0.as_int != mbmi->mv[0].as_int) {
const int ref = refs[0];
const MV ref_mv = x->mbmi_ext->ref_mvs[ref][0].as_mv;
// Keep the refined MV and WM parameters.
if (mv0.as_int != mbmi->mv[0].as_int) {
const int ref = refs[0];
const MV ref_mv = x->mbmi_ext->ref_mvs[ref][0].as_mv;
tmp_rate_mv =
av1_mv_bit_cost(&mbmi->mv[0].as_mv, &ref_mv, x->nmvjointcost,
x->mvcost, MV_COST_WEIGHT);
tmp_rate_mv =
av1_mv_bit_cost(&mbmi->mv[0].as_mv, &ref_mv, x->nmvjointcost,
x->mvcost, MV_COST_WEIGHT);
if (cpi->sf.adaptive_motion_search)
x->pred_mv[ref] = mbmi->mv[0].as_mv;
if (cpi->sf.adaptive_motion_search)
x->pred_mv[ref] = mbmi->mv[0].as_mv;
single_newmv[ref] = mbmi->mv[0];
single_newmv[ref] = mbmi->mv[0];
if (discount_newmv_test(cpi, this_mode, mbmi->mv[0], mode_mv,
refs[0])) {
tmp_rate_mv = AOMMAX((tmp_rate_mv / NEW_MV_DISCOUNT_FACTOR), 1);
}
if (discount_newmv_test(cpi, this_mode, mbmi->mv[0], mode_mv,
refs[0])) {
tmp_rate_mv = AOMMAX((tmp_rate_mv / NEW_MV_DISCOUNT_FACTOR), 1);
}
#if CONFIG_EXT_WARPED_MOTION
best_bmc_mbmi->num_proj_ref[0] = mbmi->num_proj_ref[0];
best_bmc_mbmi->num_proj_ref[0] = mbmi->num_proj_ref[0];
#endif // CONFIG_EXT_WARPED_MOTION
tmp_rate2 = rate2_bmc_nocoeff - rate_mv_bmc + tmp_rate_mv;
tmp_rate2 = rate2_bmc_nocoeff - rate_mv_bmc + tmp_rate_mv;
#if CONFIG_DUAL_FILTER
mbmi->interp_filters =
condition_interp_filters_on_mv(mbmi->interp_filters, xd);
mbmi->interp_filters =
condition_interp_filters_on_mv(mbmi->interp_filters, xd);
#endif // CONFIG_DUAL_FILTER
} else {
// Restore the old MV and WM parameters.
mbmi->mv[0] = mv0;
mbmi->wm_params[0] = wm_params0;
} else {
// Restore the old MV and WM parameters.
mbmi->mv[0] = mv0;
mbmi->wm_params[0] = wm_params0;
#if CONFIG_EXT_WARPED_MOTION
mbmi->num_proj_ref[0] = num_proj_ref0;
mbmi->num_proj_ref[0] = num_proj_ref0;
#endif // CONFIG_EXT_WARPED_MOTION
}
}
}
av1_build_inter_predictors_sb(cm, xd, mi_row, mi_col, NULL, bsize);
model_rd_for_sb(cpi, bsize, x, xd, 0, MAX_MB_PLANE - 1, &tmp_rate,
&tmp_dist, skip_txfm_sb, skip_sse_sb);
} else {
continue;
#if CONFIG_EXT_WARPED_MOTION
// Handle the special case of 0 MV.
if (handle_zero_mv(cm, x, bsize, mi_col, mi_row)) continue;
#endif // CONFIG_EXT_WARPED_MOTION
av1_build_inter_predictors_sb(cm, xd, mi_row, mi_col, NULL, bsize);
model_rd_for_sb(cpi, bsize, x, xd, 0, MAX_MB_PLANE - 1, &tmp_rate,
&tmp_dist, skip_txfm_sb, skip_sse_sb);
} else {
continue;
}
#if CONFIG_EXT_WARPED_MOTION
}
#endif // CONFIG_EXT_WARPED_MOTION
}
x->skip = 0;
......@@ -9960,7 +10028,11 @@ PALETTE_EXIT:
// Therefore, sometimes, NEWMV is chosen instead of NEARESTMV, NEARMV, and
// GLOBALMV. Here, checks are added for those cases, and the mode decisions
// are corrected.
if (best_mbmode.mode == NEWMV || best_mbmode.mode == NEW_NEWMV) {
if ((best_mbmode.mode == NEWMV || best_mbmode.mode == NEW_NEWMV)
#if CONFIG_EXT_WARPED_MOTION
&& best_mbmode.motion_mode != WARPED_CAUSAL
#endif // CONFIG_EXT_WARPED_MOTION
) {
const MV_REFERENCE_FRAME refs[2] = { best_mbmode.ref_frame[0],
best_mbmode.ref_frame[1] };
int comp_pred_mode = refs[1] > INTRA_FRAME;
......@@ -10059,7 +10131,11 @@ PALETTE_EXIT:
}
if (best_mbmode.ref_frame[0] > INTRA_FRAME &&
best_mbmode.ref_frame[1] <= INTRA_FRAME) {
best_mbmode.ref_frame[1] <= INTRA_FRAME
#if CONFIG_EXT_WARPED_MOTION
&& best_mbmode.motion_mode != WARPED_CAUSAL
#endif // CONFIG_EXT_WARPED_MOTION
) {
int8_t ref_frame_type = av1_ref_frame_type(best_mbmode.ref_frame);
int16_t mode_ctx = mbmi_ext->mode_context[ref_frame_type];
if (mode_ctx & (1 << ALL_ZERO_FLAG_OFFSET)) {
......@@ -10221,9 +10297,9 @@ void av1_rd_pick_inter_mode_sb_seg_skip(const AV1_COMP *cpi,
if (is_motion_variation_allowed_bsize(bsize) && !has_second_ref(mbmi)) {
int pts[SAMPLES_ARRAY_SIZE], pts_inref[SAMPLES_ARRAY_SIZE];
#if CONFIG_EXT_WARPED_MOTION
int pts_mv[SAMPLES_ARRAY_SIZE];
int pts_mv[SAMPLES_ARRAY_SIZE], pts_wm[SAMPLES_ARRAY_SIZE];
mbmi->num_proj_ref[0] =
findSamples(cm, xd, mi_row, mi_col, pts, pts_inref, pts_mv);
findSamples(cm, xd, mi_row, mi_col, pts, pts_inref, pts_mv, pts_wm);
// Rank the samples by motion vector difference
if (mbmi->num_proj_ref[0] > 1)
mbmi->num_proj_ref[0] = sortSamples(pts_mv, &mbmi->mv[0].as_mv, pts,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment