PR #25424 opened by Niklas Haas (haasn) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25424 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25424.patch
This is a respin of the stalled !23555, rebased onto the current codebase. >From d50494f68a1a5c722b9c2deb2162b39646c315af Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Sat, 24 Oct 2026 17:15:00 +0200 Subject: [PATCH 01/20] swscale/ops: keep track of per-component resolution for each op Adds metadata for the conceptual plane size for each op. This is needed for keeping track of subsampled planes, which may need to be split into subpasses for each uniquely sized component. It's important to note that this metadata will not actually survive the ops compilation/translation stage; at this point all op lists should be fully resolved into distinct passes that operate at a single resolution only. Sponsored-by: Sovereign Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/format.c | 7 +++++ libswscale/ops.c | 56 +++++++++++++++++++++++++++---------- libswscale/ops.h | 3 ++ libswscale/ops_optimizer.c | 2 ++ tests/ref/fate/sws-ops-list | 2 +- 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/libswscale/format.c b/libswscale/format.c index 4af9c91b351..fa42f1685f7 100644 --- a/libswscale/format.c +++ b/libswscale/format.c @@ -1112,6 +1112,13 @@ static int decode_pixfmt(SwsOpList *ops, const SwsFormat *fmt) for (int i = 0; i < rw_op.elems; i++) { inputs[i].flags = (integer ? SWS_COMP_EXACT : 0) | (swapped ? SWS_COMP_SWAPPED : 0); + + /* Calculate plane dimensions */ + const bool chroma = i == 1 || i == 2; + const int sub_x = chroma ? desc->log2_chroma_w : 0; + const int sub_y = chroma ? desc->log2_chroma_h : 0; + inputs[i].width = AV_CEIL_RSHIFT(fmt->width, sub_x); + inputs[i].height = AV_CEIL_RSHIFT(fmt->height, sub_y); } /* Generate value range information for simple unpacked formats */ diff --git a/libswscale/ops.c b/libswscale/ops.c index 2660d66d063..fb96e3f68dd 100644 --- a/libswscale/ops.c +++ b/libswscale/ops.c @@ -327,8 +327,12 @@ static SwsCompFlags merge_comp_flags(SwsCompFlags a, SwsCompFlags b) return ((a & b) & flags_and) | ((a | b) & flags_or); } -static void apply_filter_weights(SwsOpPad *pad, const SwsFilterWeights *weights) +static void apply_filter_weights(SwsOpPad *pad, SwsOpType filter, + const SwsFilterWeights *weights) { + if (!filter) + return; + const AVRational64 posw = { weights->sum_positive, SWS_FILTER_SCALE }; const AVRational64 negw = { weights->sum_negative, SWS_FILTER_SCALE }; @@ -340,6 +344,15 @@ static void apply_filter_weights(SwsOpPad *pad, const SwsFilterWeights *weights) const AVRational64 min = pad->min, max = pad->max; pad->min = ff_add_q64(ff_mul_q64(min, posw), ff_mul_q64(max, negw)); pad->max = ff_add_q64(ff_mul_q64(min, negw), ff_mul_q64(max, posw)); + + /* Update plane dimensions */ + if (filter == SWS_OP_FILTER_H && pad->width) { + av_assert1(pad->width == weights->src_size); + pad->width = weights->dst_size; + } else if (filter == SWS_OP_FILTER_V && pad->height) { + av_assert1(pad->height == weights->src_size); + pad->height = weights->dst_size; + } } static void update_output_pads(SwsOpList *ops, SwsOp *op) @@ -353,6 +366,7 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op) out[i].type = SWS_PIXEL_NONE; out[i].flags = SWS_COMP_IDENTITY; out[i].dep_in = SWS_COMP_NONE; + out[i].width = out[i].height = 0; } switch (op->op) { @@ -383,11 +397,18 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op) SwsOpPad *dst = &out[I]; \ const SwsOpPad *src = &in[J]; \ SwsCompFlags flags = src->flags; \ - if (!src->type || dst->type && dst->type != src->type) \ + if (!src->type || \ + (dst->type && dst->type != src->type) || \ + (dst->width && src->width && dst->width != src->width) || \ + (dst->height && src->width && dst->height != src->height)) \ invalid |= SWS_COMP(I); \ dst->flags = merge_comp_flags(dst->flags, (EXPR)); \ dst->dep_in |= src->dep_in; \ dst->type = src->type; \ + if (src->width) \ + dst->width = src->width; \ + if (src->height) \ + dst->height = src->height; \ } while (0) #define RESET(I) do { \ @@ -396,13 +417,16 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op) dst->flags = 0; \ dst->min = dst->max = (AVRational64) {0}; \ dst->dep_in = SWS_COMP_NONE; \ + dst->width = dst->height = 0; \ } while (0) switch (op->op) { case SWS_OP_READ: /* Active components are taken from the user-provided values, * other components are explicitly stripped */ - for (int i = 0; i < op->rw.elems; i++) { + av_assert0(op->rw.elems <= 4); + const int elems = op->rw.elems; + for (int i = 0; i < elems; i++) { int idx = 0; switch (op->rw.mode) { case SWS_RW_PALETTE: idx = i; break; @@ -412,23 +436,22 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op) SwsOpPad *src = &ops->inputs[idx]; SwsOpPad *dst = &out[i]; - dst->type = op->rw.type; + dst->type = op->rw.filter.op ? op->rw.filter.type : op->rw.type; dst->flags = src->flags & SWS_COMP_DIRTY; dst->min = src->min; dst->max = src->max; + dst->width = src->width; + dst->height = src->height; dst->dep_in = SWS_COMP(i); + apply_filter_weights(dst, op->rw.filter.op, op->rw.filter.kernel); - if (op->rw.filter.op) { - apply_filter_weights(dst, op->rw.filter.kernel); - dst->type = op->rw.filter.type; - } else if (op->rw.mode == SWS_RW_PLANAR && !op->rw.frac) { - /* Don't mark packed or fractional reads as a copy, because - * the read operation implicitly unpacks the data into - * separate components. The only case in which op lists - * involving such reads can be refcopies is in the case of - * a true no-op, which is covered by the no-op check. */ + /* Don't mark packed or fractional reads as a copy, because the read + * operation implicitly unpacks the data into separate components. + * The only case in which op lists involving such reads can be + * refcopies is in the case of a true no-op, which is covered by + * the no-op check. */ + if (op->rw.mode == SWS_RW_PLANAR && !op->rw.frac && !op->rw.filter.op) dst->flags |= SWS_COMP_COPY; - } } /* Forward unused/untouched components */ @@ -576,7 +599,7 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op) case SWS_OP_FILTER_V: { for (int i = 0; i < 4; i++) { FORWARD(i, i, flags & SWS_COMP_DIRTY); - apply_filter_weights(&out[i], op->filter.kernel); + apply_filter_weights(&out[i], op->op, op->filter.kernel); out[i].type = op->filter.type; } break; @@ -1073,6 +1096,9 @@ void ff_sws_op_list_print(void *log, int lev, int lev_extra, print_deps(bp, pad->dep_in); av_bprintf(bp, " ->"); print_deps(bp, pad->dep_out); + if (pad->width || pad->height) + av_bprintf(bp, ", %dx%d", pad->width, pad->height); + av_bprintf(bp, " %7s", describe_comp_flags(pad->flags)); if (pad->min.den || pad->max.den) { diff --git a/libswscale/ops.h b/libswscale/ops.h index 25b137f8b47..36f99192520 100644 --- a/libswscale/ops.h +++ b/libswscale/ops.h @@ -244,6 +244,9 @@ typedef struct SwsOpPad { /* Keeps track of input (forward) and output (reverse) dependencies */ SwsCompMask dep_in, dep_out; + + /* Current resolution, or 0 if unknown/indeterminate */ + int width, height; } SwsOpPad; typedef struct SwsOp SwsOp; diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c index 83b69f29ab1..90025d4c4bb 100644 --- a/libswscale/ops_optimizer.c +++ b/libswscale/ops_optimizer.c @@ -1210,6 +1210,8 @@ int ff_sws_op_list_split_at(SwsOpList *ops1, SwsOpList **out_ops2, int index) ops2->inputs[i].flags = prev->outputs[idx].flags; ops2->inputs[i].min = prev->outputs[idx].min; ops2->inputs[i].max = prev->outputs[idx].max; + ops2->inputs[i].width = prev->outputs[idx].width; + ops2->inputs[i].height = prev->outputs[idx].height; } ff_sws_op_list_remove_at(ops1, index, ops1->num_ops - index); diff --git a/tests/ref/fate/sws-ops-list b/tests/ref/fate/sws-ops-list index f5519b5e194..d66a85bdf1c 100644 --- a/tests/ref/fate/sws-ops-list +++ b/tests/ref/fate/sws-ops-list @@ -1 +1 @@ -5e7e06cdc6f4c715b0a7ce5ed278e084 +33a22c71b709405a5c3ccf687bc128f9 -- 2.54.0 >From fad9edabb758a98592374729ce9c9bc330233baf Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Sat, 24 Oct 2026 17:17:35 +0200 Subject: [PATCH 02/20] swscale/ops_optimizer: use op metadata to determine size More robust than inferring this from the read op. Sponsored-by: Sovereign Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops_optimizer.c | 41 ++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c index 90025d4c4bb..87567d2214a 100644 --- a/libswscale/ops_optimizer.c +++ b/libswscale/ops_optimizer.c @@ -1153,17 +1153,26 @@ static enum AVPixelFormat get_planar_fmt(SwsPixelType type, int nb_planes) return AV_PIX_FMT_NONE; } -static void get_input_size(const SwsOpList *ops, SwsFormat *fmt) +static int get_op_size(const SwsOp *op, int *out_w, int *out_h) { - fmt->width = ops->src.width; - fmt->height = ops->src.height; - - const SwsOp *read = ff_sws_op_list_input(ops); - if (read && read->rw.filter.op == SWS_OP_FILTER_V) { - fmt->height = read->rw.filter.kernel->dst_size; - } else if (read && read->rw.filter.op == SWS_OP_FILTER_H) { - fmt->width = read->rw.filter.kernel->dst_size; + bool first = true; + for (int i = 0; i < 4; i++) { + if (!SWS_OP_NEEDED(op, i)) + continue; + const int comp_w = op->outputs[i].width; + const int comp_h = op->outputs[i].height; + if (!comp_w || !comp_h) + return AVERROR(EINVAL); + if (first) { + *out_w = comp_w; + *out_h = comp_h; + first = false; + } else if (*out_w != comp_w || *out_h != comp_h) { + return AVERROR(EINVAL); + } } + + return 0; } int ff_sws_op_list_split_at(SwsOpList *ops1, SwsOpList **out_ops2, int index) @@ -1177,6 +1186,11 @@ int ff_sws_op_list_split_at(SwsOpList *ops1, SwsOpList **out_ops2, int index) const SwsOp *op = &ops1->ops[index]; const SwsOp *prev = &ops1->ops[index - 1]; + int inter_w, inter_h; + ret = get_op_size(prev, &inter_w, &inter_h); + if (ret < 0) + return ret; + SwsOpList *ops2 = ff_sws_op_list_duplicate(ops1); if (!ops2) return AVERROR(ENOMEM); @@ -1200,8 +1214,9 @@ int ff_sws_op_list_split_at(SwsOpList *ops1, SwsOpList **out_ops2, int index) /* Determine metadata for the intermediate format */ const SwsPixelType type = ff_sws_op_input_type(op); ops2->src.format = get_planar_fmt(type, nb_planes); - ops2->src.desc = av_pix_fmt_desc_get(ops2->src.format); - get_input_size(ops1, &ops2->src); + ops2->src.desc = av_pix_fmt_desc_get(ops2->src.format); + ops2->src.width = inter_w; + ops2->src.height = inter_h; ops1->dst = ops2->src; for (int i = 0; i < nb_planes; i++) { @@ -1210,8 +1225,8 @@ int ff_sws_op_list_split_at(SwsOpList *ops1, SwsOpList **out_ops2, int index) ops2->inputs[i].flags = prev->outputs[idx].flags; ops2->inputs[i].min = prev->outputs[idx].min; ops2->inputs[i].max = prev->outputs[idx].max; - ops2->inputs[i].width = prev->outputs[idx].width; - ops2->inputs[i].height = prev->outputs[idx].height; + ops2->inputs[i].width = inter_w; + ops2->inputs[i].height = inter_h; } ff_sws_op_list_remove_at(ops1, index, ops1->num_ops - index); -- 2.54.0 >From 96682ad4855f6e187107efa3dd0eb24c829f229c Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Sat, 20 Jun 2026 11:41:54 +0200 Subject: [PATCH 03/20] swscale/ops: add ff_sws_comp_mask_swizzle_inv() For when the swizzle op is *before* the comp mask, rather than after. Sponsored-by: Sovereign Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops.c | 13 +++++++++++++ libswscale/ops.h | 1 + 2 files changed, 14 insertions(+) diff --git a/libswscale/ops.c b/libswscale/ops.c index fb96e3f68dd..0ccf9ffdc8f 100644 --- a/libswscale/ops.c +++ b/libswscale/ops.c @@ -120,6 +120,19 @@ void ff_sws_comp_mask_swizzle(SwsCompMask *mask, const SwsSwizzleOp *swiz) *mask = res; } +void ff_sws_comp_mask_swizzle_inv(SwsCompMask *mask, const SwsSwizzleOp *swiz) +{ + const SwsCompMask orig = *mask; + SwsCompMask res = 0; + for (int i = 0; i < 4; i++) { + const int src = swiz->in[i]; + if (SWS_COMP_TEST(orig, i)) + res |= SWS_COMP(src); + } + + *mask = res; +} + SwsCompMask ff_sws_comp_mask_needed(const SwsOp *op) { SwsCompMask mask = 0; diff --git a/libswscale/ops.h b/libswscale/ops.h index 36f99192520..f95714c211c 100644 --- a/libswscale/ops.h +++ b/libswscale/ops.h @@ -140,6 +140,7 @@ typedef struct SwsSwizzleOp { #define SWS_SWIZZLE(X,Y,Z,W) ((SwsSwizzleOp) { .in = {X, Y, Z, W} }) void ff_sws_comp_mask_swizzle(SwsCompMask *mask, const SwsSwizzleOp *swiz); +void ff_sws_comp_mask_swizzle_inv(SwsCompMask *mask, const SwsSwizzleOp *swiz); typedef struct SwsShiftOp { uint8_t amount; /* number of bits to shift */ -- 2.54.0 >From 092ceaa96c2dc8d1e1c7453ffd729079e08c916f Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 8 Oct 2026 20:13:41 +0200 Subject: [PATCH 04/20] swscale/ops: add SwsCompMask to SwsFilterOp To allow scaling only some components. Needed for chroma subsampling. This commit merely adds the metadata itself. Future commits will add the integration into the optimizer etc. Sponsored-by: Sovereign Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/format.c | 17 ++++++++++------- libswscale/ops.c | 3 +++ libswscale/ops.h | 1 + 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/libswscale/format.c b/libswscale/format.c index fa42f1685f7..8757c0282c4 100644 --- a/libswscale/format.c +++ b/libswscale/format.c @@ -1718,7 +1718,8 @@ static SwsScaler get_scaler_fallback(SwsContext *ctx) } static int add_filter(SwsContext *ctx, SwsPixelType type, SwsOpList *ops, - SwsOpType filter, int src_size, int dst_size) + SwsCompMask comps, SwsOpType filter, + int src_size, int dst_size) { if (src_size == dst_size) return 0; /* no-op */ @@ -1739,10 +1740,10 @@ static int add_filter(SwsContext *ctx, SwsPixelType type, SwsOpList *ops, int mean = sqrt((int64_t) src_size * dst_size); if (mean == src_size || mean == dst_size) return AVERROR_BUG; /* sanity, prevent infinite loop */ - ret = add_filter(ctx, type, ops, filter, src_size, mean); + ret = add_filter(ctx, type, ops, comps, filter, src_size, mean); if (ret < 0) return ret; - return add_filter(ctx, type, ops, filter, mean, dst_size); + return add_filter(ctx, type, ops, comps, filter, mean, dst_size); } else if (ret < 0) { return ret; } @@ -1751,22 +1752,24 @@ static int add_filter(SwsContext *ctx, SwsPixelType type, SwsOpList *ops, .op = filter, .filter.kernel = kernel, .filter.type = type, + .filter.comps = comps, }); } static int add_filters(SwsContext *ctx, SwsPixelType type, SwsOpList *ops, - const SwsFormat *src, const SwsFormat *dst) + SwsCompMask comps, const SwsFormat *src, + const SwsFormat *dst) { /** * Always perform horizontal scaling first, since it's much more likely to * benefit from small integer optimizations; we should maybe flip the order * here if we're downscaling the vertical resolution by a lot, though. */ - int ret = add_filter(ctx, type, ops, SWS_OP_FILTER_H, src->width, dst->width); + int ret = add_filter(ctx, type, ops, comps, SWS_OP_FILTER_H, src->width, dst->width); if (ret < 0) return ret; - return add_filter(ctx, type, ops, SWS_OP_FILTER_V, src->height, dst->height); + return add_filter(ctx, type, ops, comps, SWS_OP_FILTER_V, src->height, dst->height); } /* The input and output domain are assumed to be normalized [0, 1] RGBA */ @@ -1824,7 +1827,7 @@ static int fmt_convert(SwsContext *ctx, const SwsFormat *src, const SwsFormat *d const SwsPixelType type = SWS_PIXEL_F32; RET(decode_pixfmt(ops, src)); RET(decode_colors(ctx, type, ops, src, incomplete)); - RET(add_filters(ctx, type, ops, src, dst)); + RET(add_filters(ctx, type, ops, SWS_COMP_ALL, src, dst)); if (lut3d) RET(apply_lut3d(ctx, type, ops, lut3d)); RET(encode_colors(ctx, type, ops, src, dst, incomplete)); diff --git a/libswscale/ops.c b/libswscale/ops.c index 0ccf9ffdc8f..03d887ef72c 100644 --- a/libswscale/ops.c +++ b/libswscale/ops.c @@ -1050,6 +1050,9 @@ void ff_sws_op_desc(AVBPrint *bp, const SwsOp *op) av_bprintf(bp, "%-20s: %d -> %d %s (%d taps)", name, kernel->src_size, kernel->dst_size, kernel->name, kernel->filter_size); + const SwsCompMask comps = op->filter.comps & mask; + if (comps != mask) /* non-uniform filter */ + av_bprintf(bp, " on %s", ff_sws_comp_mask_str(comps)); break; } case SWS_OP_LUT_3D: diff --git a/libswscale/ops.h b/libswscale/ops.h index f95714c211c..0387f784f9b 100644 --- a/libswscale/ops.h +++ b/libswscale/ops.h @@ -199,6 +199,7 @@ uint32_t ff_sws_linear_mask(const SwsLinearOp *c); typedef struct SwsFilterOp { SwsFilterWeights *kernel; /* filter kernel (refstruct) */ SwsPixelType type; /* pixel type to store result as */ + SwsCompMask comps; /* affected components */ } SwsFilterOp; typedef struct SwsLut3dOp { -- 2.54.0 >From 13a97b59971eb07351ae39d5d7129a8de4bede18 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 8 Oct 2026 20:14:03 +0200 Subject: [PATCH 05/20] swscale/ops: add ff_sws_op_intersects() helper Needed to know where to properly split op lists involving subsampling; to maximize the chance of fusing redundant work into the (ideally planar) write. Sponsored-by: Soveriegn Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops.c | 13 +++++++++++++ libswscale/ops.h | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/libswscale/ops.c b/libswscale/ops.c index 03d887ef72c..3b0f578dfe6 100644 --- a/libswscale/ops.c +++ b/libswscale/ops.c @@ -201,6 +201,19 @@ SwsPixelType ff_sws_op_input_type(const SwsOp *op) return ff_sws_op_output_type(ff_sws_op_prev(op)); } +bool ff_sws_op_intersects(const SwsOp *op, SwsCompMask comps) +{ + for (int i = 0; i < 4; i++) { + if (!SWS_OP_NEEDED(op, i)) + continue; + const SwsCompMask deps = op->outputs[i].dep_in; + if ((deps & comps) && (deps & ~comps)) + return true; + } + + return false; +} + /* biased towards `a` */ static AVRational64 av_min_q64(AVRational64 a, AVRational64 b) { diff --git a/libswscale/ops.h b/libswscale/ops.h index 0387f784f9b..160ba1b5eeb 100644 --- a/libswscale/ops.h +++ b/libswscale/ops.h @@ -317,6 +317,13 @@ void ff_sws_apply_op_q(const SwsOp *op, AVRational64 x[4]); const SwsOp *ff_sws_op_prev(const SwsOp *op); const SwsOp *ff_sws_op_next(const SwsOp *op); +/** + * Return whether or not any of op's outputs involve both input components + * in `comps` and input components not in `comps`, i.e. whether or not `op` + * may introduce cross-plane interactions between included/excluded comps. + **/ +bool ff_sws_op_intersects(const SwsOp *op, SwsCompMask comps); + /** * Helper struct for representing a list of operations. */ -- 2.54.0 >From 56e6b1c6e964496fbf339152df1d573f182b82ea Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 8 Oct 2026 20:14:35 +0200 Subject: [PATCH 06/20] swscale/ops_optimizer: enforce filter uniformity when commuting It would be nice if we could also fix this to allow promotion when the linear op doesn't involve cross-plane dependencies, but I'm not sure if that would ever come up in a real world op list. This includes the case of multiple read ops, which might become a thing in the near future. Sponsored-by: Sovereign Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops_optimizer.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c index 87567d2214a..4a372d45bd0 100644 --- a/libswscale/ops_optimizer.c +++ b/libswscale/ops_optimizer.c @@ -170,6 +170,12 @@ static bool op_commute_swizzle(SwsOp *op, SwsOp *next) return false; } +static bool filter_is_uniform(const SwsOp *op) +{ + const SwsCompMask needed = ff_sws_comp_mask_needed(op); + return (op->filter.comps & needed) == needed; +} + /** * Try to commute a filter op with the previous operation. Makes any * adjustments to the operations as needed, but does not perform the actual @@ -182,14 +188,17 @@ static bool op_commute_filter(SwsOp *op, SwsOp *prev) av_assert0(!ff_sws_pixel_type_is_int(op->filter.type)); switch (prev->op) { + case SWS_OP_LINEAR: + if (!filter_is_uniform(op)) + return false; /* might break cross-component operations */ + op->filter.comps = SWS_COMP_ALL; /* act on all linear inputs */ + for (int i = 0; i < 4; i++) + prev->lin.type[i] = op->filter.type; + return true; case SWS_OP_SWIZZLE: case SWS_OP_SCALE: case SWS_OP_DITHER: return true; - case SWS_OP_LINEAR: - for (int i = 0; i < 4; i++) - prev->lin.type[i] = op->filter.type; - return true; case SWS_OP_CONVERT: case SWS_OP_INVALID: case SWS_OP_READ: @@ -365,7 +374,7 @@ retry: } /* Merge filter with prior conversion */ - if (prev->op == SWS_OP_CONVERT) { + if (prev->op == SWS_OP_CONVERT && filter_is_uniform(op)) { const SwsPixelType input_type = ff_sws_op_input_type(prev); int size_from = ff_sws_pixel_type_size(input_type); int size_to = ff_sws_pixel_type_size(prev->convert.to); @@ -744,7 +753,9 @@ retry: case SWS_OP_FILTER_V: /* Merge with prior simple planar read */ if (prev->op == SWS_OP_READ && !prev->rw.filter.op && - prev->rw.mode == SWS_RW_PLANAR && !prev->rw.frac) { + prev->rw.mode == SWS_RW_PLANAR && !prev->rw.frac && + (op->filter.comps & needed) == SWS_COMP_ELEMS(prev->rw.elems)) + { prev->rw.filter.op = op->op; prev->rw.filter.kernel = av_refstruct_ref(op->filter.kernel); prev->rw.filter.type = op->filter.type; -- 2.54.0 >From 25636eb6bff01c557e44b5fbd6be6280a359e420 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Wed, 17 Jun 2026 20:12:09 +0200 Subject: [PATCH 07/20] swscale/ops_optimizer: update filter comps when commuting past swizzle Sponsored-by: Sovereign Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops_optimizer.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c index 4a372d45bd0..5ff28414a4a 100644 --- a/libswscale/ops_optimizer.c +++ b/libswscale/ops_optimizer.c @@ -110,8 +110,10 @@ static bool op_commute_swizzle(SwsOp *op, SwsOp *next) case SWS_OP_LSHIFT: case SWS_OP_RSHIFT: case SWS_OP_SCALE: + return true; case SWS_OP_FILTER_H: case SWS_OP_FILTER_V: + ff_sws_comp_mask_swizzle_inv(&next->filter.comps, &op->swizzle); return true; /** @@ -196,6 +198,8 @@ static bool op_commute_filter(SwsOp *op, SwsOp *prev) prev->lin.type[i] = op->filter.type; return true; case SWS_OP_SWIZZLE: + ff_sws_comp_mask_swizzle_inv(&op->filter.comps, &prev->swizzle); + return true; case SWS_OP_SCALE: case SWS_OP_DITHER: return true; -- 2.54.0 >From a7e7befda7c293b21060ab97ed24c52b9af7675d Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Tue, 1 Sep 2026 13:42:14 +0200 Subject: [PATCH 08/20] swscale/ops_optimizer: allow re-ordering filter ops Ensure all horizontal filters come before equivalent vertical filters. Sponsored-by: Sovereign Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops_optimizer.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c index 5ff28414a4a..1720aca5644 100644 --- a/libswscale/ops_optimizer.c +++ b/libswscale/ops_optimizer.c @@ -215,10 +215,14 @@ static bool op_commute_filter(SwsOp *op, SwsOp *prev) case SWS_OP_CLEAR: case SWS_OP_MIN: case SWS_OP_MAX: - case SWS_OP_FILTER_H: - case SWS_OP_FILTER_V: case SWS_OP_LUT_3D: return false; + case SWS_OP_FILTER_H: + case SWS_OP_FILTER_V: + /* Allow commuting horizontal filters before vertical filters; this + * promotes fusion between adjacent similar filter types */ + return op->op == SWS_OP_FILTER_H && prev->op == SWS_OP_FILTER_V && + op->filter.type == prev->filter.type; case SWS_OP_TYPE_NB: break; } -- 2.54.0 >From 4170a67e6f8f8f58aea583c6d29dc5434f768d3f Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Fri, 19 Jun 2026 16:15:49 +0200 Subject: [PATCH 09/20] swscale/ops_optimizer: eliminate no-op filters May be left-over after plane splitting. Sponsored-by: Sovereign Tech Fund Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops_optimizer.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c index 1720aca5644..ec871b7ed95 100644 --- a/libswscale/ops_optimizer.c +++ b/libswscale/ops_optimizer.c @@ -759,6 +759,12 @@ retry: case SWS_OP_FILTER_H: case SWS_OP_FILTER_V: + /* Eliminate redundant no-op filter */ + if (!(op->filter.comps & needed)) { + ff_sws_op_list_remove_at(ops, n, 1); + goto retry; + } + /* Merge with prior simple planar read */ if (prev->op == SWS_OP_READ && !prev->rw.filter.op && prev->rw.mode == SWS_RW_PLANAR && !prev->rw.frac && -- 2.54.0 >From 351dfc21d143b2e7bfcc088d9eed484e59f15758 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Tue, 1 Sep 2026 18:42:09 +0200 Subject: [PATCH 10/20] swscale/filters: constrain SWS_FILTER_BICUBIC parameter range The filter is not really useful outside of these parameters, and clamping the domain lets us reason about the expected overshoot. In particular, B < 0 is completely broken, as is B >= 3 (already identified in the code). C is more relaxed, but outside [-1.5, 2.0] it would also overshoot by too much (relative to the upcoming change). There's a final constraint of 2B + C <= 3. Limiting both parameters to [0, 1] solves all problems at once and is the standard practice. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/filters.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libswscale/filters.c b/libswscale/filters.c index fb54d2bbd8b..3073ed8cdc5 100644 --- a/libswscale/filters.c +++ b/libswscale/filters.c @@ -148,7 +148,8 @@ static bool validate_params(const SwsFilterFunction *fun, SwsScaler scaler) case SWS_SCALE_LANCZOS: return fun->params[0] >= 1.0 && fun->params[0] <= RADIUS_MAX; /* radius */ case SWS_SCALE_BICUBIC: - return fun->params[0] < 3.0; /* B param (division by zero) */ + return fun->params[0] >= 0.0 && fun->params[0] <= 1.0 /* B */ && + fun->params[1] >= 0.0 && fun->params[1] <= 1.0 /* C */; default: return true; } -- 2.54.0 >From d08418d18f5208a2e63938a4882ed0ede33b1217 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Tue, 1 Sep 2026 21:29:41 +0200 Subject: [PATCH 11/20] swscale/filters: properly handle filter tails When the filter overlaps an image boundary, we need to add all the weights past the edge to the appropriate edge weight. Apart from generating correct filtering behavior near boundaries, this also fixes an edge case where the `wsum < 0` guard could fire, as well as some examples of catastrophic filter overshoot. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/filters.c | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/libswscale/filters.c b/libswscale/filters.c index 3073ed8cdc5..811c5729ecd 100644 --- a/libswscale/filters.c +++ b/libswscale/filters.c @@ -82,13 +82,11 @@ static void compute_row(SwsFilterWeights *f, const SwsFilterFunction *fun, return; } - /* First pixel that is actually within the filter envelope */ - const double start_pos = src_pos - radius; - int64_t start_idx = ceil(start_pos); - start_idx = FFMAX(start_idx, 0); /* edge clamping */ - start_idx = FFMIN(start_idx, f->src_size - f->filter_size); - const double offset = start_idx - src_pos; - *pos = start_idx; + /* First and last pixels that are actually within the filter envelope */ + const int start = ceil (src_pos - radius); + const int stop = floor(src_pos + radius); + const int clamped = av_clip(start, 0, f->src_size - f->filter_size); + *pos = clamped; /** * Generate raw filter weights with maximum precision. Sum the positive @@ -96,14 +94,26 @@ static void compute_row(SwsFilterWeights *f, const SwsFilterFunction *fun, * summation order should already give the best precision because abs(w) * is monotonically decreasing */ - const double base = stretch_inv * offset; + const double base = stretch_inv * (clamped - src_pos); double wsum_pos = 0.0, wsum_neg = 0.0; for (int i = 0; i < f->filter_size; i++) { - tmp[i] = scaler_sample(fun, base + stretch_inv * i); - if (tmp[i] >= 0) - wsum_pos += tmp[i]; + /* Fold extra edge weights into the first/last weight in case the + * filter got truncated; loop over all needed samples */ + int i0 = i, i1 = i; + if (i == 0) + i0 = start - clamped; + if (i == f->filter_size - 1) + i1 = stop - clamped; + + double w = 0.0; + for (int k = i0; k <= i1; k++) + w += scaler_sample(fun, base + stretch_inv * k); + + if (w >= 0) + wsum_pos += w; else - wsum_neg += tmp[i]; + wsum_neg += w; + tmp[i] = w; } const double wsum = wsum_pos + wsum_neg; -- 2.54.0 >From 995fb4de056179653e9b6bb30813acd869bf400e Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Tue, 1 Sep 2026 18:24:01 +0200 Subject: [PATCH 12/20] swscale/filters: add ff_sws_filter_overshoot Computes a static bound on the filter overshoot, rather than measuring this at filter generation time. This will allow us to avoid having to call the expensive ff_sws_filter_generate() during op list generation, until we actually have to emit the settled filter. These are slight overestimates, all of the filter overshoots are somewhere between 0 and 1, but the extra range here doesn't actually affect anything in the optimizer (as the following commits prove). Signed-off-by: Niklas Haas <[email protected]> --- libswscale/filters.c | 24 ++++++++++++++++++++++++ libswscale/filters.h | 7 +++++++ 2 files changed, 31 insertions(+) diff --git a/libswscale/filters.c b/libswscale/filters.c index 811c5729ecd..1f7d10bf5c1 100644 --- a/libswscale/filters.c +++ b/libswscale/filters.c @@ -195,6 +195,30 @@ static double est_filter_radius(const SwsFilterFunction *fun) return radius; } +int ff_sws_filter_overshoot(const SwsFilterParams *params) +{ + switch (params->scaler) { + case SWS_SCALE_BILINEAR: + case SWS_SCALE_POINT: + case SWS_SCALE_AREA: + case SWS_SCALE_GAUSSIAN: + return 0; /* non-negative kernels */ + case SWS_SCALE_AUTO: /* bicubic */ + case SWS_SCALE_BICUBIC: { + double c = params->scaler_params[1]; + if (c == SWS_PARAM_DEFAULT) + c = filter_functions[SWS_SCALE_BICUBIC].params[1]; + return c ? SWS_FILTER_SCALE : 0; /* no negative lobes at C=0 */ + } + case SWS_SCALE_LANCZOS: + case SWS_SCALE_SPLINE: + case SWS_SCALE_SINC: + return SWS_FILTER_SCALE; /* ca. 0.903 at RADIUS_MAX (sinc) */ + } + + return AVERROR(EINVAL); +} + int ff_sws_filter_generate(void *log, const SwsFilterParams *params, SwsFilterWeights **out) { diff --git a/libswscale/filters.h b/libswscale/filters.h index a10e6f39640..07c7bfc477c 100644 --- a/libswscale/filters.h +++ b/libswscale/filters.h @@ -79,6 +79,13 @@ typedef struct SwsFilterParams { double offset; } SwsFilterParams; +/** + * Returns the maximum amount (scaled relative to SWS_FILTER_SCALE) by which + * a filter can overshoot (or undershoot) the input range, or a negative + * error code for invalid parameters. + */ +int ff_sws_filter_overshoot(const SwsFilterParams *params); + /** * Represents a computed filter kernel. */ -- 2.54.0 >From 0472028371a24839feaa92799f6900b8a062627e Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Tue, 1 Sep 2026 18:36:34 +0200 Subject: [PATCH 13/20] swscale/filters: report overshoot instead of sums These are strictly redundant, because the row sum is already enforced as SWS_FILTER_SCALE = sum_positive + sum_negative. So the row with the highest overshoot in one direction has the highest overshoot in the other direction as well. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/filters.c | 9 +++------ libswscale/filters.h | 3 +-- libswscale/ops.c | 6 +++--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/libswscale/filters.c b/libswscale/filters.c index 1f7d10bf5c1..a3896fa63dc 100644 --- a/libswscale/filters.c +++ b/libswscale/filters.c @@ -137,10 +137,9 @@ static void compute_row(SwsFilterWeights *f, const SwsFilterFunction *fun, sum_neg += out[i]; } - if (sum_pos > f->sum_positive) - f->sum_positive = sum_pos; - if (sum_neg < f->sum_negative) - f->sum_negative = sum_neg; + const int overshoot = sum_pos - SWS_FILTER_SCALE; + if (overshoot > f->overshoot) + f->overshoot = overshoot; } static void sws_filter_free(AVRefStructOpaque opaque, void *obj) @@ -299,8 +298,6 @@ int ff_sws_filter_generate(void *log, const SwsFilterParams *params, filter->virtual_size = virtual_size; filter->offset = params->offset; filter->filter_size = filter_size; - if (filter->filter_size == 1) - filter->sum_positive = SWS_FILTER_SCALE; av_log(log, AV_LOG_DEBUG, "Generating %s filter with %d taps (radius = %f)\n", filter->name, filter->filter_size, radius); diff --git a/libswscale/filters.h b/libswscale/filters.h index 07c7bfc477c..f6f1e745570 100644 --- a/libswscale/filters.h +++ b/libswscale/filters.h @@ -124,8 +124,7 @@ typedef struct SwsFilterWeights { * tracker about the filter's behavior. */ char name[16]; /* name of the configured filter kernel */ - int sum_positive; /* (maximum) sum of all positive weights */ - int sum_negative; /* (minimum) sum of all negative weights */ + int overshoot; /* maximum filter overshoot (see ff_sws_filter_overshoot()) */ } SwsFilterWeights; /** diff --git a/libswscale/ops.c b/libswscale/ops.c index 3b0f578dfe6..59064a5c042 100644 --- a/libswscale/ops.c +++ b/libswscale/ops.c @@ -359,14 +359,14 @@ static void apply_filter_weights(SwsOpPad *pad, SwsOpType filter, if (!filter) return; - const AVRational64 posw = { weights->sum_positive, SWS_FILTER_SCALE }; - const AVRational64 negw = { weights->sum_negative, SWS_FILTER_SCALE }; - /* Only point sampling preserves exactness */ if (weights->filter_size != 1) pad->flags &= ~SWS_COMP_EXACT; /* Update min/max assuming extremes */ + const int one = SWS_FILTER_SCALE, overshoot = weights->overshoot; + const AVRational64 posw = { one + overshoot, one }; + const AVRational64 negw = { -overshoot, one }; const AVRational64 min = pad->min, max = pad->max; pad->min = ff_add_q64(ff_mul_q64(min, posw), ff_mul_q64(max, negw)); pad->max = ff_add_q64(ff_mul_q64(min, negw), ff_mul_q64(max, posw)); -- 2.54.0 >From d41c80701d9e1ddc223ba032c725c7199214efb2 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Tue, 1 Sep 2026 15:09:16 +0200 Subject: [PATCH 14/20] swscale/filters: allow ff_sws_filter_generate() to be called on NULL Needed to test if filters would exceed the design limit without actually requiring us to compute the filter body. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/filters.c | 3 +++ libswscale/filters.h | 3 +++ 2 files changed, 6 insertions(+) diff --git a/libswscale/filters.c b/libswscale/filters.c index a3896fa63dc..b4403431189 100644 --- a/libswscale/filters.c +++ b/libswscale/filters.c @@ -288,6 +288,9 @@ int ff_sws_filter_generate(void *log, const SwsFilterParams *params, if (filter_size > SWS_FILTER_SIZE_MAX) return AVERROR(ENOTSUP); + if (!out) + return 0; /* generation would success */ + SwsFilterWeights *filter; filter = av_refstruct_alloc_ext(sizeof(*filter), 0, NULL, sws_filter_free); if (!filter) diff --git a/libswscale/filters.h b/libswscale/filters.h index f6f1e745570..68398a9ca3b 100644 --- a/libswscale/filters.h +++ b/libswscale/filters.h @@ -131,6 +131,9 @@ typedef struct SwsFilterWeights { * Generate a filter kernel for the given parameters. The generated filter is * allocated as a refstruct and must be unref'd by the caller. * + * If `out` is NULL, this function will only validate the parameters and + * return success/failure. + * * Returns 0 or a negative error code. In particular, this may return: * - AVERROR(ENOMEM) if memory allocation fails. * - AVERROR(EINVAL) if the provided parameters are invalid (e.g. out of range). -- 2.54.0 >From 6247128ec948f838c1df0e4236a479dfa8e32d52 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Wed, 2 Sep 2026 14:59:49 +0200 Subject: [PATCH 15/20] swscale/ops_chain: add ff_sws_op_chain_attach() to eliminate duplicate code Signed-off-by: Niklas Haas <[email protected]> --- libswscale/aarch64/ops.c | 8 +++----- libswscale/ops_chain.c | 10 ++++++++++ libswscale/ops_chain.h | 8 ++++++++ libswscale/uops_backend.c | 9 ++------- libswscale/x86/ops.c | 12 +----------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/libswscale/aarch64/ops.c b/libswscale/aarch64/ops.c index 963bfec7308..53416049848 100644 --- a/libswscale/aarch64/ops.c +++ b/libswscale/aarch64/ops.c @@ -169,10 +169,8 @@ static int aarch64_compile_uops(SwsContext *ctx, const SwsUOpList *uops, chain->cpu_flags = AV_CPU_FLAG_NEON; *out = (SwsCompiledOp) { - .priv = chain, - .slice_align = 1, - .free = ff_sws_op_chain_free_cb, .block_size = block_size, + .slice_align = 1, }; /* Look up kernel functions. */ @@ -207,8 +205,8 @@ static int aarch64_compile_uops(SwsContext *ctx, const SwsUOpList *uops, case 4: process_func = (SwsOpFunc) ff_sws_process_1111_neon; break; } - out->func = process_func; - out->cpu_flags = chain->cpu_flags; + out->func = process_func; + ff_sws_op_chain_attach(chain, out); return 0; diff --git a/libswscale/ops_chain.c b/libswscale/ops_chain.c index 42f26035a7b..ff48ede4a24 100644 --- a/libswscale/ops_chain.c +++ b/libswscale/ops_chain.c @@ -57,6 +57,16 @@ int ff_sws_op_chain_append(SwsOpChain *chain, SwsFuncPtr func, return 0; } +void ff_sws_op_chain_attach(const SwsOpChain *chain, SwsCompiledOp *out) +{ + av_assert0(!out->priv); + out->priv = (void *) chain; + out->free = ff_sws_op_chain_free_cb; + out->cpu_flags = chain->cpu_flags; + memcpy(out->over_read, chain->over_read, sizeof(out->over_read)); + memcpy(out->over_write, chain->over_write, sizeof(out->over_write)); +} + int ff_sws_uop_lookup(SwsContext *ctx, const SwsUOpTable *const tables[], int num_tables, const SwsUOp *uop, const int block_size, SwsOpChain *chain) diff --git a/libswscale/ops_chain.h b/libswscale/ops_chain.h index e3a25e3af54..f68b3f72ad4 100644 --- a/libswscale/ops_chain.h +++ b/libswscale/ops_chain.h @@ -102,6 +102,14 @@ static inline void ff_sws_op_chain_free(SwsOpChain *chain) int ff_sws_op_chain_append(SwsOpChain *chain, SwsFuncPtr func, void (*free)(SwsOpPriv *), const SwsOpPriv *priv); +/** + * Attach a SwsOpChain and all metadata to a SwsCompiledOp (as out->priv). + * + * Note: This does not initialize *out. The caller is responsible for + * assigning out->func. + */ +void ff_sws_op_chain_attach(const SwsOpChain *chain, SwsCompiledOp *out); + typedef struct SwsImplParams { const SwsUOpTable *table; const SwsUOp *uop; diff --git a/libswscale/uops_backend.c b/libswscale/uops_backend.c index 5dfb829b4e4..851662860ae 100644 --- a/libswscale/uops_backend.c +++ b/libswscale/uops_backend.c @@ -154,17 +154,11 @@ static int compile_uops_c(SwsContext *ctx, const SwsUOpList *uops, SwsCompiledOp } *out = (SwsCompiledOp) { - .slice_align = 1, .block_size = SWS_BLOCK_SIZE, - .cpu_flags = chain->cpu_flags, - .priv = chain, - .free = ff_sws_op_chain_free_cb, + .slice_align = 1, .func = process, }; - memcpy(out->over_read, chain->over_read, sizeof(out->over_read)); - memcpy(out->over_write, chain->over_write, sizeof(out->over_write)); - av_log(ctx, AV_LOG_DEBUG, "Compiled micro-ops:\n"); for (int i = 0; i < uops->num_ops; i++) { char name[SWS_UOP_NAME_MAX]; @@ -172,6 +166,7 @@ static int compile_uops_c(SwsContext *ctx, const SwsUOpList *uops, SwsCompiledOp av_log(ctx, AV_LOG_DEBUG, " %s\n", name); } + ff_sws_op_chain_attach(chain, out); return 0; fail: diff --git a/libswscale/x86/ops.c b/libswscale/x86/ops.c index 23785c58ca8..e166214b2ba 100644 --- a/libswscale/x86/ops.c +++ b/libswscale/x86/ops.c @@ -665,8 +665,6 @@ static int compile_uops_x86(SwsContext *ctx, const SwsUOpList *uops, SwsCompiled /* Use at most two full YMM regs during the widest precision section */ .block_size = 2 * FFMIN(mmsize, 32) / uops->pixel_size_max, .slice_align = 1, - .free = ff_sws_op_chain_free_cb, - .priv = chain, }; for (int i = 0; i < uops->num_ops; i++) { @@ -693,15 +691,6 @@ static int compile_uops_x86(SwsContext *ctx, const SwsUOpList *uops, SwsCompiled case 4: out->func = ff_sws_process4_x86; break; } - if (ret < 0) { - ff_sws_op_chain_free(chain); - return ret; - } - - out->cpu_flags = chain->cpu_flags; - memcpy(out->over_read, chain->over_read, sizeof(out->over_read)); - memcpy(out->over_write, chain->over_write, sizeof(out->over_write)); - av_log(ctx, AV_LOG_DEBUG, "Compiled micro-ops:\n"); for (int i = 0; i < uops->num_ops; i++) { char name[SWS_UOP_NAME_MAX]; @@ -709,6 +698,7 @@ static int compile_uops_x86(SwsContext *ctx, const SwsUOpList *uops, SwsCompiled av_log(ctx, AV_LOG_DEBUG, " %s\n", name); } + ff_sws_op_chain_attach(chain, out); return 0; fail: -- 2.54.0 >From d37cbe4b65b84fd6733300982ec1d52d5cdc6c32 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Sat, 24 Oct 2026 18:09:19 +0200 Subject: [PATCH 16/20] swscale/ops_dispatch: source SwsFilterWeights from SwsCompiledOp Instead of assuming these come from the read op directly. Pre-requisite for the upcoming change. This does require a bit of annoying plumbing to get through the SwsUOpList -> SwsOpChain -> SwsCompiledOp pipeline, but doing it this way every SwsOpChain consumer gets it for free. I've pre-emptively generalized this to an array of filters, in anticipation of allowing multiple diverging read ops in a single pass. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops_chain.c | 17 +++++++++++++++++ libswscale/ops_chain.h | 3 +++ libswscale/ops_dispatch.c | 6 +++++- libswscale/ops_dispatch.h | 4 ++++ libswscale/vulkan/ops.c | 8 ++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/libswscale/ops_chain.c b/libswscale/ops_chain.c index ff48ede4a24..0b315976c60 100644 --- a/libswscale/ops_chain.c +++ b/libswscale/ops_chain.c @@ -39,6 +39,8 @@ void ff_sws_op_chain_free_cb(void *ptr) chain->free[i](&chain->impl[i].priv); } + for (int i = 0; i < 4; i++) + av_refstruct_unref(&chain->filter[i]); av_free(chain); } @@ -65,6 +67,7 @@ void ff_sws_op_chain_attach(const SwsOpChain *chain, SwsCompiledOp *out) out->cpu_flags = chain->cpu_flags; memcpy(out->over_read, chain->over_read, sizeof(out->over_read)); memcpy(out->over_write, chain->over_write, sizeof(out->over_write)); + memcpy(out->filter, chain->filter, sizeof(out->filter)); } int ff_sws_uop_lookup(SwsContext *ctx, const SwsUOpTable *const tables[], @@ -127,6 +130,20 @@ int ff_sws_uop_lookup(SwsContext *ctx, const SwsUOpTable *const tables[], return ret; } + switch (uop->uop) { + case SWS_UOP_READ_PLANAR_FH: + case SWS_UOP_READ_PLANAR_FV: + case SWS_UOP_READ_PLANAR_FV_FMA: + av_assert0(uop->data.kernel); + for (int i = 0; i < 4; i++) { + if (!SWS_COMP_TEST(uop->mask, i)) + continue; + av_assert0(!chain->filter[i]); + chain->filter[i] = av_refstruct_ref(uop->data.kernel); + } + break; + } + for (int i = 0; i < 4; i++) { chain->over_read[i] = FFMAX(chain->over_read[i], res.over_read[i]); chain->over_write[i] = FFMAX(chain->over_write[i], res.over_write[i]); diff --git a/libswscale/ops_chain.h b/libswscale/ops_chain.h index f68b3f72ad4..31e0584652d 100644 --- a/libswscale/ops_chain.h +++ b/libswscale/ops_chain.h @@ -89,6 +89,9 @@ typedef struct SwsOpChain { int cpu_flags; /* set of all used CPU flags */ int over_read[4]; /* chain over-reads input by this many bytes */ int over_write[4]; /* chain over-writes output by this many bytes */ + + /* Input filters for this chain, or NULL */ + SwsFilterWeights *filter[4]; } SwsOpChain; SwsOpChain *ff_sws_op_chain_alloc(void); diff --git a/libswscale/ops_dispatch.c b/libswscale/ops_dispatch.c index 21e5535f14b..81a43ecd7fb 100644 --- a/libswscale/ops_dispatch.c +++ b/libswscale/ops_dispatch.c @@ -635,7 +635,11 @@ static int compile_single(const CompileArgs *args, const SwsOpList *ops, p->idx_out[i] = idx; } - const SwsFilterWeights *filter = read ? read->rw.filter.kernel : NULL; + /* TODO: ops_dispatch currently does not have a way for the op list to + * have multiple read ops; nor a way for read ops to specify non-zero + * starting components; but once these two facts are changed, this hard + * coded [0] index needs to be adjusted */ + const SwsFilterWeights *filter = comp->filter[0]; if (read && read->rw.filter.op == SWS_OP_FILTER_V) { p->offsets_y = av_refstruct_ref(filter->offsets); p->filter_size_v = filter->filter_size; diff --git a/libswscale/ops_dispatch.h b/libswscale/ops_dispatch.h index f88ba638256..c41ebde6654 100644 --- a/libswscale/ops_dispatch.h +++ b/libswscale/ops_dispatch.h @@ -115,6 +115,10 @@ typedef struct SwsCompiledOp { /* Set by ff_sws_ops_compile(), informative */ const struct SwsOpBackend *backend; + /* Resolved read filters, per component. This struct does not hold a + * direct ref, the owner of `priv` is responsible for freeing it */ + const SwsFilterWeights *filter[4]; + /* Execution parameters for all functions */ int slice_align; /* slice height alignment */ int cpu_flags; /* active set of CPU flags (informative) */ diff --git a/libswscale/vulkan/ops.c b/libswscale/vulkan/ops.c index 5492a9390e5..0da8672d6e1 100644 --- a/libswscale/vulkan/ops.c +++ b/libswscale/vulkan/ops.c @@ -89,6 +89,8 @@ typedef struct VulkanPriv { VkFormat src_view_fmt[AV_NUM_DATA_POINTERS]; VkFormat dst_view_fmt[AV_NUM_DATA_POINTERS]; int interlaced; + + SwsFilterWeights *read_filters[4]; } VulkanPriv; static void spirv_process(const SwsFrame *dst, const SwsFrame *src, @@ -185,6 +187,8 @@ static void free_fn(void *priv) ff_vk_shader_free(&p->s->vkctx, &p->shd); for (int i = 0; i < p->nb_data_bufs; i++) ff_vk_free_buf(&p->s->vkctx, &p->data_bufs[i]); + for (int i = 0; i < 4; i++) + av_refstruct_unref(&p->read_filters[i]); av_refstruct_unref(&p->s); av_free(priv); } @@ -650,6 +654,9 @@ static int emit_read_filtered(SPICtx *spi, SPIRVIDs *id, const SwsOp *op, if (p->nb_data_bufs >= MAX_DATA_BUFS) return AVERROR(ENOTSUP); + for (int i = 0; i < op->rw.elems; i++) + p->read_filters[i] = av_refstruct_ref(wd); + int err = create_filter_buf(p->s, wd, &p->data_bufs[p->nb_data_bufs]); if (err < 0) return err; @@ -1248,6 +1255,7 @@ static int compile_spirv(SwsContext *sws, const SwsOpList *ops, .free = free_fn, }; + memcpy(out->filter, p->read_filters, sizeof(out->filter)); return 0; fail: -- 2.54.0 >From 84de356fc89116264fbc6732c91362adf9006123 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Sat, 24 Oct 2026 17:32:20 +0200 Subject: [PATCH 17/20] swscale/ops: drop tap count from filter op description This is not known in advance in the following commit. Split off to prove the next commit's claim of not affecting the ops list. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/ops.c | 9 ++++----- tests/ref/fate/sws-ops-list | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libswscale/ops.c b/libswscale/ops.c index 59064a5c042..0c097f4c995 100644 --- a/libswscale/ops.c +++ b/libswscale/ops.c @@ -996,9 +996,8 @@ void ff_sws_op_desc(AVBPrint *bp, const SwsOp *op) if (!op->rw.filter.op) break; const SwsFilterWeights *kernel = op->rw.filter.kernel; - av_bprintf(bp, " + %d tap %s filter (%c)", - kernel->filter_size, kernel->name, - op->rw.filter.op == SWS_OP_FILTER_H ? 'H' : 'V'); + av_bprintf(bp, " + %s filter (%c)", + kernel->name, op->rw.filter.op == SWS_OP_FILTER_H ? 'H' : 'V'); break; case SWS_OP_LSHIFT: av_bprintf(bp, "%-20s: << %u", name, op->shift.amount); @@ -1060,9 +1059,9 @@ void ff_sws_op_desc(AVBPrint *bp, const SwsOp *op) case SWS_OP_FILTER_H: case SWS_OP_FILTER_V: { const SwsFilterWeights *kernel = op->filter.kernel; - av_bprintf(bp, "%-20s: %d -> %d %s (%d taps)", name, + av_bprintf(bp, "%-20s: %d -> %d %s", name, kernel->src_size, kernel->dst_size, - kernel->name, kernel->filter_size); + kernel->name); const SwsCompMask comps = op->filter.comps & mask; if (comps != mask) /* non-uniform filter */ av_bprintf(bp, " on %s", ff_sws_comp_mask_str(comps)); diff --git a/tests/ref/fate/sws-ops-list b/tests/ref/fate/sws-ops-list index d66a85bdf1c..b30f896b297 100644 --- a/tests/ref/fate/sws-ops-list +++ b/tests/ref/fate/sws-ops-list @@ -1 +1 @@ -33a22c71b709405a5c3ccf687bc128f9 +c1be5887af323c70069dd01ce6ee6f12 -- 2.54.0 >From 1bae929c87b15d367c932dc197751e7e8942a913 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Sat, 24 Oct 2026 18:14:00 +0200 Subject: [PATCH 18/20] swscale/ops: keep filters as parameter-only until translation This is a prerequisite for manipulating filters on the symbolic level inside the optimizer (e.g. for filter fusion). Does not (yet) affect the actual runtime of fate-sws-uops-macros etc, because those tests still perform the UOP translation, but the next patch will address that. Requires a bit of fiddling since it's now the backend's job to actually generate the filter kernel, which represents a bit of a headache since the Vulkan backend was never updated to use the SwsUOps infrastructure. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/format.c | 5 ++-- libswscale/ops.c | 59 +++++++++++++++++++------------------- libswscale/ops.h | 4 +-- libswscale/ops_optimizer.c | 2 +- libswscale/uops.c | 5 +++- libswscale/vulkan/ops.c | 19 ++++++++---- 6 files changed, 51 insertions(+), 43 deletions(-) diff --git a/libswscale/format.c b/libswscale/format.c index 8757c0282c4..7f18fd03c4b 100644 --- a/libswscale/format.c +++ b/libswscale/format.c @@ -1733,8 +1733,7 @@ static int add_filter(SwsContext *ctx, SwsPixelType type, SwsOpList *ops, for (int i = 0; i < SWS_NUM_SCALER_PARAMS; i++) params.scaler_params[i] = ctx->scaler_params[i]; - SwsFilterWeights *kernel; - int ret = ff_sws_filter_generate(ctx, ¶ms, &kernel); + int ret = ff_sws_filter_generate(ctx, ¶ms, NULL); if (ret == AVERROR(ENOTSUP)) { /* Filter size exceeds limit; cascade with geometric mean size */ int mean = sqrt((int64_t) src_size * dst_size); @@ -1750,7 +1749,7 @@ static int add_filter(SwsContext *ctx, SwsPixelType type, SwsOpList *ops, return ff_sws_op_list_append(ops, &(SwsOp) { .op = filter, - .filter.kernel = kernel, + .filter.params = params, .filter.type = type, .filter.comps = comps, }); diff --git a/libswscale/ops.c b/libswscale/ops.c index 0c097f4c995..f2ae05b5fbc 100644 --- a/libswscale/ops.c +++ b/libswscale/ops.c @@ -354,17 +354,17 @@ static SwsCompFlags merge_comp_flags(SwsCompFlags a, SwsCompFlags b) } static void apply_filter_weights(SwsOpPad *pad, SwsOpType filter, - const SwsFilterWeights *weights) + const SwsFilterParams *params) { if (!filter) return; /* Only point sampling preserves exactness */ - if (weights->filter_size != 1) + if (params->src_size > 1 && params->scaler != SWS_SCALE_POINT) pad->flags &= ~SWS_COMP_EXACT; /* Update min/max assuming extremes */ - const int one = SWS_FILTER_SCALE, overshoot = weights->overshoot; + const int one = SWS_FILTER_SCALE, overshoot = ff_sws_filter_overshoot(params); const AVRational64 posw = { one + overshoot, one }; const AVRational64 negw = { -overshoot, one }; const AVRational64 min = pad->min, max = pad->max; @@ -373,11 +373,11 @@ static void apply_filter_weights(SwsOpPad *pad, SwsOpType filter, /* Update plane dimensions */ if (filter == SWS_OP_FILTER_H && pad->width) { - av_assert1(pad->width == weights->src_size); - pad->width = weights->dst_size; + av_assert1(pad->width == params->src_size); + pad->width = params->dst_size; } else if (filter == SWS_OP_FILTER_V && pad->height) { - av_assert1(pad->height == weights->src_size); - pad->height = weights->dst_size; + av_assert1(pad->height == params->src_size); + pad->height = params->dst_size; } } @@ -469,7 +469,7 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op) dst->width = src->width; dst->height = src->height; dst->dep_in = SWS_COMP(i); - apply_filter_weights(dst, op->rw.filter.op, op->rw.filter.kernel); + apply_filter_weights(dst, op->rw.filter.op, &op->rw.filter.params); /* Don't mark packed or fractional reads as a copy, because the read * operation implicitly unpacks the data into separate components. @@ -625,7 +625,9 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op) case SWS_OP_FILTER_V: { for (int i = 0; i < 4; i++) { FORWARD(i, i, flags & SWS_COMP_DIRTY); - apply_filter_weights(&out[i], op->op, op->filter.kernel); + if (!SWS_COMP_TEST(op->filter.comps, i)) + continue; + apply_filter_weights(&out[i], op->op, &op->filter.params); out[i].type = op->filter.type; } break; @@ -738,17 +740,9 @@ void ff_sws_op_list_update(SwsOpList *ops) static void op_ref(SwsOp *op) { switch (op->op) { - case SWS_OP_READ: - if (op->rw.filter.kernel) - av_refstruct_ref(op->rw.filter.kernel); - break; case SWS_OP_DITHER: av_refstruct_ref(op->dither.matrix); break; - case SWS_OP_FILTER_H: - case SWS_OP_FILTER_V: - av_refstruct_ref(op->filter.kernel); - break; case SWS_OP_LUT_3D: av_refstruct_ref_c(op->lut3d.lut); break; @@ -758,16 +752,9 @@ static void op_ref(SwsOp *op) static void op_unref(SwsOp *op) { switch (op->op) { - case SWS_OP_READ: - av_refstruct_unref(&op->rw.filter.kernel); - break; case SWS_OP_DITHER: av_refstruct_unref(&op->dither.matrix); break; - case SWS_OP_FILTER_H: - case SWS_OP_FILTER_V: - av_refstruct_unref(&op->filter.kernel); - break; case SWS_OP_LUT_3D: av_refstruct_unref(&op->lut3d.lut); break; @@ -978,6 +965,18 @@ static const char *const rw_mode_names[] = { [SWS_RW_PALETTE] = "palette" }; +static const char *const scaler_names[] = { + [SWS_SCALE_AUTO] = "auto", + [SWS_SCALE_BILINEAR] = "bilinear", + [SWS_SCALE_BICUBIC] = "bicubic", + [SWS_SCALE_POINT] = "point", + [SWS_SCALE_AREA] = "area", + [SWS_SCALE_GAUSSIAN] = "gaussian", + [SWS_SCALE_SINC] = "sinc", + [SWS_SCALE_LANCZOS] = "lanczos", + [SWS_SCALE_SPLINE] = "spline", +}; + void ff_sws_op_desc(AVBPrint *bp, const SwsOp *op) { const char *name = ff_sws_op_type_name(op->op); @@ -995,9 +994,9 @@ void ff_sws_op_desc(AVBPrint *bp, const SwsOp *op) rw_mode_names[op->rw.mode], op->rw.frac); if (!op->rw.filter.op) break; - const SwsFilterWeights *kernel = op->rw.filter.kernel; - av_bprintf(bp, " + %s filter (%c)", - kernel->name, op->rw.filter.op == SWS_OP_FILTER_H ? 'H' : 'V'); + const SwsFilterParams *params = &op->rw.filter.params; + av_bprintf(bp, " + %s filter (%c)", scaler_names[params->scaler], + op->rw.filter.op == SWS_OP_FILTER_H ? 'H' : 'V'); break; case SWS_OP_LSHIFT: av_bprintf(bp, "%-20s: << %u", name, op->shift.amount); @@ -1058,10 +1057,10 @@ void ff_sws_op_desc(AVBPrint *bp, const SwsOp *op) break; case SWS_OP_FILTER_H: case SWS_OP_FILTER_V: { - const SwsFilterWeights *kernel = op->filter.kernel; + const SwsFilterParams *params = &op->filter.params; av_bprintf(bp, "%-20s: %d -> %d %s", name, - kernel->src_size, kernel->dst_size, - kernel->name); + params->src_size, params->dst_size, + scaler_names[params->scaler]); const SwsCompMask comps = op->filter.comps & mask; if (comps != mask) /* non-uniform filter */ av_bprintf(bp, " on %s", ff_sws_comp_mask_str(comps)); diff --git a/libswscale/ops.h b/libswscale/ops.h index 160ba1b5eeb..e2473e3f86e 100644 --- a/libswscale/ops.h +++ b/libswscale/ops.h @@ -113,8 +113,8 @@ typedef struct SwsReadWriteOp { */ struct { SwsOpType op; /* some value of SWS_OP_FILTER_* */ - SwsFilterWeights *kernel; /* (refstruct) */ SwsPixelType type; /* pixel type to store result as */ + SwsFilterParams params; /* filter parameters */ } filter; } SwsReadWriteOp; @@ -197,7 +197,7 @@ typedef struct SwsLinearOp { uint32_t ff_sws_linear_mask(const SwsLinearOp *c); typedef struct SwsFilterOp { - SwsFilterWeights *kernel; /* filter kernel (refstruct) */ + SwsFilterParams params; /* filter parameters */ SwsPixelType type; /* pixel type to store result as */ SwsCompMask comps; /* affected components */ } SwsFilterOp; diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c index ec871b7ed95..38e5dcc6852 100644 --- a/libswscale/ops_optimizer.c +++ b/libswscale/ops_optimizer.c @@ -771,7 +771,7 @@ retry: (op->filter.comps & needed) == SWS_COMP_ELEMS(prev->rw.elems)) { prev->rw.filter.op = op->op; - prev->rw.filter.kernel = av_refstruct_ref(op->filter.kernel); + prev->rw.filter.params = op->filter.params; prev->rw.filter.type = op->filter.type; ff_sws_op_list_remove_at(ops, n, 1); goto retry; diff --git a/libswscale/uops.c b/libswscale/uops.c index 6f8b276d33f..90c118a800a 100644 --- a/libswscale/uops.c +++ b/libswscale/uops.c @@ -281,7 +281,6 @@ static int translate_rw_op(SwsContext *ctx, SwsUOpList *ops, SwsUOpFlags flags, if (op->op == SWS_OP_WRITE || op->rw.frac || op->rw.mode != SWS_RW_PLANAR) return AVERROR(ENOTSUP); uop.par.filter.type = op->rw.filter.type; - uop.data.kernel = av_refstruct_ref(op->rw.filter.kernel); if (op->rw.filter.op == SWS_OP_FILTER_H) { uop.uop = SWS_UOP_READ_PLANAR_FH; } else if (check_filter_fma(ctx, flags, op)) { @@ -289,6 +288,10 @@ static int translate_rw_op(SwsContext *ctx, SwsUOpList *ops, SwsUOpFlags flags, } else { uop.uop = SWS_UOP_READ_PLANAR_FV; } + + int ret = ff_sws_filter_generate(ctx, &op->rw.filter.params, &uop.data.kernel); + if (ret < 0) + return ret; } else if (op->rw.mode == SWS_RW_PACKED && op->rw.elems > 1) { if (op->rw.frac) return AVERROR(ENOTSUP); diff --git a/libswscale/vulkan/ops.c b/libswscale/vulkan/ops.c index 0da8672d6e1..4bc2231c48e 100644 --- a/libswscale/vulkan/ops.c +++ b/libswscale/vulkan/ops.c @@ -88,9 +88,11 @@ typedef struct VulkanPriv { int nb_data_bufs; VkFormat src_view_fmt[AV_NUM_DATA_POINTERS]; VkFormat dst_view_fmt[AV_NUM_DATA_POINTERS]; + SwsFilterWeights *filters[MAX_FILT_BUFS]; + int nb_filters; int interlaced; - SwsFilterWeights *read_filters[4]; + const SwsFilterWeights *read_filters[4]; /* points into filters[] */ } VulkanPriv; static void spirv_process(const SwsFrame *dst, const SwsFrame *src, @@ -187,8 +189,8 @@ static void free_fn(void *priv) ff_vk_shader_free(&p->s->vkctx, &p->shd); for (int i = 0; i < p->nb_data_bufs; i++) ff_vk_free_buf(&p->s->vkctx, &p->data_bufs[i]); - for (int i = 0; i < 4; i++) - av_refstruct_unref(&p->read_filters[i]); + for (int i = 0; i < p->nb_filters; i++) + av_refstruct_unref(&p->filters[i]); av_refstruct_unref(&p->s); av_free(priv); } @@ -649,15 +651,20 @@ static int emit_read_filtered(SPICtx *spi, SPIRVIDs *id, const SwsOp *op, int *data) { const SwsOpList *ops = id->ops; - const SwsFilterWeights *wd = op->rw.filter.kernel; VulkanPriv *p = id->priv; - if (p->nb_data_bufs >= MAX_DATA_BUFS) + if (p->nb_data_bufs >= MAX_DATA_BUFS || p->nb_filters >= MAX_FILT_BUFS) return AVERROR(ENOTSUP); + const SwsFilterParams *par = &op->rw.filter.params; + int err = ff_sws_filter_generate(&p->s->vkctx, par, &p->filters[p->nb_filters]); + if (err < 0) + return err; + + SwsFilterWeights *wd = p->filters[p->nb_filters++]; for (int i = 0; i < op->rw.elems; i++) p->read_filters[i] = av_refstruct_ref(wd); - int err = create_filter_buf(p->s, wd, &p->data_bufs[p->nb_data_bufs]); + err = create_filter_buf(p->s, wd, &p->data_bufs[p->nb_data_bufs]); if (err < 0) return err; const int binding = add_buf_binding(id); -- 2.54.0 >From 824bee5db04a066fad75a4fcf14b1276994057c6 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Wed, 2 Sep 2026 14:39:30 +0200 Subject: [PATCH 19/20] swscale/uops: add SWS_UOP_FLAG_NO_DATA to skip uop data generation 10%-20% speedup for the FATE tests. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/tests/sws_ops.c | 3 ++- libswscale/uops.c | 14 +++++++++++--- libswscale/uops.h | 1 + libswscale/uops_macros_gen.c | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/libswscale/tests/sws_ops.c b/libswscale/tests/sws_ops.c index b2ca5ec41ea..1851ad941e3 100644 --- a/libswscale/tests/sws_ops.c +++ b/libswscale/tests/sws_ops.c @@ -41,7 +41,8 @@ static int print_ops(SwsContext *ctx, const SwsOpList *ops, SwsCompiledOp *out) const SwsUOpFlags flags = SWS_UOP_FLAG_EXPAND_BIT | SWS_UOP_FLAG_READ_PALETTE - | SWS_UOP_FLAG_ADD; + | SWS_UOP_FLAG_ADD + | SWS_UOP_FLAG_NO_DATA; int ret = ff_sws_ops_translate(ctx, ops, flags, uops); if (ret == AVERROR(ENOTSUP)) goto fail; diff --git a/libswscale/uops.c b/libswscale/uops.c index 90c118a800a..c3576b1b9f4 100644 --- a/libswscale/uops.c +++ b/libswscale/uops.c @@ -289,9 +289,14 @@ static int translate_rw_op(SwsContext *ctx, SwsUOpList *ops, SwsUOpFlags flags, uop.uop = SWS_UOP_READ_PLANAR_FV; } - int ret = ff_sws_filter_generate(ctx, &op->rw.filter.params, &uop.data.kernel); - if (ret < 0) - return ret; + /* Skip generation of filter data if the caller doesn't need it, + * since this can be quite expensive */ + if (!(flags & SWS_UOP_FLAG_NO_DATA)) { + int ret = ff_sws_filter_generate(ctx, &op->rw.filter.params, + &uop.data.kernel); + if (ret < 0) + return ret; + } } else if (op->rw.mode == SWS_RW_PACKED && op->rw.elems > 1) { if (op->rw.frac) return AVERROR(ENOTSUP); @@ -455,6 +460,9 @@ static int translate_dither_op(SwsUOpList *ops, SwsUOpFlags flags, const SwsOp * uop.par.dither.y_offset[i] = off; } + if (flags & SWS_UOP_FLAG_NO_DATA) + return ff_sws_uop_list_append(ops, &uop); + /* Allocate extra rows to allow over-reading for row offsets. Note that * y_offset is currently never larger than 5, so the extra space needed * for this over-allocation is bounded by 5 * size * sizeof(float), diff --git a/libswscale/uops.h b/libswscale/uops.h index 8660de3c41a..b5effd6d726 100644 --- a/libswscale/uops.h +++ b/libswscale/uops.h @@ -152,6 +152,7 @@ typedef enum SwsUOpFlagBits { SWS_UOP_FLAG_EXPAND_BIT = (1 << 2), /* backend implements SWS_UOP_EXPAND_BIT */ SWS_UOP_FLAG_READ_PALETTE = (1 << 3), /* backend implements SWS_UOP_READ_PALETTE */ SWS_UOP_FLAG_ADD = (1 << 4), /* backend implements SWS_UOP_ADD */ + SWS_UOP_FLAG_NO_DATA = (1 << 5), /* skip generation of op data (where beneficial) */ } SwsUOpFlagBits; typedef enum SwsUOpType { diff --git a/libswscale/uops_macros_gen.c b/libswscale/uops_macros_gen.c index 5cdf35e802d..21a20fc2a33 100644 --- a/libswscale/uops_macros_gen.c +++ b/libswscale/uops_macros_gen.c @@ -226,7 +226,7 @@ static int register_flags(SwsContext *ctx, const SwsOpList *ops, SwsUOpFlags fla if (!uops) return AVERROR(ENOMEM); - int ret = ff_sws_ops_translate(ctx, ops, flags, uops); + int ret = ff_sws_ops_translate(ctx, ops, flags | SWS_UOP_FLAG_NO_DATA, uops); if (ret < 0) goto fail; -- 2.54.0 >From 4cd2a2bd78e5965a9f440a0c6c2ec0d512011335 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Sat, 24 Oct 2026 17:37:31 +0200 Subject: [PATCH 20/20] swscale/filters: merge consecutive (compatible) filters This collapses any sort of separate chroma-upscaling + scaling pass into a single filter step. Requires an extra optimization step to remove now-noop identity filters. Will become relevant after adding chroma scaling support. Signed-off-by: Niklas Haas <[email protected]> --- libswscale/filters.c | 40 ++++++++++++++++++++++++++++++++++++++ libswscale/filters.h | 7 +++++++ libswscale/ops_optimizer.c | 18 ++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/libswscale/filters.c b/libswscale/filters.c index b4403431189..74024e8a1d3 100644 --- a/libswscale/filters.c +++ b/libswscale/filters.c @@ -164,6 +164,46 @@ static bool validate_params(const SwsFilterFunction *fun, SwsScaler scaler) } } +bool ff_sws_filter_merge(SwsFilterParams *p1, const SwsFilterParams *p2) +{ + if (p1->dst_size != p2->src_size) + return false; + + SwsFilterParams merged = *p1; + if (merged.scaler == SWS_SCALE_AUTO) { + merged.scaler = p2->scaler; + memcpy(merged.scaler_params, p2->scaler_params, sizeof(p2->scaler_params)); + } + + /* Check scaler equality */ + if (merged.scaler != p2->scaler) + return false; + for (int i = 0; i < SWS_NUM_SCALER_PARAMS; i++) { + if (merged.scaler_params[i] != p2->scaler_params[i]) + return false; + } + + const double V1 = p1->virtual_size ? p1->virtual_size : p1->dst_size; + const double V2 = p2->virtual_size ? p2->virtual_size : p2->dst_size; + + /** + * Solve for the affine composition of + * T(x) = a*x + (a - 1)/2 + offset + * with a = src_size / V + */ + merged.virtual_size = V1 * V2 / p1->dst_size; + merged.offset = p1->offset + (p1->src_size / V1) * p2->offset; + merged.src_size = p1->src_size; + merged.dst_size = p2->dst_size; + + /* Test if merged filter is representable */ + if (ff_sws_filter_generate(NULL, &merged, NULL) < 0) + return false; + + *p1 = merged; + return true; +} + /** * Numerically estimate the last intersection between the function value * and the cutoff domain [-SWS_MAX_REDUCE_CUTOFF, SWS_MAX_REDUCE_CUTOFF]. diff --git a/libswscale/filters.h b/libswscale/filters.h index 68398a9ca3b..f2a70d6eb12 100644 --- a/libswscale/filters.h +++ b/libswscale/filters.h @@ -21,6 +21,8 @@ #ifndef SWSCALE_FILTERS_H #define SWSCALE_FILTERS_H +#include <stdbool.h> + #include <libavutil/refstruct.h> #include "swscale.h" @@ -86,6 +88,11 @@ typedef struct SwsFilterParams { */ int ff_sws_filter_overshoot(const SwsFilterParams *params); +/** + * Try merging two filters into a single filter. Returns true on success. + */ +bool ff_sws_filter_merge(SwsFilterParams *p1, const SwsFilterParams *p2); + /** * Represents a computed filter kernel. */ diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c index 38e5dcc6852..bfadf362ba8 100644 --- a/libswscale/ops_optimizer.c +++ b/libswscale/ops_optimizer.c @@ -360,6 +360,12 @@ static int op_result_is_exact(const SwsOp *op) return true; } +static int filter_is_noop(const SwsFilterParams *p) +{ + return p->src_size == p->dst_size && fabs(p->offset) < 1e-9 && + (!p->virtual_size || fabs(p->virtual_size - p->dst_size) < 1e-9); +} + int ff_sws_op_list_optimize(SwsOpList *ops) { int ret; @@ -760,11 +766,21 @@ retry: case SWS_OP_FILTER_H: case SWS_OP_FILTER_V: /* Eliminate redundant no-op filter */ - if (!(op->filter.comps & needed)) { + if (!(op->filter.comps & needed) || filter_is_noop(&op->filter.params)) { ff_sws_op_list_remove_at(ops, n, 1); goto retry; } + /* Try to merge with adjacent filter in the same direction */ + if (op->op == next->op && + (op->filter.comps & needed) == (next->filter.comps & needed) && + ff_sws_filter_merge(&op->filter.params, &next->filter.params)) + { + op->filter.type = next->filter.type; + ff_sws_op_list_remove_at(ops, n + 1, 1); + goto retry; + } + /* Merge with prior simple planar read */ if (prev->op == SWS_OP_READ && !prev->rw.filter.op && prev->rw.mode == SWS_RW_PLANAR && !prev->rw.frac && -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
