mirror of
https://gitlab.freedesktop.org/wayland/weston.git
synced 2026-05-07 04:58:16 +02:00
backend-drm: offload pre-blend color xform
Use the per-plane color pipelines to offload pre-blend Weston color transformations when possible. Signed-off-by: Leandro Ribeiro <leandro.ribeiro@collabora.com>
This commit is contained in:
parent
b79aee2a2a
commit
22d907bc03
7 changed files with 914 additions and 3 deletions
|
|
@ -186,6 +186,193 @@ drm_colorop_3x1d_lut_blob_create(struct drm_device *device,
|
|||
return lut;
|
||||
}
|
||||
|
||||
enum lowering_curve_policy {
|
||||
LOWERING_CURVE_POLICY_ALLOW = true,
|
||||
LOWERING_CURVE_POLICY_DENY = false,
|
||||
};
|
||||
|
||||
static const char *
|
||||
lowering_curve_policy_str(enum lowering_curve_policy policy)
|
||||
{
|
||||
switch (policy) {
|
||||
case LOWERING_CURVE_POLICY_DENY:
|
||||
return "deny lowering curve";
|
||||
case LOWERING_CURVE_POLICY_ALLOW:
|
||||
return "allow lowering curve";
|
||||
}
|
||||
return "???";
|
||||
}
|
||||
|
||||
static struct drm_colorop_3x1d_lut_blob *
|
||||
drm_colorop_3x1d_lut_blob_from_curve(struct drm_device *device,
|
||||
struct weston_color_transform *xform,
|
||||
enum weston_color_curve_step curve_step,
|
||||
uint32_t lut_len)
|
||||
{
|
||||
struct weston_compositor *compositor = xform->cm->compositor;
|
||||
struct drm_backend *b = device->backend;
|
||||
struct drm_colorop_3x1d_lut_blob *colorop_lut;
|
||||
char *err_msg;
|
||||
struct weston_vec3f *cm_lut;
|
||||
|
||||
/* No need to create, 3x1D LUT colorop already exists. */
|
||||
colorop_lut = drm_colorop_3x1d_lut_blob_search(device, xform, curve_step,
|
||||
DRM_COLOROP_3X1D_LUT_BLOB_QUANTIZATION_U32,
|
||||
lut_len);
|
||||
if (colorop_lut)
|
||||
return colorop_lut;
|
||||
|
||||
cm_lut = weston_color_curve_to_3x1D_LUT(compositor, xform, curve_step,
|
||||
WESTON_COLOR_PRECISION_CARELESS,
|
||||
lut_len, &err_msg);
|
||||
if (!cm_lut) {
|
||||
drm_debug(b, "[colorop] failed to create colorop 3x1D from curve: %s\n",
|
||||
err_msg);
|
||||
free(err_msg);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
colorop_lut =
|
||||
drm_colorop_3x1d_lut_blob_create(device, xform, curve_step,
|
||||
DRM_COLOROP_3X1D_LUT_BLOB_QUANTIZATION_U32,
|
||||
cm_lut, lut_len);
|
||||
free(cm_lut);
|
||||
if (!colorop_lut) {
|
||||
drm_debug(b, "[colorop] failed to create colorop 3x1D from curve\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return colorop_lut;
|
||||
}
|
||||
|
||||
static void
|
||||
drm_colorop_matrix_blob_destroy(struct drm_colorop_matrix_blob *mat)
|
||||
{
|
||||
wl_list_remove(&mat->destroy_listener.link);
|
||||
wl_list_remove(&mat->link);
|
||||
drmModeDestroyPropertyBlob(mat->device->kms_device->fd, mat->blob_id);
|
||||
free(mat);
|
||||
}
|
||||
|
||||
static void
|
||||
drm_colorop_matrix_blob_destroy_handler(struct wl_listener *l, void *data)
|
||||
{
|
||||
struct drm_colorop_matrix_blob *mat =
|
||||
wl_container_of(l, mat, destroy_listener);
|
||||
|
||||
drm_colorop_matrix_blob_destroy(mat);
|
||||
}
|
||||
|
||||
static struct drm_colorop_matrix_blob *
|
||||
drm_colorop_matrix_blob_search(struct drm_device *device,
|
||||
struct weston_color_transform *xform)
|
||||
{
|
||||
struct drm_colorop_matrix_blob *mat;
|
||||
|
||||
wl_list_for_each(mat, &device->drm_colorop_matrix_blob_list, link)
|
||||
if (mat->xform == xform)
|
||||
return mat;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Float to S31.32 sign-magnitude representation.
|
||||
*/
|
||||
static uint64_t
|
||||
float_to_s31_32_sign_magnitude(float val)
|
||||
{
|
||||
uint64_t ret;
|
||||
|
||||
if (val < 0) {
|
||||
ret = (uint64_t) (-val * (1ULL << 32));
|
||||
ret |= 1ULL << 63;
|
||||
} else {
|
||||
ret = (uint64_t) (val * (1ULL << 32));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static struct drm_colorop_matrix_blob *
|
||||
drm_colorop_matrix_blob_create(struct drm_device *device,
|
||||
struct weston_color_transform *xform,
|
||||
struct drm_color_ctm_3x4 *matrix)
|
||||
{
|
||||
struct drm_backend *b = device->backend;
|
||||
struct drm_colorop_matrix_blob *colorop_mat;
|
||||
uint32_t blob_id;
|
||||
int ret;
|
||||
|
||||
ret = drmModeCreatePropertyBlob(device->kms_device->fd, matrix,
|
||||
sizeof(*matrix), &blob_id);
|
||||
if (ret < 0) {
|
||||
drm_debug(b, "[colorop] failed to create blob for matrix\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
colorop_mat = xzalloc(sizeof(*colorop_mat));
|
||||
|
||||
colorop_mat->blob_id = blob_id;
|
||||
colorop_mat->device = device;
|
||||
colorop_mat->xform = xform;
|
||||
wl_list_insert(&device->drm_colorop_matrix_blob_list, &colorop_mat->link);
|
||||
colorop_mat->destroy_listener.notify = drm_colorop_matrix_blob_destroy_handler;
|
||||
wl_signal_add(&xform->destroy_signal, &colorop_mat->destroy_listener);
|
||||
|
||||
return colorop_mat;
|
||||
}
|
||||
|
||||
static struct drm_colorop_matrix_blob *
|
||||
drm_colorop_matrix_blob_from_mapping(struct drm_device *device,
|
||||
struct weston_color_transform *xform)
|
||||
{
|
||||
struct drm_backend *b = device->backend;
|
||||
struct weston_color_mapping *mapping = &xform->mapping;
|
||||
struct drm_colorop_matrix_blob *colorop_mat;
|
||||
struct drm_color_ctm_3x4 *mat_3x4;
|
||||
unsigned int row, col;
|
||||
float val;
|
||||
|
||||
/* No need to create, colorop matrix already exists. */
|
||||
colorop_mat = drm_colorop_matrix_blob_search(device, xform);
|
||||
if (colorop_mat)
|
||||
return colorop_mat;
|
||||
|
||||
mat_3x4 = xzalloc(sizeof(*mat_3x4));
|
||||
|
||||
/**
|
||||
* mapping->u.mat.matrix is in column-major order. We transpose it and
|
||||
* also add a new column with the offset. Also, kernel requires the
|
||||
* values in S31.32 sign-magnitude representation.
|
||||
*/
|
||||
for (row = 0; row < 3; row++) {
|
||||
for (col = 0; col < 3; col++) {
|
||||
val = mapping->u.mat.matrix.col[col].el[row];
|
||||
mat_3x4->matrix[row * 4 + col] = float_to_s31_32_sign_magnitude(val);
|
||||
}
|
||||
val = mapping->u.mat.offset.el[row];
|
||||
mat_3x4->matrix[row * 4 + 3] = float_to_s31_32_sign_magnitude(val);
|
||||
}
|
||||
|
||||
colorop_mat = drm_colorop_matrix_blob_create(device, xform, mat_3x4);
|
||||
free(mat_3x4);
|
||||
if (!colorop_mat) {
|
||||
drm_debug(b, "[colorop] failed to create colorop matrix from mapping\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return colorop_mat;
|
||||
}
|
||||
|
||||
static enum wdrm_colorop_curve_1d
|
||||
weston_tf_to_colorop_curve(const struct weston_color_tf_info *tf_info,
|
||||
enum weston_tf_direction tf_direction)
|
||||
{
|
||||
return (tf_direction == WESTON_INVERSE_TF) ?
|
||||
tf_info->kms_colorop_inverse : tf_info->kms_colorop;
|
||||
}
|
||||
|
||||
static void
|
||||
drm_colorop_destroy(struct drm_colorop *colorop)
|
||||
{
|
||||
|
|
@ -248,12 +435,428 @@ drm_colorop_create(struct drm_color_pipeline *pipeline, uint32_t colorop_id,
|
|||
return colorop;
|
||||
}
|
||||
|
||||
static const char *
|
||||
/**
|
||||
* Given a colorop this returns its type as a string.
|
||||
*
|
||||
* \param colorop The colorop.
|
||||
* \return The colorop type as a string.
|
||||
*/
|
||||
const char *
|
||||
drm_colorop_type_to_str(struct drm_colorop *colorop)
|
||||
{
|
||||
return colorop->props[WDRM_COLOROP_TYPE].enum_values[colorop->type].name;
|
||||
}
|
||||
|
||||
static struct drm_colorop *
|
||||
drm_colorop_iterate(struct drm_color_pipeline *pipeline, struct drm_colorop *iter)
|
||||
{
|
||||
struct wl_list *list = &pipeline->colorop_list;
|
||||
struct wl_list *node;
|
||||
|
||||
if (iter)
|
||||
node = iter->link.next;
|
||||
else
|
||||
node = list->next;
|
||||
|
||||
if (node == list)
|
||||
return NULL;
|
||||
|
||||
return container_of(node, struct drm_colorop, link);
|
||||
}
|
||||
|
||||
static bool
|
||||
is_colorop_compatible_with_curve(struct weston_compositor *compositor,
|
||||
struct drm_colorop *colorop,
|
||||
struct weston_color_curve *curve)
|
||||
{
|
||||
struct weston_color_curve_parametric param;
|
||||
struct drm_property_info *prop_info;
|
||||
enum wdrm_colorop_curve_1d curve_type;
|
||||
bool ret;
|
||||
|
||||
if (colorop->type == WDRM_COLOROP_TYPE_1D_CURVE) {
|
||||
if (curve->type != WESTON_COLOR_CURVE_TYPE_ENUM)
|
||||
return false;
|
||||
|
||||
curve_type = weston_tf_to_colorop_curve(curve->u.enumerated.tf.info,
|
||||
curve->u.enumerated.tf_direction);
|
||||
if (curve_type == WDRM_COLOROP_CURVE_1D__COUNT)
|
||||
return false;
|
||||
|
||||
prop_info = &colorop->props[WDRM_COLOROP_CURVE_1D];
|
||||
if (!prop_info->enum_values[curve_type].valid)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
} else if (colorop->type == WDRM_COLOROP_TYPE_1D_LUT) {
|
||||
switch (curve->type) {
|
||||
case WESTON_COLOR_CURVE_TYPE_LUT_3x1D:
|
||||
return true;
|
||||
case WESTON_COLOR_CURVE_TYPE_PARAMETRIC:
|
||||
/* Parametric can be lowered to LUT. */
|
||||
return true;
|
||||
case WESTON_COLOR_CURVE_TYPE_ENUM:
|
||||
switch (curve->u.enumerated.tf.info->tf) {
|
||||
case WESTON_TF_ST2084_PQ:
|
||||
/* This TF is implemented, so we can lower curve to LUT. */
|
||||
return true;
|
||||
default:
|
||||
/* If we can lower the TF to parametric, we can use it
|
||||
* to create a LUT. */
|
||||
ret = weston_color_curve_enum_get_parametric(compositor,
|
||||
&curve->u.enumerated,
|
||||
¶m);
|
||||
return ret;
|
||||
}
|
||||
case WESTON_COLOR_CURVE_TYPE_IDENTITY:
|
||||
/* Dead code, function never called for IDENTITY. */
|
||||
weston_assert_not_reached(compositor,
|
||||
"no need to get colorop for identity curve");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct drm_colorop *
|
||||
search_colorop_compatible_curve(struct drm_color_pipeline *pipeline,
|
||||
struct drm_colorop *previous_colorop,
|
||||
struct weston_color_curve *curve,
|
||||
enum lowering_curve_policy policy)
|
||||
{
|
||||
struct drm_backend *b = pipeline->plane->device->backend;
|
||||
struct drm_colorop *colorop = previous_colorop;
|
||||
|
||||
/**
|
||||
* Identity curve should not need a colorop, so calling this func for
|
||||
* IDENTITY is not allowed.
|
||||
*/
|
||||
weston_assert_u32_ne(b->compositor,
|
||||
curve->type, WESTON_COLOR_CURVE_TYPE_IDENTITY);
|
||||
|
||||
while ((colorop = drm_colorop_iterate(pipeline, colorop))) {
|
||||
switch (curve->type) {
|
||||
case WESTON_COLOR_CURVE_TYPE_ENUM:
|
||||
if (colorop->type == WDRM_COLOROP_TYPE_1D_CURVE &&
|
||||
is_colorop_compatible_with_curve(b->compositor, colorop, curve))
|
||||
return colorop;
|
||||
else if (colorop->type == WDRM_COLOROP_TYPE_1D_LUT &&
|
||||
policy == LOWERING_CURVE_POLICY_ALLOW &&
|
||||
is_colorop_compatible_with_curve(b->compositor, colorop, curve))
|
||||
return colorop;
|
||||
break;
|
||||
case WESTON_COLOR_CURVE_TYPE_PARAMETRIC:
|
||||
case WESTON_COLOR_CURVE_TYPE_LUT_3x1D:
|
||||
if (colorop->type == WDRM_COLOROP_TYPE_1D_LUT)
|
||||
return colorop;
|
||||
break;
|
||||
case WESTON_COLOR_CURVE_TYPE_IDENTITY:
|
||||
/* Dead code. */
|
||||
weston_assert_not_reached(b->compositor,
|
||||
"no need to get colorop for identity curve");
|
||||
}
|
||||
|
||||
if (!colorop->can_bypass)
|
||||
break;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct drm_colorop *
|
||||
search_colorop_type(struct drm_color_pipeline *pipeline,
|
||||
struct drm_colorop *previous_colorop,
|
||||
enum wdrm_colorop_type type)
|
||||
{
|
||||
struct drm_colorop *colorop = previous_colorop;
|
||||
|
||||
while ((colorop = drm_colorop_iterate(pipeline, colorop))) {
|
||||
if (colorop->type == type)
|
||||
return colorop;
|
||||
|
||||
if (!colorop->can_bypass)
|
||||
break;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct drm_colorop_state *
|
||||
drm_colorop_state_create(struct drm_color_pipeline_state *pipeline_state,
|
||||
struct drm_colorop *colorop,
|
||||
struct drm_colorop_state_object so)
|
||||
{
|
||||
struct drm_colorop_state *colorop_state;
|
||||
|
||||
colorop_state = xzalloc(sizeof(*colorop_state));
|
||||
|
||||
wl_list_insert(pipeline_state->colorop_state_list.prev, &colorop_state->link);
|
||||
|
||||
colorop_state->colorop = colorop;
|
||||
colorop_state->object = so;
|
||||
|
||||
return colorop_state;
|
||||
}
|
||||
|
||||
static void
|
||||
drm_colorop_state_destroy(struct drm_colorop_state *colorop_state)
|
||||
{
|
||||
wl_list_remove(&colorop_state->link);
|
||||
free(colorop_state);
|
||||
}
|
||||
|
||||
static struct drm_color_pipeline_state *
|
||||
drm_color_pipeline_state_create(struct drm_color_pipeline *pipeline)
|
||||
{
|
||||
struct drm_color_pipeline_state *state;
|
||||
|
||||
state = xzalloc(sizeof(*state));
|
||||
|
||||
state->pipeline = pipeline;
|
||||
|
||||
wl_list_init(&state->colorop_state_list);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a color pipeline state.
|
||||
*
|
||||
* @param state The pipeline state to destroy.
|
||||
*/
|
||||
void
|
||||
drm_color_pipeline_state_destroy(struct drm_color_pipeline_state *state)
|
||||
{
|
||||
struct drm_colorop_state *colorop_state, *tmp_colorop_state;
|
||||
|
||||
if (!state)
|
||||
return;
|
||||
|
||||
wl_list_for_each_safe(colorop_state, tmp_colorop_state,
|
||||
&state->colorop_state_list, link)
|
||||
drm_colorop_state_destroy(colorop_state);
|
||||
|
||||
free(state);
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
prop_val_from_curve(struct drm_device *device, struct drm_colorop *colorop,
|
||||
struct weston_color_curve *curve)
|
||||
{
|
||||
struct weston_compositor *compositor = device->backend->compositor;
|
||||
enum wdrm_colorop_curve_1d curve_type;
|
||||
struct drm_property_enum_info *prop_info;
|
||||
|
||||
weston_assert_u32_eq(compositor, curve->type,
|
||||
WESTON_COLOR_CURVE_TYPE_ENUM);
|
||||
|
||||
curve_type = weston_tf_to_colorop_curve(curve->u.enumerated.tf.info,
|
||||
curve->u.enumerated.tf_direction);
|
||||
weston_assert_u32_ne(compositor, curve_type,
|
||||
WDRM_COLOROP_CURVE_1D__COUNT);
|
||||
|
||||
prop_info = &colorop->props[WDRM_COLOROP_CURVE_1D].enum_values[curve_type];
|
||||
weston_assert_true(compositor, prop_info->valid);
|
||||
|
||||
return prop_info->value;
|
||||
}
|
||||
|
||||
static struct drm_colorop_state *
|
||||
curve_create_colorop_state(struct drm_color_pipeline_state *pipeline_state,
|
||||
struct drm_colorop *previous_colorop,
|
||||
struct weston_color_transform *xform,
|
||||
enum weston_color_curve_step curve_step,
|
||||
enum lowering_curve_policy policy)
|
||||
{
|
||||
struct drm_color_pipeline *pipeline = pipeline_state->pipeline;
|
||||
struct weston_compositor *compositor = pipeline->plane->base.compositor;
|
||||
struct drm_device *device = pipeline->plane->device;
|
||||
struct drm_colorop_3x1d_lut_blob *lut_blob;
|
||||
struct weston_color_curve *curve;
|
||||
struct drm_colorop_state_object so = { 0 };
|
||||
struct drm_colorop *colorop;
|
||||
uint32_t lut_len;
|
||||
|
||||
curve = (curve_step == WESTON_COLOR_CURVE_STEP_PRE) ? &xform->pre_curve :
|
||||
&xform->post_curve;
|
||||
|
||||
colorop = search_colorop_compatible_curve(pipeline, previous_colorop,
|
||||
curve, policy);
|
||||
if (!colorop)
|
||||
return NULL;
|
||||
|
||||
switch (colorop->type) {
|
||||
case WDRM_COLOROP_TYPE_1D_CURVE:
|
||||
so.type = COLOROP_OBJECT_TYPE_CURVE;
|
||||
so.curve_type_prop_val = prop_val_from_curve(device, colorop, curve);
|
||||
break;
|
||||
case WDRM_COLOROP_TYPE_1D_LUT:
|
||||
lut_len = colorop->size;
|
||||
lut_blob = drm_colorop_3x1d_lut_blob_from_curve(device, xform,
|
||||
curve_step, lut_len);
|
||||
if (!lut_blob)
|
||||
return NULL;
|
||||
so.type = COLOROP_OBJECT_TYPE_3x1D_LUT;
|
||||
so.lut_3x1d_blob_id = lut_blob->blob_id;
|
||||
break;
|
||||
default:
|
||||
weston_assert_not_reached(compositor,
|
||||
"curve colorop should be 1D curve or 1D LUT");
|
||||
}
|
||||
|
||||
return drm_colorop_state_create(pipeline_state, colorop, so);
|
||||
}
|
||||
|
||||
static struct drm_colorop_state *
|
||||
mapping_create_colorop_state(struct drm_color_pipeline_state *pipeline_state,
|
||||
struct drm_colorop *previous_colorop,
|
||||
struct weston_color_transform *xform)
|
||||
{
|
||||
struct drm_color_pipeline *pipeline = pipeline_state->pipeline;
|
||||
struct weston_compositor *compositor = pipeline->plane->base.compositor;
|
||||
struct drm_device *device = pipeline->plane->device;
|
||||
struct weston_color_mapping *mapping = &xform->mapping;
|
||||
struct drm_colorop_matrix_blob *mat_blob;
|
||||
struct drm_colorop_state_object so = { 0 };
|
||||
struct drm_colorop *colorop;
|
||||
|
||||
/* For now Weston has only matrices color mapping. */
|
||||
weston_assert_u32_eq(compositor,
|
||||
mapping->type, WESTON_COLOR_MAPPING_TYPE_MATRIX);
|
||||
|
||||
colorop = search_colorop_type(pipeline, previous_colorop,
|
||||
WDRM_COLOROP_TYPE_CTM_3X4);
|
||||
if (!colorop)
|
||||
return NULL;
|
||||
|
||||
mat_blob = drm_colorop_matrix_blob_from_mapping(device, xform);
|
||||
if (!mat_blob)
|
||||
return NULL;
|
||||
|
||||
so.type = COLOROP_OBJECT_TYPE_MATRIX;
|
||||
so.matrix_blob_id = mat_blob->blob_id;
|
||||
|
||||
return drm_colorop_state_create(pipeline_state, colorop, so);
|
||||
}
|
||||
|
||||
static struct drm_color_pipeline_state *
|
||||
drm_color_pipeline_state_from_xform_steps(struct drm_color_pipeline *pipeline,
|
||||
struct weston_color_transform *xform,
|
||||
enum lowering_curve_policy policy,
|
||||
const char *indent)
|
||||
{
|
||||
struct drm_backend *b = pipeline->plane->device->backend;
|
||||
struct drm_color_pipeline_state *pipeline_state;
|
||||
struct drm_colorop_state *colorop_state;
|
||||
struct drm_colorop *previous_colorop;
|
||||
uint32_t type;
|
||||
|
||||
pipeline_state = drm_color_pipeline_state_create(pipeline);
|
||||
|
||||
/* First previous_colorop: none. */
|
||||
previous_colorop = NULL;
|
||||
|
||||
/* Find colorop for pre-curve. */
|
||||
type = xform->pre_curve.type;
|
||||
if (type != WESTON_COLOR_CURVE_TYPE_IDENTITY) {
|
||||
colorop_state = curve_create_colorop_state(pipeline_state,
|
||||
previous_colorop, xform,
|
||||
WESTON_COLOR_CURVE_STEP_PRE,
|
||||
policy);
|
||||
if (!colorop_state)
|
||||
goto err;
|
||||
|
||||
previous_colorop = colorop_state->colorop;
|
||||
}
|
||||
|
||||
/* Find colorop for color mapping. */
|
||||
type = xform->mapping.type;
|
||||
if (type != WESTON_COLOR_MAPPING_TYPE_IDENTITY) {
|
||||
colorop_state = mapping_create_colorop_state(pipeline_state,
|
||||
previous_colorop, xform);
|
||||
if (!colorop_state)
|
||||
goto err;
|
||||
|
||||
previous_colorop = colorop_state->colorop;
|
||||
}
|
||||
|
||||
/* Find colorop for post-curve. */
|
||||
type = xform->post_curve.type;
|
||||
if (type != WESTON_COLOR_CURVE_TYPE_IDENTITY) {
|
||||
colorop_state = curve_create_colorop_state(pipeline_state,
|
||||
previous_colorop, xform,
|
||||
WESTON_COLOR_CURVE_STEP_POST,
|
||||
policy);
|
||||
if (!colorop_state)
|
||||
goto err;
|
||||
}
|
||||
|
||||
drm_debug(b, "%s[colorop] color pipeline id %u IS compatible with xform t%u;\n" \
|
||||
"%s policy: %s\n",
|
||||
indent, pipeline->id, xform->id, indent,
|
||||
lowering_curve_policy_str(policy));
|
||||
return pipeline_state;
|
||||
|
||||
err:
|
||||
drm_color_pipeline_state_destroy(pipeline_state);
|
||||
drm_debug(b, "%s[colorop] color pipeline id %u NOT compatible with xform t%u;\n" \
|
||||
"%s policy: %s\n",
|
||||
indent, pipeline->id, xform->id, indent,
|
||||
lowering_curve_policy_str(policy));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a color transformation, returns a color pipeline state that can
|
||||
* be used to offload such xform to KMS.
|
||||
*
|
||||
* @param plane The DRM plane that we plan to use to offload the view.
|
||||
* @param xform The xform to offload.
|
||||
* @param indent To print debug error messages with proper indentation.
|
||||
* @return The color pipeline state, or NULL if no color pipelines are
|
||||
* compatible with the xform.
|
||||
*/
|
||||
struct drm_color_pipeline_state *
|
||||
drm_color_pipeline_state_from_xform(struct drm_plane *plane,
|
||||
struct weston_color_transform *xform,
|
||||
const char *indent)
|
||||
{
|
||||
struct drm_backend *b = plane->device->backend;
|
||||
struct drm_color_pipeline_state *pipeline_state;
|
||||
unsigned int i, mode_index;
|
||||
enum lowering_curve_policy policy;
|
||||
enum lowering_curve_policy policy_modes[2] = {
|
||||
LOWERING_CURVE_POLICY_DENY, LOWERING_CURVE_POLICY_ALLOW
|
||||
};
|
||||
|
||||
drm_debug(b, "%s[colorop] searching color pipeline compatible with xform t%u\n",
|
||||
indent, xform->id);
|
||||
|
||||
/**
|
||||
* Try to find a compatible pipeline.
|
||||
*
|
||||
* First, we try to find a compatible pipeline but not allowing Weston
|
||||
* enumerated color curves to be lowered to parametric. If we can't find
|
||||
* something, we start allowing that.
|
||||
*/
|
||||
if (xform->steps_valid) {
|
||||
for (mode_index = 0; mode_index < ARRAY_LENGTH(policy_modes); mode_index++) {
|
||||
policy = policy_modes[mode_index];
|
||||
for (i = 0; i < plane->num_color_pipelines; i++) {
|
||||
pipeline_state =
|
||||
drm_color_pipeline_state_from_xform_steps(&plane->pipelines[i],
|
||||
xform, policy, indent);
|
||||
if (pipeline_state)
|
||||
return pipeline_state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
drm_color_pipeline_print(struct drm_color_pipeline *pipeline, FILE *fp)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,6 +28,34 @@
|
|||
#include "drm-internal.h"
|
||||
#include "drm-kms-enums.h"
|
||||
|
||||
struct drm_colorop_clut_blob {
|
||||
/* drm_device::drm_colorop_clut_blob_list */
|
||||
struct wl_list link;
|
||||
struct drm_device *device;
|
||||
|
||||
/* Lifetime matches the xform. */
|
||||
struct weston_color_transform *xform;
|
||||
struct wl_listener destroy_listener;
|
||||
|
||||
uint32_t shaper_len;
|
||||
uint32_t clut_len;
|
||||
|
||||
uint32_t shaper_blob_id;
|
||||
uint32_t clut_blob_id;
|
||||
};
|
||||
|
||||
struct drm_colorop_matrix_blob {
|
||||
/* drm_device::drm_colorop_matrix_blob_list */
|
||||
struct wl_list link;
|
||||
struct drm_device *device;
|
||||
|
||||
/* Lifetime matches the xform. */
|
||||
struct weston_color_transform *xform;
|
||||
struct wl_listener destroy_listener;
|
||||
|
||||
uint32_t blob_id;
|
||||
};
|
||||
|
||||
struct drm_colorop {
|
||||
struct drm_color_pipeline *pipeline;
|
||||
struct wl_list link; /* drm_pipeline::colorop_list */
|
||||
|
|
@ -46,14 +74,53 @@ struct drm_colorop {
|
|||
struct drm_property_info props[WDRM_COLOROP__COUNT];
|
||||
};
|
||||
|
||||
enum colorop_object_type {
|
||||
COLOROP_OBJECT_TYPE_CURVE = 0,
|
||||
COLOROP_OBJECT_TYPE_MATRIX,
|
||||
COLOROP_OBJECT_TYPE_3x1D_LUT,
|
||||
};
|
||||
|
||||
struct drm_colorop_state_object {
|
||||
/* Defines which of the below is valid. The others are zero. */
|
||||
enum colorop_object_type type;
|
||||
|
||||
uint64_t curve_type_prop_val;
|
||||
uint32_t matrix_blob_id;
|
||||
uint32_t lut_3x1d_blob_id;
|
||||
};
|
||||
|
||||
struct drm_colorop_state {
|
||||
struct drm_colorop *colorop;
|
||||
/* struct drm_color_pipeline_state::colorop_state_list */
|
||||
struct wl_list link;
|
||||
|
||||
/* Object that should be programmed through the colorop. */
|
||||
struct drm_colorop_state_object object;
|
||||
};
|
||||
|
||||
struct drm_color_pipeline {
|
||||
struct drm_plane *plane;
|
||||
struct wl_list colorop_list; /* drm_colorop::link */
|
||||
uint32_t id;
|
||||
};
|
||||
|
||||
struct drm_color_pipeline_state {
|
||||
struct drm_color_pipeline *pipeline;
|
||||
|
||||
/* struct drm_colorop_state::link */
|
||||
struct wl_list colorop_state_list;
|
||||
};
|
||||
|
||||
#if CAN_OFFLOAD_COLOR_PIPELINE
|
||||
|
||||
void
|
||||
drm_color_pipeline_state_destroy(struct drm_color_pipeline_state *state);
|
||||
|
||||
struct drm_color_pipeline_state *
|
||||
drm_color_pipeline_state_from_xform(struct drm_plane *plane,
|
||||
struct weston_color_transform *xform,
|
||||
const char *indent);
|
||||
|
||||
struct drm_colorop_3x1d_lut_blob *
|
||||
drm_colorop_3x1d_lut_blob_create(struct drm_device *device,
|
||||
struct weston_color_transform *xform,
|
||||
|
|
@ -68,6 +135,9 @@ drm_colorop_3x1d_lut_blob_search(struct drm_device *device,
|
|||
enum drm_colorop_3x1d_lut_blob_quantization quantization,
|
||||
uint32_t lut_len);
|
||||
|
||||
const char *
|
||||
drm_colorop_type_to_str(struct drm_colorop *colorop);
|
||||
|
||||
void
|
||||
drm_plane_populate_color_pipelines(struct drm_plane *plane,
|
||||
drmModeObjectPropertiesPtr plane_props);
|
||||
|
|
@ -77,6 +147,19 @@ drm_plane_release_color_pipelines(struct drm_plane *plane);
|
|||
|
||||
#else /* CAN_OFFLOAD_COLOR_PIPELINE */
|
||||
|
||||
static inline void
|
||||
drm_color_pipeline_state_destroy(struct drm_color_pipeline_state *state)
|
||||
{
|
||||
}
|
||||
|
||||
static inline struct drm_color_pipeline_state *
|
||||
drm_color_pipeline_state_from_xform(struct drm_plane *plane,
|
||||
struct weston_color_transform *xform,
|
||||
const char *indent)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static inline struct drm_colorop_3x1d_lut_blob *
|
||||
drm_colorop_3x1d_lut_blob_create(struct drm_device *device,
|
||||
struct weston_color_transform *xform,
|
||||
|
|
@ -97,6 +180,12 @@ drm_colorop_3x1d_lut_blob_search(struct drm_device *device,
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static inline const char *
|
||||
drm_colorop_type_to_str(struct drm_colorop *colorop)
|
||||
{
|
||||
return "undefined";
|
||||
}
|
||||
|
||||
static inline void
|
||||
drm_plane_populate_color_pipelines(struct drm_plane *plane,
|
||||
drmModeObjectPropertiesPtr plane_props)
|
||||
|
|
|
|||
|
|
@ -253,6 +253,8 @@ struct drm_device {
|
|||
|
||||
/* struct drm_colorop_3x1d_lut_blob::link */
|
||||
struct wl_list drm_colorop_3x1d_lut_blob_list;
|
||||
/* struct drm_colorop_matrix_blob::link */
|
||||
struct wl_list drm_colorop_matrix_blob_list;
|
||||
|
||||
int reused_state_failures;
|
||||
};
|
||||
|
|
@ -441,6 +443,9 @@ struct drm_plane_state {
|
|||
|
||||
struct weston_paint_node *paint_node; /**< maintained for drm_assign_planes only */
|
||||
|
||||
/* only when a color transformation is being offloaded */
|
||||
struct drm_color_pipeline_state *pipeline_state;
|
||||
|
||||
int32_t src_x, src_y;
|
||||
uint32_t src_w, src_h;
|
||||
int32_t dest_x, dest_y;
|
||||
|
|
|
|||
|
|
@ -4162,6 +4162,7 @@ drm_device_destroy(struct drm_device *device)
|
|||
drm_writeback_destroy(writeback);
|
||||
|
||||
weston_assert_list_empty(ec, &device->drm_colorop_3x1d_lut_blob_list);
|
||||
weston_assert_list_empty(ec, &device->drm_colorop_matrix_blob_list);
|
||||
|
||||
if (device->drm_event_source)
|
||||
wl_event_source_remove(device->drm_event_source);
|
||||
|
|
@ -4502,6 +4503,7 @@ drm_device_create(struct drm_backend *backend,
|
|||
create_planes(device);
|
||||
|
||||
wl_list_init(&device->drm_colorop_3x1d_lut_blob_list);
|
||||
wl_list_init(&device->drm_colorop_matrix_blob_list);
|
||||
|
||||
wl_list_init(&device->writeback_connector_list);
|
||||
if (drm_backend_discover_connectors(device, device->kms_device->udev_device, res) < 0) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@
|
|||
#include <libweston/libweston.h>
|
||||
#include <libweston/backend-drm.h>
|
||||
#include "shared/helpers.h"
|
||||
#include "shared/string-helpers.h"
|
||||
#include "shared/weston-assert.h"
|
||||
#include "shared/weston-drm-fourcc.h"
|
||||
#include "colorops.h"
|
||||
#include "drm-internal.h"
|
||||
#include "pixel-formats.h"
|
||||
#include "presentation-time-server-protocol.h"
|
||||
|
|
@ -1159,6 +1162,28 @@ plane_add_prop(drmModeAtomicReq *req, struct drm_plane *plane,
|
|||
return (ret <= 0) ? -1 : 0;
|
||||
}
|
||||
|
||||
static int
|
||||
colorop_add_prop(drmModeAtomicReq *req, struct drm_colorop *colorop,
|
||||
enum wdrm_colorop_property prop, uint64_t val)
|
||||
{
|
||||
struct drm_plane *plane = colorop->pipeline->plane;
|
||||
struct drm_device *device = plane->device;
|
||||
struct drm_backend *b = device->backend;
|
||||
struct drm_property_info *info = &colorop->props[prop];
|
||||
int ret;
|
||||
|
||||
drm_debug(b, "\t\t\t[COLOROP:%lu] %lu (%s) -> %llu (0x%llx)\n",
|
||||
(unsigned long) colorop->id,
|
||||
(unsigned long) info->prop_id, info->name,
|
||||
(unsigned long long) val, (unsigned long long) val);
|
||||
|
||||
if (info->prop_id == 0)
|
||||
return -1;
|
||||
|
||||
ret = drmModeAtomicAddProperty(req, colorop->id, info->prop_id, val);
|
||||
return (ret <= 0) ? -1 : 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
drm_connector_has_prop(struct drm_connector *connector,
|
||||
enum wdrm_connector_property prop)
|
||||
|
|
@ -1360,6 +1385,156 @@ drm_plane_set_color_range(struct drm_plane *plane,
|
|||
return plane_add_prop(req, plane, WDRM_PLANE_COLOR_RANGE, color_range);
|
||||
}
|
||||
|
||||
static bool
|
||||
colorop_program(drmModeAtomicReq *req, struct drm_colorop *colorop,
|
||||
enum wdrm_colorop_property colorop_prop,
|
||||
uint64_t prop_val, char **err_msg)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (colorop->can_bypass) {
|
||||
ret = colorop_add_prop(req, colorop, WDRM_COLOROP_BYPASS, 0);
|
||||
if (ret < 0) {
|
||||
str_printf(err_msg, "failed to set colorop id %u bypass == false",
|
||||
colorop->id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ret = colorop_add_prop(req, colorop, colorop_prop, prop_val);
|
||||
if (ret < 0) {
|
||||
str_printf(err_msg, "failed to program colorop id %u type %s",
|
||||
colorop->id, drm_colorop_type_to_str(colorop));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
set_interp(drmModeAtomicReq *req, struct drm_colorop *colorop,
|
||||
enum wdrm_colorop_property interp_prop, uint64_t interp_val)
|
||||
{
|
||||
struct drm_property_info *info = &colorop->props[interp_prop];
|
||||
int ret;
|
||||
|
||||
if (info->enum_values[interp_val].valid) {
|
||||
ret = colorop_add_prop(req, colorop, interp_prop, interp_val);
|
||||
if (ret >= 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
drm_colorop_program(drmModeAtomicReq *req, struct drm_colorop_state *colorop_state,
|
||||
const char *indent, char **err_msg)
|
||||
{
|
||||
struct drm_colorop *colorop = colorop_state->colorop;
|
||||
struct drm_color_pipeline *pipeline = colorop->pipeline;
|
||||
struct drm_backend *b = pipeline->plane->device->backend;
|
||||
struct weston_compositor *compositor = pipeline->plane->base.compositor;
|
||||
enum wdrm_colorop_property colorop_prop;
|
||||
uint64_t prop_val;
|
||||
|
||||
switch (colorop_state->object.type) {
|
||||
case COLOROP_OBJECT_TYPE_CURVE:
|
||||
colorop_prop = WDRM_COLOROP_CURVE_1D;
|
||||
prop_val = colorop_state->object.curve_type_prop_val;
|
||||
return colorop_program(req, colorop, colorop_prop, prop_val, err_msg);
|
||||
case COLOROP_OBJECT_TYPE_MATRIX:
|
||||
colorop_prop = WDRM_COLOROP_DATA;
|
||||
prop_val = colorop_state->object.matrix_blob_id;
|
||||
return colorop_program(req, colorop, colorop_prop, prop_val, err_msg);
|
||||
case COLOROP_OBJECT_TYPE_3x1D_LUT:
|
||||
if (!set_interp(req, colorop, WDRM_COLOROP_LUT1D_INTERPOLATION,
|
||||
WDRM_COLOROP_LUT1D_INTERPOLATION_LINEAR))
|
||||
drm_debug(b, "%s[colorop] linear LUT1D interpolation not supported or failed to set;\n"
|
||||
"%susing current value set on driver\n", indent, indent);
|
||||
colorop_prop = WDRM_COLOROP_DATA;
|
||||
prop_val = colorop_state->object.lut_3x1d_blob_id;
|
||||
return colorop_program(req, colorop, colorop_prop, prop_val, err_msg);
|
||||
}
|
||||
weston_assert_not_reached(compositor,
|
||||
"unknown drm_colorop_state object type");
|
||||
}
|
||||
|
||||
static struct drm_colorop_state *
|
||||
drm_colorop_state_iter(struct drm_color_pipeline_state *pipeline_state,
|
||||
struct drm_colorop_state *iter)
|
||||
{
|
||||
struct wl_list *list = &pipeline_state->colorop_state_list;
|
||||
struct wl_list *node;
|
||||
|
||||
if (iter)
|
||||
node = iter->link.next;
|
||||
else
|
||||
node = list->next;
|
||||
|
||||
if (node == list)
|
||||
return NULL;
|
||||
|
||||
return container_of(node, struct drm_colorop_state, link);
|
||||
}
|
||||
|
||||
static int
|
||||
drm_color_pipeline_program(drmModeAtomicReq *req,
|
||||
struct drm_color_pipeline_state *pipeline_state,
|
||||
const char *indent)
|
||||
{
|
||||
struct drm_color_pipeline *pipeline = pipeline_state->pipeline;
|
||||
struct drm_plane *plane = pipeline->plane;
|
||||
struct drm_backend *b = plane->device->backend;
|
||||
struct drm_colorop_state *colorop_state;
|
||||
struct drm_colorop *colorop;
|
||||
char *err_msg;
|
||||
int ret_drm;
|
||||
bool ret;
|
||||
|
||||
drm_debug(b, "%s[PLANE:%lu] %lu (%s) -> %llu (0x%llx)\n",
|
||||
indent, (unsigned long) plane->plane_id,
|
||||
(unsigned long) plane->pipeline_props_id, "COLOR_PIPELINE",
|
||||
(unsigned long long) pipeline_state->pipeline->id,
|
||||
(unsigned long long) pipeline_state->pipeline->id);
|
||||
|
||||
colorop_state = drm_colorop_state_iter(pipeline_state,
|
||||
NULL /* previous colorop state (none) */);
|
||||
wl_list_for_each(colorop, &pipeline->colorop_list, link) {
|
||||
/* If a colorop is not in the colorop state list, bypass it. */
|
||||
if (!colorop_state || colorop != colorop_state->colorop) {
|
||||
weston_assert_true(b->compositor, colorop->can_bypass);
|
||||
|
||||
ret_drm = colorop_add_prop(req, colorop, WDRM_COLOROP_BYPASS, 1);
|
||||
if (ret_drm >= 0)
|
||||
continue;
|
||||
|
||||
drm_debug(b, "%s%s[colorop] failed to set colorop id %u bypass == true",
|
||||
indent, indent, colorop->id);
|
||||
goto err;
|
||||
}
|
||||
|
||||
ret = drm_colorop_program(req, colorop_state, indent, &err_msg);
|
||||
if (!ret) {
|
||||
drm_debug(b, "%s%s[colorop] %s\n", indent, indent, err_msg);
|
||||
free(err_msg);
|
||||
goto err;
|
||||
}
|
||||
|
||||
colorop_state = drm_colorop_state_iter(pipeline_state, colorop_state);
|
||||
}
|
||||
weston_assert_ptr_null(b->compositor, colorop_state);
|
||||
|
||||
/* Set plane pipeline. */
|
||||
drmModeAtomicAddProperty(req, plane->plane_id,
|
||||
plane->pipeline_props_id, pipeline->id);
|
||||
return 0;
|
||||
|
||||
err:
|
||||
drm_debug(b, "%s%s[colorop] failed to program pipeline\n", indent, indent);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
drm_output_apply_state_atomic(struct drm_output_state *state,
|
||||
drmModeAtomicReq *req,
|
||||
|
|
@ -1521,6 +1696,16 @@ drm_output_apply_state_atomic(struct drm_output_state *state,
|
|||
ret |= plane_add_prop(req, plane, WDRM_PLANE_FB_DAMAGE_CLIPS,
|
||||
plane_state->damage_blob_id);
|
||||
|
||||
if (plane->props[WDRM_PLANE_COLOR_PIPELINE].prop_id != 0) {
|
||||
if (plane_state->pipeline_state) {
|
||||
ret |= drm_color_pipeline_program(req, plane_state->pipeline_state,
|
||||
"\t\t\t");
|
||||
} else {
|
||||
ret |= plane_add_prop(req, plane,
|
||||
WDRM_PLANE_COLOR_PIPELINE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (plane_state->fb && plane_state->fb->format)
|
||||
pinfo = plane_state->fb->format;
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
#include "colorops.h"
|
||||
#include "drm-internal.h"
|
||||
#include "shared/weston-assert.h"
|
||||
#include "shared/weston-drm-fourcc.h"
|
||||
|
|
@ -95,6 +96,8 @@ drm_plane_state_free(struct drm_plane_state *state, bool force)
|
|||
state->in_fence_fd = -1;
|
||||
state->zpos = DRM_PLANE_ZPOS_INVALID_PLANE;
|
||||
state->alpha = DRM_PLANE_ALPHA_OPAQUE;
|
||||
drm_color_pipeline_state_destroy(state->pipeline_state);
|
||||
state->pipeline_state = NULL;
|
||||
|
||||
/* Once the damage blob has been submitted, it is refcounted internally
|
||||
* by the kernel, which means we can safely discard it.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
#include "drm-internal.h"
|
||||
|
||||
#include "color.h"
|
||||
#include "colorops.h"
|
||||
#include "color-representation.h"
|
||||
#include "linux-dmabuf.h"
|
||||
#include "presentation-time-server-protocol.h"
|
||||
|
|
@ -141,6 +142,21 @@ drm_output_try_paint_node_on_plane(struct drm_plane_handle *handle,
|
|||
state->fb = drm_fb_ref(fb);
|
||||
state->in_fence_fd = surface->acquire_fence_fd;
|
||||
|
||||
drm_color_pipeline_state_destroy(state->pipeline_state);
|
||||
state->pipeline_state = NULL;
|
||||
if (pnode->surf_xform.transform) {
|
||||
state->pipeline_state =
|
||||
drm_color_pipeline_state_from_xform(plane,
|
||||
pnode->surf_xform.transform,
|
||||
"\t\t\t\t");
|
||||
if (!state->pipeline_state) {
|
||||
drm_debug(b, "\t\t\t\t[view] not placing paint node %s on plane %lu: "
|
||||
"not compatible with surface color xform\n",
|
||||
pnode->internal_name, (unsigned long) plane->plane_id);
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
if (fb->format && fb->format->color_model == COLOR_MODEL_YUV) {
|
||||
struct weston_color_representation color_rep;
|
||||
const struct weston_color_matrix_coef_info *matrix_coef_info;
|
||||
|
|
@ -584,6 +600,14 @@ pnode_can_use_plane(struct drm_output_state *output_state,
|
|||
return false;
|
||||
}
|
||||
|
||||
/* If we have a surf color xform we need to be able to offload that to KMS. */
|
||||
if (pnode->surf_xform.transform && plane->num_color_pipelines == 0) {
|
||||
drm_debug(b, "\t\t\t\t[plane] not trying plane %d: surf_xform present "
|
||||
"but plane has no color pipelines\n",
|
||||
plane->plane_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* If the surface buffer has an in-fence fd, but the plane doesn't
|
||||
* support fences, we can't place the buffer on this plane. */
|
||||
if (pnode->surface->acquire_fence_fd >= 0 &&
|
||||
|
|
@ -1371,8 +1395,8 @@ drm_output_propose_state(struct weston_output *output_base,
|
|||
pnode->try_view_on_plane_failure_reasons |=
|
||||
FAILURE_REASONS_OUTPUT_COLOR_EFFECT;
|
||||
|
||||
if (pnode->surf_xform.transform != NULL ||
|
||||
!pnode->surf_xform.identity_pipeline)
|
||||
if (pnode->surf_xform.transform && (!device->color_pipeline_supported ||
|
||||
!pnode->output->from_blend_to_output_by_backend))
|
||||
pnode->try_view_on_plane_failure_reasons |=
|
||||
FAILURE_REASONS_NO_COLOR_TRANSFORM;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue