[vlc-devel] [PATCH 5/5] opengl: WIP introduce libplacebo sampling filters
Louis Régnier
louis.videolabs at gmail.com
Fri Aug 28 18:49:39 CEST 2020
This patch allows to use libplacebo's polar sampling filters
(ewa_jinc, ewa_lanczos, ewa_ginseng, ewa_hann, HaasnSoft (blurred EWA Hann))
in order to perform scaling.
It introduce a new opengl sampler who handle scaling.
---
modules/video_output/opengl/sampler.c | 466 ++++++++++++++++++++++
modules/video_output/opengl/vout_helper.h | 3 +
modules/video_output/placebo_utils.h | 23 ++
3 files changed, 492 insertions(+)
diff --git a/modules/video_output/opengl/sampler.c b/modules/video_output/opengl/sampler.c
index ace1d723e1..997be3030d 100644
--- a/modules/video_output/opengl/sampler.c
+++ b/modules/video_output/opengl/sampler.c
@@ -29,8 +29,10 @@
#include <vlc_opengl.h>
#ifdef HAVE_LIBPLACEBO
+#include <libplacebo/dummy.h>
#include <libplacebo/shaders.h>
#include <libplacebo/shaders/colorspace.h>
+#include <libplacebo/shaders/sampling.h>
#include "../placebo_utils.h"
#endif
@@ -65,6 +67,15 @@ struct vlc_gl_sampler_priv {
GLfloat conv_matrix[4*4];
/* libplacebo context */
+ struct {
+ GLuint lut_texture;
+ GLuint texture;
+ GLint *pl_vars;
+ GLint *pl_desc;
+ const struct pl_shader_res *sh;
+ const struct pl_shader_res *sh_color;
+ } upscale;
+ const struct pl_gpu *pl_gpu;
struct pl_context *pl_ctx;
struct pl_shader *pl_sh;
const struct pl_shader_res *pl_sh_res;
@@ -84,6 +95,9 @@ struct vlc_gl_sampler_priv {
struct vlc_gl_interop *interop;
};
+static const int polar_filters[] = { 0, SCALE_EWA_JINC, SCALE_EWA_LANCZOS,
+SCALE_EWA_GINSENG, SCALE_HAASNSOFT, SCALE_EWA_HANN };
+
#define PRIV(sampler) container_of(sampler, struct vlc_gl_sampler_priv, sampler)
static const float MATRIX_COLOR_RANGE_LIMITED[4*3] = {
@@ -698,12 +712,457 @@ InitOrientationMatrix(GLfloat matrix[static 4*4],
}
}
+static void
+sampler_scale_load(const struct vlc_gl_sampler *sampler)
+{
+ struct vlc_gl_sampler_priv *priv = PRIV(sampler);
+ const struct vlc_gl_interop *interop = priv->interop;
+ const opengl_vtable_t *vt = priv->vt;
+
+ /*Default texture setup*/
+ if (priv->yuv_color)
+ vt->UniformMatrix4fv(priv->uloc.ConvMatrix, 1, GL_FALSE, priv->conv_matrix);
+
+ for (unsigned i = 0; i < interop->tex_count; ++i)
+ {
+ vt->Uniform1i(priv->uloc.Textures[i], i);
+
+ assert(priv->textures[i] != 0);
+ vt->ActiveTexture(GL_TEXTURE0 + i);
+ vt->BindTexture(interop->tex_target, priv->textures[i]);
+
+ vt->UniformMatrix3fv(priv->uloc.TexCoordsMaps[i], 1, GL_FALSE,
+ priv->var.TexCoordsMaps[i]);
+ }
+
+ const GLfloat *tm = GetTransformMatrix(interop);
+ vt->UniformMatrix4fv(priv->uloc.TransformMatrix, 1, GL_FALSE, tm);
+
+ vt->UniformMatrix4fv(priv->uloc.OrientationMatrix, 1, GL_FALSE,
+ priv->var.OrientationMatrix);
+
+ if (interop->tex_target == GL_TEXTURE_RECTANGLE)
+ {
+ for (unsigned i = 0; i < interop->tex_count; ++i)
+ vt->Uniform2f(priv->uloc.TexSizes[i], priv->tex_widths[i],
+ priv->tex_heights[i]);
+ }
+
+ /*placebo shader requierements*/
+ const struct pl_shader_res *res = priv->upscale.sh;
+ for (int i = 0; res && i < res->num_variables; i++)
+ {
+ GLint loc = priv->upscale.pl_vars[i];
+ assert(loc != -1);
+
+ struct pl_shader_var sv = res->variables[i];
+ struct pl_var var = sv.var;
+
+ assert(!(var.type != PL_VAR_FLOAT));
+ assert(!(var.dim_m > 1 && var.dim_m != var.dim_v));
+
+ const float *f = sv.data;
+ switch (var.dim_m)
+ {
+ case 4: vt->UniformMatrix4fv(loc, 1, GL_FALSE, f); break;
+ case 3: vt->UniformMatrix3fv(loc, 1, GL_FALSE, f); break;
+ case 2: vt->UniformMatrix2fv(loc, 1, GL_FALSE, f); break;
+
+ case 1:
+ switch (var.dim_v)
+ {
+ case 1: vt->Uniform1f(loc, f[0]); break;
+ case 2: vt->Uniform2f(loc, f[0], f[1]); break;
+ case 3: vt->Uniform3f(loc, f[0], f[1], f[2]); break;
+ case 4: vt->Uniform4f(loc, f[0], f[1], f[2], f[3]); break;
+ }
+ break;
+ }
+ }
+
+ for (int i = 0; res && i < res->num_descriptors; i++)
+ {
+ GLint loc = priv->upscale.pl_desc[i];
+ assert(loc != -1);
+
+ vt->ActiveTexture(GL_TEXTURE0 + PICTURE_PLANE_MAX + i );
+ // TODO: bind the correct target_type
+
+ if (i == 0)
+ vt->BindTexture(GL_TEXTURE_1D, priv->upscale.lut_texture);
+ vt->Uniform1i(loc, PICTURE_PLANE_MAX + i);
+ }
+}
+
+static void
+sampler_scale_fetch_locations(struct vlc_gl_sampler *sampler, GLuint program)
+{
+ struct vlc_gl_sampler_priv *priv = PRIV(sampler);
+ const opengl_vtable_t *vt = priv->vt;
+ const struct vlc_gl_interop *interop = priv->interop;
+
+ /*Default texture setup*/
+ if (priv->yuv_color)
+ {
+ priv->uloc.ConvMatrix = vt->GetUniformLocation(program, "ConvMatrix");
+ assert(priv->uloc.ConvMatrix != -1);
+ }
+
+ priv->uloc.TransformMatrix =
+ vt->GetUniformLocation(program, "TransformMatrix");
+ assert(priv->uloc.TransformMatrix != -1);
+
+ priv->uloc.OrientationMatrix =
+ vt->GetUniformLocation(program, "OrientationMatrix");
+ assert(priv->uloc.OrientationMatrix != -1);
+
+ for (unsigned int i = 0; i < interop->tex_count; ++i)
+ {
+ char name[sizeof("TexCoordsMapX")];
+
+ snprintf(name, sizeof(name), "Texture%1u", i);
+ priv->uloc.Textures[i] = vt->GetUniformLocation(program, name);
+ assert(priv->uloc.Textures[i] != -1);
+
+ snprintf(name, sizeof(name), "TexCoordsMap%1u", i);
+ priv->uloc.TexCoordsMaps[i] = vt->GetUniformLocation(program, name);
+ assert(priv->uloc.TexCoordsMaps[i] != -1);
+
+ if (interop->tex_target == GL_TEXTURE_RECTANGLE)
+ {
+ snprintf(name, sizeof(name), "TexSize%1u", i);
+ priv->uloc.TexSizes[i] = vt->GetUniformLocation(program, name);
+ assert(priv->uloc.TexSizes[i] != -1);
+ }
+ }
+
+ const struct pl_shader_res *res = priv->upscale.sh;
+ GLint *pl_vars = priv->upscale.pl_vars;
+ for (int i = 0; i < res->num_variables; i++)
+ {
+ struct pl_shader_var sv = res->variables[i];
+ pl_vars[i] = vt->GetUniformLocation(program, sv.var.name);
+ assert(pl_vars[i] != -1);
+ }
+
+ GLint *pl_desc = priv->upscale.pl_desc;
+ for (int i = 0; i < res->num_descriptors; i++)
+ {
+ struct pl_shader_desc desc = res->descriptors[i];
+ pl_desc[i] = vt->GetUniformLocation(program, desc.desc.name);
+ assert(pl_desc[i] != -1);
+ }
+}
+
+#define ADD(x) vlc_memstream_puts(&ms, x)
+#define ADDF(x, ...) vlc_memstream_printf(&ms, x, ##__VA_ARGS__)
+
+static int
+opengl_init_shader_scale(struct vlc_gl_sampler *sampler, GLenum tex_target,
+ vlc_fourcc_t chroma, video_color_space_t yuv_space,
+ video_orientation_t orientation)
+{
+ struct vlc_gl_sampler_priv *priv = PRIV(sampler);
+ struct vlc_gl_interop *interop = priv->interop;
+ const opengl_vtable_t *vt = priv->vt;
+
+ const char *swizzle_per_tex[PICTURE_PLANE_MAX] = { NULL };
+ const bool is_yuv = vlc_fourcc_IsYUV(chroma);
+
+ const vlc_chroma_description_t *desc = vlc_fourcc_GetChromaDescription(chroma);
+ if (desc == NULL)
+ return VLC_EGENERIC;
+
+ InitOrientationMatrix(priv->var.OrientationMatrix, orientation);
+
+ if (is_yuv)
+ {
+ int ret;
+ ret = sampler_yuv_base_init(sampler, chroma, desc, yuv_space);
+ if (ret != VLC_SUCCESS)
+ return ret;
+ ret = opengl_init_swizzle(interop, swizzle_per_tex, chroma, desc);
+ if (ret != VLC_SUCCESS)
+ return ret;
+ }
+
+ const char *sampler_name;
+ switch (tex_target)
+ {
+ case GL_TEXTURE_EXTERNAL_OES:
+ sampler_name = "samplerExternalOES";
+ break;
+ case GL_TEXTURE_2D:
+ sampler_name = "sampler2D";
+ break;
+ case GL_TEXTURE_RECTANGLE:
+ sampler_name = "sampler2DRect";
+ break;
+ default:
+ vlc_assert_unreachable();
+ }
+
+ struct pl_plane_data planes[4] = { 0 };
+ int nb_planes = vlc_placebo_PlaneFormat(&interop->fmt_out, planes);
+ assert(nb_planes);
+
+ int num_components = 0;
+ while (planes[0].component_size[num_components])
+ ++num_components;
+
+ struct pl_tex_params tex_params = {
+ .h = interop->fmt_out.i_height * 2,
+ .w = interop->fmt_out.i_width * 2,
+ .d = 0,
+ .format = pl_find_fmt(priv->pl_gpu, PL_FMT_UNORM, num_components, 0, 0, PL_FMT_CAP_RENDERABLE),
+ .sampleable = true,
+ .sample_mode = PL_TEX_SAMPLE_LINEAR,
+ .address_mode = PL_TEX_ADDRESS_CLAMP,
+ .user_data = NULL,
+ };
+ assert(tex_params.format);
+
+ struct pl_sample_src sample_src =
+ {
+ .tex = NULL,
+ .sampler_params = tex_params,
+ .sampled_w = interop->fmt_out.i_width,
+ .sampled_h = interop->fmt_out.i_height,
+ .new_w = 0,
+ .new_h = 0,
+ .scale = 0,
+ .components = num_components,
+ };
+
+ struct pl_shader_obj *lut = NULL;
+ int filter_id = polar_filters[var_InheritInteger(priv->gl, "pl-scale")];
+ struct pl_sample_filter_params filter_params = {
+ .filter = *scale_config[filter_id],
+ .lut = &lut,
+ };
+
+ struct pl_glsl_desc gl_desc = { .version = 120, .gles = false, .vulkan = false, };
+
+ struct pl_shader *shader_color = pl_shader_alloc(priv->pl_ctx,
+ &(struct pl_shader_params) { .id = 0, .gpu = priv->pl_gpu, .glsl = gl_desc, });
+ assert(shader_color);
+
+ struct pl_color_map_params color_params = pl_color_map_default_params;
+ color_params.intent = var_InheritInteger(priv->gl, "rendering-intent");
+ color_params.tone_mapping_algo = var_InheritInteger(priv->gl, "tone-mapping");
+ color_params.tone_mapping_param = var_InheritFloat(priv->gl, "tone-mapping-param");
+ #if PL_API_VER >= 10
+ color_params.desaturation_strength = var_InheritFloat(priv->gl, "desat-strength");
+ color_params.desaturation_exponent = var_InheritFloat(priv->gl, "desat-exponent");
+ color_params.desaturation_base = var_InheritFloat(priv->gl, "desat-base");
+ #else
+ color_params.tone_mapping_desaturate = var_InheritFloat(priv->gl, "tone-mapping-desat");
+ #endif
+ color_params.gamut_warning = var_InheritBool(priv->gl, "tone-mapping-warn");
+
+ struct pl_color_space dst_space = pl_color_space_unknown;
+ dst_space.primaries = var_InheritInteger(priv->gl, "target-prim");
+ dst_space.transfer = var_InheritInteger(priv->gl, "target-trc");
+
+ pl_shader_color_map(shader_color, &color_params,
+ vlc_placebo_ColorSpace(&interop->fmt_out),
+ dst_space, NULL, false);
+
+ struct pl_shader_obj *dither_state = NULL;
+ int method = var_InheritInteger(priv->gl, "dither-algo");
+ if (method >= 0)
+ {
+ unsigned out_bits = 0;
+ int override = var_InheritInteger(priv->gl, "dither-depth");
+ if (override > 0)
+ out_bits = override;
+ else
+ {
+ GLint fb_depth = 0;
+ #if !defined(USE_OPENGL_ES2)
+ /* fetch framebuffer depth (we are already bound to the default one). */
+ if (vt->GetFramebufferAttachmentParameteriv != NULL)
+ vt->GetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_BACK_LEFT,
+ GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE,
+ &fb_depth);
+ #endif
+ if (fb_depth <= 0)
+ fb_depth = 8;
+ out_bits = fb_depth;
+ }
+
+ pl_shader_dither(shader_color, out_bits, &dither_state, &(struct pl_dither_params) {
+ .method = method,
+ });
+ pl_shader_obj_destroy(&dither_state);
+ }
+ priv->upscale.sh_color = pl_shader_finalize(shader_color);
+
+ GLenum texture_type;
+ const char *sampler_lut;
+ assert(filter_params.filter.polar);
+ if (filter_params.filter.polar)
+ {
+ struct pl_shader *shader = pl_shader_alloc(priv->pl_ctx,
+ &(struct pl_shader_params) { .id = 0, .gpu = priv->pl_gpu, .glsl = gl_desc, });
+ assert(shader);
+
+ pl_shader_sample_polar(shader, &sample_src, &filter_params);
+ priv->upscale.sh = pl_shader_finalize(shader);
+ texture_type = GL_TEXTURE_1D;
+ sampler_lut = "sampler1D";
+ }
+
+ const struct pl_shader_res *result = priv->upscale.sh;
+ assert(result);
+ float *data = NULL;
+ unsigned lut_w, lut_h;
+ for (int n = 0; n < result->num_descriptors; n++)
+ {
+ const struct pl_shader_desc *sd = &result->descriptors[n];
+
+ if(sd->desc.type != PL_DESC_SAMPLED_TEX)
+ continue;
+
+ const struct pl_tex *tex = sd->object;
+ data = (float *) pl_tex_dummy_data(tex);
+ lut_w = tex->params.w;
+ lut_h = tex->params.h;
+ if (data)
+ break;
+ }
+ assert(data);
+
+ vt->ActiveTexture(GL_TEXTURE0 + PICTURE_PLANE_MAX);
+ vt->GenTextures(1, &priv->upscale.lut_texture);
+ assert(lut_h == 0);
+ vt->BindTexture(texture_type, priv->upscale.lut_texture);
+ vt->TexImage1D(texture_type, 0, GL_RGBA, lut_w, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
+ vt->TexParameteri(texture_type, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ vt->TexParameteri(texture_type, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ vt->TexParameteri(texture_type, GL_TEXTURE_WRAP_S, GL_REPEAT);
+ vt->TexParameteri(texture_type, GL_TEXTURE_WRAP_T, GL_REPEAT);
+ vt->ActiveTexture(GL_TEXTURE0);
+
+ struct vlc_memstream ms;
+ if (vlc_memstream_open(&ms) != 0)
+ return VLC_EGENERIC;
+
+ if (is_yuv)
+ ADD("uniform mat4 ConvMatrix;\n");
+
+ ADD("uniform mat4 TransformMatrix;\n"
+ "uniform mat4 OrientationMatrix;\n");
+ for (unsigned i = 0; i < interop->tex_count; ++i)
+ ADDF("uniform %s Texture%u;\n"
+ "uniform mat3 TexCoordsMap%u;\n", sampler_name, i, i);
+
+ priv->upscale.pl_vars = calloc(priv->upscale.sh->num_variables, sizeof(GLint));
+ priv->upscale.pl_desc = calloc(priv->upscale.sh->num_descriptors, sizeof(GLint));
+
+ for (int n = 0; n < result->num_variables; n++)
+ {
+ const struct pl_shader_var *shader_var = &result->variables[n];
+ const struct pl_var *var = &shader_var->var;
+
+ ADDF("uniform %s %s;\n", pl_var_glsl_type_name(*var), var->name);
+ }
+
+ for (int n = 0; n < result->num_vertex_attribs; n++)
+ {
+ const struct pl_shader_va *vertex_attribs = &result->vertex_attribs[n];
+ const struct pl_vertex_attrib *attr = &vertex_attribs->attr;
+ const struct pl_fmt *fmt = attr->fmt;
+ const char *name = attr->name;
+
+ ADDF("uniform %s %s;\n", fmt->glsl_type, name);
+ }
+
+ for (int n = 0; n < result->num_descriptors; n++)
+ {
+ const struct pl_shader_desc *sd = &result->descriptors[n];
+ const struct pl_desc *sh_desc = &sd->desc;
+
+ ADDF("uniform %s %s;\n", sampler_lut, sh_desc->name);
+ }
+ ADDF("%s", priv->upscale.sh->glsl);
+ ADDF("%s", priv->upscale.sh_color->glsl);
+
+ ADD("vec4 vlc_texture(vec2 pic_coords) {\n"
+ " vec3 pic_hcoords = vec3((TransformMatrix * OrientationMatrix * "
+ "vec4(pic_coords, 0.0, 1.0)).st, 1.0);\n"
+ " vec2 tex_coords;\n");
+
+
+ unsigned color_count;
+ if (is_yuv) {
+ ADD(" vec4 texel;\n"
+ " vec4 pixel = vec4(0.0, 0.0, 0.0, 1.0);\n");
+
+ unsigned color_idx = 0;
+ for (unsigned i = 0; i < interop->tex_count; ++i)
+ {
+ const char *swizzle = swizzle_per_tex[i];
+ assert(swizzle);
+ size_t swizzle_count = strlen(swizzle);
+ ADDF(" tex_coords = (TexCoordsMap%u * pic_hcoords).st;\n", i);
+ if (tex_target == GL_TEXTURE_RECTANGLE)
+ {
+ /* The coordinates are in texels values, not normalized */
+ ADDF(" tex_coords = vec2(tex_coords.x * TexSize%u.x,\n"
+ " tex_coords.y * TexSize%u.y);\n", i, i);
+ }
+ ADDF(" texel = %s(Texture%.1u, tex_coords);\n", priv->upscale.sh->name, i);
+ for (unsigned j = 0; j < swizzle_count; ++j)
+ {
+ ADDF(" pixel[%u] = texel.%c;\n", color_idx, swizzle[j]);
+ color_idx++;
+ assert(color_idx <= PICTURE_PLANE_MAX);
+ }
+ }
+ ADD(" vec4 result = ConvMatrix * pixel;\n");
+ color_count = color_idx;
+ }
+ else
+ {
+ ADD(" tex_coords = (TexCoordsMap0 * pic_hcoords).st;\n");
+ ADDF(" vec4 result = %s(Texture0, tex_coords);\n", priv->upscale.sh->name);
+ color_count = 1;
+ }
+ assert(yuv_space == COLOR_SPACE_UNDEF || color_count == 3);
+
+ ADDF(" return result = %s(result);\n", priv->upscale.sh_color->name);
+ ADD("}\n");
+#undef ADD
+#undef ADDF
+
+ if (vlc_memstream_close(&ms) != 0)
+ return VLC_EGENERIC;
+
+ sampler->shader.extensions = NULL;
+ sampler->shader.body = ms.ptr;
+
+ static const struct vlc_gl_sampler_ops ops = {
+ .fetch_locations = sampler_scale_fetch_locations,
+ .load = sampler_scale_load,
+ };
+ sampler->ops = &ops;
+
+ return VLC_SUCCESS;
+}
+
static int
opengl_fragment_shader_init(struct vlc_gl_sampler *sampler, GLenum tex_target,
vlc_fourcc_t chroma, video_color_space_t yuv_space,
video_orientation_t orientation)
{
struct vlc_gl_sampler_priv *priv = PRIV(sampler);
+ int scale_filter = var_InheritInteger(priv->gl, "pl-scale");
+ if(scale_filter > 0 && scale_filter < 6)
+ {
+ return opengl_init_shader_scale(sampler, tex_target, chroma, yuv_space,
+ orientation);
+ }
struct vlc_gl_interop *interop = priv->interop;
@@ -971,6 +1430,7 @@ vlc_gl_sampler_New(struct vlc_gl_interop *interop)
# endif
.vulkan = false,
},
+ .limits = { 0 },
};
const opengl_vtable_t *vt = interop->vt;
@@ -1076,6 +1536,12 @@ vlc_gl_sampler_Delete(struct vlc_gl_sampler *sampler)
#ifdef HAVE_LIBPLACEBO
FREENULL(priv->uloc.pl_vars);
+ if (priv->upscale.pl_vars)
+ FREENULL(priv->upscale.pl_vars);
+ if (priv->upscale.pl_vars)
+ FREENULL(priv->upscale.pl_desc);
+ if (priv->pl_gpu)
+ pl_gpu_dummy_destroy(&priv->pl_gpu);
if (priv->pl_ctx)
pl_context_destroy(&priv->pl_ctx);
#endif
diff --git a/modules/video_output/opengl/vout_helper.h b/modules/video_output/opengl/vout_helper.h
index 482eabb554..3937c1e3e6 100644
--- a/modules/video_output/opengl/vout_helper.h
+++ b/modules/video_output/opengl/vout_helper.h
@@ -51,6 +51,9 @@
#endif
#define add_glopts_placebo() \
+ set_section(N_("Scaling algorithm"), NULL) \
+ add_integer("pl-scale", 0, SCALE_FILTER_TEXT, SCALE_FILTER_LONGTEXT, false) \
+ change_integer_list(upscale_filters_values, upscale_filters_text) \
set_section(N_("Colorspace conversion"), NULL) \
add_integer("rendering-intent", pl_color_map_default_params.intent, \
RENDER_INTENT_TEXT, RENDER_INTENT_LONGTEXT, false) \
diff --git a/modules/video_output/placebo_utils.h b/modules/video_output/placebo_utils.h
index 99c3f318b5..cdfb040f16 100644
--- a/modules/video_output/placebo_utils.h
+++ b/modules/video_output/placebo_utils.h
@@ -330,6 +330,29 @@ static const struct pl_filter_config *const scale_config[] = {
[SCALE_CUSTOM] = NULL,
};
+#define SCALE_FILTER_TEXT "Upscale filters"
+#define SCALE_FILTER_LONGTEXT "Upscale filters"
+
+static const int upscale_filters_values[] = {
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+};
+
+static const char * const upscale_filters_text[] = {
+ "No filter",
+ "Unwindowed EWA Jinc (clipped)",
+ "Jinc / EWA Lanczos 3 taps (high quality, slow)",
+ "EWA Ginseng",
+ "EWA Hann",
+ "HaasnSoft (blurred EWA Hann)",
+ "EWA Robidoux",
+ "EWA RobidouxSharp",
+};
+
#define UPSCALER_PRESET_TEXT "Upscaler preset"
#define DOWNSCALER_PRESET_TEXT "Downscaler preset"
#define SCALER_PRESET_LONGTEXT "Choose from one of the built-in scaler presets. If set to custom, you can choose your own combination of kernel/window functions."
--
2.28.0
More information about the vlc-devel
mailing list