diff mbox series

[FFmpeg-devel] lavfi: add a libplacebo filter

Message ID 20210309163802.67960-1-ffmpeg@haasn.xyz
State New
Headers show
Series [FFmpeg-devel] lavfi: add a libplacebo filter | expand

Checks

Context Check Description
andriy/x86_make success Make finished
andriy/x86_make_fate success Make fate finished
andriy/PPC64_make success Make finished
andriy/PPC64_make_fate success Make fate finished

Commit Message

Niklas Haas March 9, 2021, 4:38 p.m. UTC
From: Niklas Haas <git@haasn.xyz>

This filter conceptually maps the libplacebo `pl_renderer` API into
libavfilter, which is a high-level image rendering API designed to work
with an RGB pipeline internally. As such, there's no way to avoid e.g.
chroma interpolation with this filter, although new versions of
libplacebo support outputting back to subsampled YCbCr after processing
is done.

That being said, `pl_renderer` supports automatic integration of the
majority of libplacebo's shaders, ranging from debanding to tone
mapping, and also supports loading custom mpv-style user shaders, making
this API a natural candidate for getting a lot of functionality out of
relatively little code.

In the future, I may approach this problem either by rewriting this
filter to also support a non-renderer codepath, or by upgrading
libplacebo's renderer to support a full YCbCr pipeline.

It's worth noting that this filter could be extended in the future to
support HWframes frames of type AV_PIX_FMT_VULKAN, but this is not
currently possible due to API limitations in the vulkan hwaccel.
(Although it's also worth pointing out that, apart from a single
`pl_vulkan_create` call, the current `vf_libplacebo` code does not
depend on vulkan at all - so in theory it could be extended to support
OpenGL or other libplacebo backends)
---
 configure                   |   3 +
 libavfilter/Makefile        |   1 +
 libavfilter/allfilters.c    |   1 +
 libavfilter/vf_libplacebo.c | 627 ++++++++++++++++++++++++++++++++++++
 4 files changed, 632 insertions(+)
 create mode 100644 libavfilter/vf_libplacebo.c

Comments

James Almer March 9, 2021, 4:53 p.m. UTC | #1
On 3/9/2021 1:38 PM, Niklas Haas wrote:
> +    if (s->skip_av1_grain)
> +        image.av1_grain = (struct pl_av1_grain_data) {0};

[...]

> +    if (!s->skip_av1_grain)
> +        av_frame_remove_side_data(out, AV_FRAME_DATA_FILM_GRAIN_PARAMS);

[...]

> +    { "skip_av1_grain", "Disable AV1 grain application", OFFSET(skip_av1_grain), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },

How does this work? Does pl_upload_avframe() support parsing the 
filmgrain side data? If so, that's pretty cool.

Also, I'd prefer if this was inverted. So make it filmgrain or maybe 
apply_filmgrain, and of course set it to 1 by default. And it doesn't 
need to specify it's for AV1 since the AV_FRAME_DATA_FILM_GRAIN_PARAMS 
side data type could be extended to support the H.274 implementation in 
the future.
Niklas Haas March 9, 2021, 5:18 p.m. UTC | #2
On Tue, 09 Mar 2021 13:53:20 -0300 James Almer <jamrial@gmail.com> wrote:
> On 3/9/2021 1:38 PM, Niklas Haas wrote:
> > +    if (s->skip_av1_grain)
> > +        image.av1_grain = (struct pl_av1_grain_data) {0};
> 
> [...]
> 
> > +    if (!s->skip_av1_grain)
> > +        av_frame_remove_side_data(out, AV_FRAME_DATA_FILM_GRAIN_PARAMS);
> 
> [...]
> 
> > +    { "skip_av1_grain", "Disable AV1 grain application", OFFSET(skip_av1_grain), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
> 
> How does this work? Does pl_upload_avframe() support parsing the 
> filmgrain side data? If so, that's pretty cool.

Yes. More specifically, `pl_frame_from_avframe` translates the film
grain side data into `pl_av1_grain_data`, and `pl_render_image` applies
film grain using a compute shader.

> Also, I'd prefer if this was inverted. So make it filmgrain or maybe 
> apply_filmgrain, and of course set it to 1 by default. And it doesn't 
> need to specify it's for AV1 since the AV_FRAME_DATA_FILM_GRAIN_PARAMS 
> side data type could be extended to support the H.274 implementation in 
> the future.

This makes sense. Changed. Upon further consideration, I'm also not sure
if this should really be enabled by default. As I understand it, this
side data is only present if the user specifically disabled film grain
application in the decoder, so overriding that by just moving it to
vf_libplacebo seems like a surprising default.
James Almer March 9, 2021, 5:28 p.m. UTC | #3
On 3/9/2021 2:18 PM, Niklas Haas wrote:
> On Tue, 09 Mar 2021 13:53:20 -0300 James Almer <jamrial@gmail.com> wrote:
>> On 3/9/2021 1:38 PM, Niklas Haas wrote:
>>> +    if (s->skip_av1_grain)
>>> +        image.av1_grain = (struct pl_av1_grain_data) {0};
>>
>> [...]
>>
>>> +    if (!s->skip_av1_grain)
>>> +        av_frame_remove_side_data(out, AV_FRAME_DATA_FILM_GRAIN_PARAMS);
>>
>> [...]
>>
>>> +    { "skip_av1_grain", "Disable AV1 grain application", OFFSET(skip_av1_grain), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
>>
>> How does this work? Does pl_upload_avframe() support parsing the
>> filmgrain side data? If so, that's pretty cool.
> 
> Yes. More specifically, `pl_frame_from_avframe` translates the film
> grain side data into `pl_av1_grain_data`, and `pl_render_image` applies
> film grain using a compute shader.
> 
>> Also, I'd prefer if this was inverted. So make it filmgrain or maybe
>> apply_filmgrain, and of course set it to 1 by default. And it doesn't
>> need to specify it's for AV1 since the AV_FRAME_DATA_FILM_GRAIN_PARAMS
>> side data type could be extended to support the H.274 implementation in
>> the future.
> 
> This makes sense. Changed. Upon further consideration, I'm also not sure
> if this should really be enabled by default. As I understand it, this
> side data is only present if the user specifically disabled film grain
> application in the decoder, so overriding that by just moving it to
> vf_libplacebo seems like a surprising default.

Yes, film grain is exported as side data instead of being applied by the 
decoder when the AV_CODEC_EXPORT_DATA_FILM_GRAIN flag is set in the 
AVCodecContext export_side_data field.

So yeah, disabled by default sounds better. The caller may not want 
vf_libplacebo to apply it, and instead leave that to some other filter 
in the chain, or just use it in some other way after getting the 
resulting AVFrame.
Nicolas George March 9, 2021, 5:53 p.m. UTC | #4
Niklas Haas (12021-03-09):
> From: Niklas Haas <git@haasn.xyz>
> 
> This filter conceptually maps the libplacebo `pl_renderer` API into
> libavfilter, which is a high-level image rendering API designed to work
> with an RGB pipeline internally. As such, there's no way to avoid e.g.
> chroma interpolation with this filter, although new versions of
> libplacebo support outputting back to subsampled YCbCr after processing
> is done.
> 
> That being said, `pl_renderer` supports automatic integration of the
> majority of libplacebo's shaders, ranging from debanding to tone
> mapping, and also supports loading custom mpv-style user shaders, making
> this API a natural candidate for getting a lot of functionality out of
> relatively little code.
> 
> In the future, I may approach this problem either by rewriting this
> filter to also support a non-renderer codepath, or by upgrading
> libplacebo's renderer to support a full YCbCr pipeline.
> 
> It's worth noting that this filter could be extended in the future to
> support HWframes frames of type AV_PIX_FMT_VULKAN, but this is not
> currently possible due to API limitations in the vulkan hwaccel.
> (Although it's also worth pointing out that, apart from a single
> `pl_vulkan_create` call, the current `vf_libplacebo` code does not
> depend on vulkan at all - so in theory it could be extended to support
> OpenGL or other libplacebo backends)
> ---
>  configure                   |   3 +
>  libavfilter/Makefile        |   1 +
>  libavfilter/allfilters.c    |   1 +
>  libavfilter/vf_libplacebo.c | 627 ++++++++++++++++++++++++++++++++++++
>  4 files changed, 632 insertions(+)
>  create mode 100644 libavfilter/vf_libplacebo.c

Thanks for the patch. I have not yet looked at the code itself.

Please include some documentation, it would be helpful even for
reviewing.

Regards,
Lynne March 9, 2021, 9:53 p.m. UTC | #5
Mar 9, 2021, 17:38 by ffmpeg@haasn.xyz:

> From: Niklas Haas <git@haasn.xyz>
>
> This filter conceptually maps the libplacebo `pl_renderer` API into
> libavfilter, which is a high-level image rendering API designed to work
> with an RGB pipeline internally. As such, there's no way to avoid e.g.
> chroma interpolation with this filter, although new versions of
> libplacebo support outputting back to subsampled YCbCr after processing
> is done.
>
> That being said, `pl_renderer` supports automatic integration of the
> majority of libplacebo's shaders, ranging from debanding to tone
> mapping, and also supports loading custom mpv-style user shaders, making
> this API a natural candidate for getting a lot of functionality out of
> relatively little code.
>
> In the future, I may approach this problem either by rewriting this
> filter to also support a non-renderer codepath, or by upgrading
> libplacebo's renderer to support a full YCbCr pipeline.
>
> It's worth noting that this filter could be extended in the future to
> support HWframes frames of type AV_PIX_FMT_VULKAN, but this is not
> currently possible due to API limitations in the vulkan hwaccel.
> (Although it's also worth pointing out that, apart from a single
> `pl_vulkan_create` call, the current `vf_libplacebo` code does not
> depend on vulkan at all - so in theory it could be extended to support
> OpenGL or other libplacebo backends)
>

Sorry, as we discussed on IRC the Vulkan hwcontext should be
improved first and this filter should be kept as a pure hardware
filter.

I'm sorry, I know I've been holding up this patch for a few months,
but could you wait until I do my job with the hwcontext? I'm already
working on dozens of different things and I don't need extra pressure.
Others can discuss filter options and such, but do keep in mind
this isn't going to be the final version.
James Almer March 9, 2021, 9:57 p.m. UTC | #6
On 3/9/2021 6:53 PM, Lynne wrote:
> Mar 9, 2021, 17:38 by ffmpeg@haasn.xyz:
> 
>> From: Niklas Haas <git@haasn.xyz>
>>
>> This filter conceptually maps the libplacebo `pl_renderer` API into
>> libavfilter, which is a high-level image rendering API designed to work
>> with an RGB pipeline internally. As such, there's no way to avoid e.g.
>> chroma interpolation with this filter, although new versions of
>> libplacebo support outputting back to subsampled YCbCr after processing
>> is done.
>>
>> That being said, `pl_renderer` supports automatic integration of the
>> majority of libplacebo's shaders, ranging from debanding to tone
>> mapping, and also supports loading custom mpv-style user shaders, making
>> this API a natural candidate for getting a lot of functionality out of
>> relatively little code.
>>
>> In the future, I may approach this problem either by rewriting this
>> filter to also support a non-renderer codepath, or by upgrading
>> libplacebo's renderer to support a full YCbCr pipeline.
>>
>> It's worth noting that this filter could be extended in the future to
>> support HWframes frames of type AV_PIX_FMT_VULKAN, but this is not
>> currently possible due to API limitations in the vulkan hwaccel.
>> (Although it's also worth pointing out that, apart from a single
>> `pl_vulkan_create` call, the current `vf_libplacebo` code does not
>> depend on vulkan at all - so in theory it could be extended to support
>> OpenGL or other libplacebo backends)
>>
> 
> Sorry, as we discussed on IRC the Vulkan hwcontext should be
> improved first and this filter should be kept as a pure hardware
> filter.

Can you explain why should it be a pure hardware filter? Why can't it be 
committed like this now, then updated to use the Vulkan hwcontext once 
you improve it?

> 
> I'm sorry, I know I've been holding up this patch for a few months,
> but could you wait until I do my job with the hwcontext? I'm already
> working on dozens of different things and I don't need extra pressure.
> Others can discuss filter options and such, but do keep in mind
> this isn't going to be the final version.
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
> 
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
>
Niklas Haas March 9, 2021, 10:36 p.m. UTC | #7
On Tue, 09 Mar 2021 22:53:18 +0100 Lynne <dev@lynne.ee> wrote:
> Sorry, as we discussed on IRC the Vulkan hwcontext should be
> improved first and this filter should be kept as a pure hardware
> filter.

I changed my mind on this because:

- I think it's reasonably self-contained as is, and actually simpler to
  implement this way.

- There's no duplication of code within this patch.

- Sharing the VkImages directly is simply not a good approach. It
  creates an unnecessary API hassle (as demonstrated by the multitude of
  solved and unsolved issues surrounding device negotiation,
  synchronization, etc.)

- Therefore, it's better to share using an agreed-upon mechanism like
  a shared memory object exported/imported using the Vulkan API.

- A good API for this already exists (dmabufs).

- You can already derive a drm device from a vulkan device and export
  vulkan hwframes as drmprime frames.

I plan on adding support for drmprime frames to this filter, which I
think is the cleaner way to avoid the upload/download. But I wanted to
get feedback on the current version, since it's *functionally* complete
as-is.

If you're worried about dmabufs being a Linux-only API, I would still
rather see an approach based on Vulkan memory object imports/exports
rather than attempting to share and synchronize the VkImages directly.

Even just the fact that we can't each enable extensions/features we want
enabled independently is a reason that trying to share a single vulkan
device is a bad idea. Right now the result of that is that vf_libplacebo
atop hwcontext_vulkan would simply be missing out on features and
performance, compared to this version.
Lynne March 9, 2021, 10:55 p.m. UTC | #8
Mar 9, 2021, 23:36 by ffmpeg@haasn.xyz:

> On Tue, 09 Mar 2021 22:53:18 +0100 Lynne <dev@lynne.ee> wrote:
>
>> Sorry, as we discussed on IRC the Vulkan hwcontext should be
>> improved first and this filter should be kept as a pure hardware
>> filter.
>>
>
> I changed my mind on this because:
>
> - I think it's reasonably self-contained as is, and actually simpler to
>  implement this way.
>

It's simpler because it makes no effort to integrate with anything
we use.


> - There's no duplication of code within this patch.
>

There is, it's just hidden in libplacebo. If you integrate with the
framework code like in the previous PR, there's no duplication.
And no, we will not be removing the current Vulkan filters and
we intend to continue expanding them. Not even if you add similar
filters to libplacebo.


> - Sharing the VkImages directly is simply not a good approach. It
>  creates an unnecessary API hassle (as demonstrated by the multitude of
>  solved and unsolved issues surrounding device negotiation,
>  synchronization, etc.)
>

It's a good approach since API users work with VkImages. It's more
complex because it's explicit.


> - Therefore, it's better to share using an agreed-upon mechanism like
>  a shared memory object exported/imported using the Vulkan API.
>

There is a generic shared hardware memory export/import format,
it's called Vulkan, this is partly why it was created.


> - A good API for this already exists (dmabufs).
>

See above.


> - You can already derive a drm device from a vulkan device and export
>  vulkan hwframes as drmprime frames.
>

With an extension. Only supported on Linux. Only supported on recent
AMD cards and drivers. Which is also buggy and crashes drivers. And
the Intel implementation has been sitting as a PR for more than 3 years!
You may have the privilege of using it because you lucked out on
hardware, but the rest of us are not so lucky.


> I plan on adding support for drmprime frames to this filter, which I
> think is the cleaner way to avoid the upload/download. But I wanted to
> get feedback on the current version, since it's *functionally* complete
> as-is.
>

I won't accept adding drmprime filtering functionality either.
This is a pure hardware Vulkan filtering infrastructure. We keep nothing
hidden from users when it comes to filtering.


> If you're worried about dmabufs being a Linux-only API, I would still
> rather see an approach based on Vulkan memory object imports/exports
> rather than attempting to share and synchronize the VkImages directly.
>

Windows still accounts for a huge amount of our userbase. Even on
servers which do encoding. And so does Nvidia hardware.
In fact, Nvidia transcoding setups are the major users of hardware filtering.


> Even just the fact that we can't each enable extensions/features we want
> enabled independently is a reason that trying to share a single vulkan
> device is a bad idea. Right now the result of that is that vf_libplacebo
> atop hwcontext_vulkan would simply be missing out on features and
> performance, compared to this version.
>

That's plain wrong. We wouldn't be missing out on features.

I'm sorry, but I'm not going to change my mind on this. I'd really appreciate
your suggestions elsewhere, particularly in the are of making our libraries
interoperable.
Niklas Haas March 11, 2021, 11:34 a.m. UTC | #9
On Tue, 09 Mar 2021 23:55:36 +0100 Lynne <dev@lynne.ee> wrote:
> It's simpler because it makes no effort to integrate with anything
> we use.

To be clear, I agree that integration with the hwcontext is the ultimate
goal. I'm just arguing about the usefulness of allowing
vf_libplacebo to exist as-is until then. The amount of extra code to
support a CPU filter path is negligible, so why not merge it as-is
rather than let it rot? Do you have a good reason to NAK this patch
as-is other than "it's slower than it could be"?

> There is, it's just hidden in libplacebo. If you integrate with the
> framework code like in the previous PR, there's no duplication.
> And no, we will not be removing the current Vulkan filters and
> we intend to continue expanding them. Not even if you add similar
> filters to libplacebo.

The duplication in libplacebo exists already for reasons independent of
ffmpeg, so there's no wasted effort here. And this duplication of logic
will fundamentally continue existing even if we do integrate with
hwcontext_vulkan, because libplacebo performs its own vulkan calls one
way or the other.

(Also, as I argued before, it's not strictly speaking a duplication
because the context creation code in libplacebo is technically different
from the one in libavutil - notably, libplacebo enables a different set
of extensions and features.)

> It's a good approach since API users work with VkImages. It's more
> complex because it's explicit.

It may be a good approach in principle but it doesn't seem to be working
in practice, which is my concern. The downside of vulkan's explicitness
is that it's harder to share vulkan state across API or ABI boundaries,
and as the synchronization issue highlights, getting this API right
seems to be nontrivial enough that it's preventing me from using
hwcontext_vulkan.

Khronos specifically points out that they intended for existing
platform-specific resource sharing mechanisms (fds and handles) to be
used to communicate VkImages across process boundaries or between
VkDevice instances, which is why VK_KHR_external_memory exists.

While libplacebo and libavfilter may technically share the same address
space, the differing requirements between the two seems a valid reason
to have separate VkDevices and thus use explicit resource sharing
extensions rather than communicating VkImage pointers directly.

> With an extension. Only supported on Linux. Only supported on recent
> AMD cards and drivers. Which is also buggy and crashes drivers. And
> the Intel implementation has been sitting as a PR for more than 3 years!
> You may have the privilege of using it because you lucked out on
> hardware, but the rest of us are not so lucky.

(I'm not sure what you mean - what extension is required? Are you talking
about DRM modifier support inside vulkan? Those work fine on intel, and
now also AMD.)

That said, I changed my mind and agree that something inherently
Linux-only is probably not a long-term solution, so we definitely do
need some vendor-agnostic vulkan-based sharing API. Whether it's based
on sharing VkImages directly or by sharing vulkan memory objects
indirectly is not as important as that it works. But I guess let's not
confuse the issue of how to fix hwcontext_vulkan with the issue of
whether it's okay to merge this patch as-is for the time being.
Lynne March 11, 2021, 12:55 p.m. UTC | #10
Mar 11, 2021, 12:34 by ffmpeg@haasn.xyz:

> On Tue, 09 Mar 2021 23:55:36 +0100 Lynne <dev@lynne.ee> wrote:
>
>> It's simpler because it makes no effort to integrate with anything
>> we use.
>>
>
> To be clear, I agree that integration with the hwcontext is the ultimate
> goal. I'm just arguing about the usefulness of allowing
> vf_libplacebo to exist as-is until then. The amount of extra code to
> support a CPU filter path is negligible, so why not merge it as-is
> rather than let it rot? Do you have a good reason to NAK this patch
> as-is other than "it's slower than it could be"?
>

Yes, it's a hardware filter library that will be poorly integrated. I've said
it before, when it comes to hardware it's our policy to leave everything
to the user, right down to device selection, uploading and choosing a
hardware version of a filter. And it also duplicates functionality that
we already have "as a workaround", while it's my opinion that if there's
something lacking that prevents us from being able to integrate with
other libraries, it should be improved, rather than being left to stagnate
due to workarounds which have become the standard, with the proper
solution being relegated to obscurity.


>> There is, it's just hidden in libplacebo. If you integrate with the
>> framework code like in the previous PR, there's no duplication.
>> And no, we will not be removing the current Vulkan filters and
>> we intend to continue expanding them. Not even if you add similar
>> filters to libplacebo.
>>
>
> The duplication in libplacebo exists already for reasons independent of
> ffmpeg, so there's no wasted effort here. And this duplication of logic
> will fundamentally continue existing even if we do integrate with
> hwcontext_vulkan, because libplacebo performs its own vulkan calls one
> way or the other.
>

It's because you chose to bundle everything into libplacebo. It's hard to
not have code duplication unless we did GUI code or window initialization.
I think this was possibly a wrong direction, and a more minimal approach would
have been better for the main library, with other components like uploading,
downloading, rendering, filtering, wrappers being split into separate libraries.
That way libplacebo's API would have been less monolithic and more
accommodating to API users who don't want to have everything handled
for them.


> (Also, as I argued before, it's not strictly speaking a duplication
> because the context creation code in libplacebo is technically different
> from the one in libavutil - notably, libplacebo enables a different set
> of extensions and features.)
>

Which we can easily update.
I think our queue hwcontext code and image uploading/downloading
is also done better than what libplacebo does.


>> It's a good approach since API users work with VkImages. It's more
>> complex because it's explicit.
>>
>
> It may be a good approach in principle but it doesn't seem to be working
> in practice, which is my concern. The downside of vulkan's explicitness
> is that it's harder to share vulkan state across API or ABI boundaries,
> and as the synchronization issue highlights, getting this API right
> seems to be nontrivial enough that it's preventing me from using
> hwcontext_vulkan.
>

We designed the APIs to work together, and you already submitted
an older version of the patch which used the API. It's me who blocked
it because I wanted to rely on having proper synchronization.

If you want to blame me for not working on it right away and fixing it
2 months ago, feel free to PM me. But please, do not use patches
like these to force me to do it.


> Khronos specifically points out that they intended for existing
> platform-specific resource sharing mechanisms (fds and handles) to be
> used to communicate VkImages across process boundaries or between
> VkDevice instances, which is why VK_KHR_external_memory exists.
>

Even if they do (which I don't recall reading about, and no, the APIs
being there do not indicate that's their intended purpose), why bother
with external image representations when you still have to follow the
exact same rules regarding synchronization as everything else?
Also, it gets better - some platforms (like Linux) completely lack synchronization
primitives for their images. There's no API, no way to link a semaphore
to an FD, nothing. So it would be worse than having difficult to get
synchronization, unless you wanted to see screen tearing in lavfi.


>> With an extension. Only supported on Linux. Only supported on recent
>> AMD cards and drivers. Which is also buggy and crashes drivers. And
>> the Intel implementation has been sitting as a PR for more than 3 years!
>> You may have the privilege of using it because you lucked out on
>> hardware, but the rest of us are not so lucky.
>>
>
> (I'm not sure what you mean - what extension is required? Are you talking
> about DRM modifier support inside vulkan? Those work fine on intel, and
> now also AMD.)
>

Those do NOT work fine on Intel. If you had any contact with lower levels
of hardware you'd have known. Modifiers work in that a SINGLE modifier
is assumed (X tiling IIRC) and all others are ignored. So screen sharing
doesn't work if your buffer is compressed or uses Y tiling. VAAPI to
Vulkan importing works purely out of coincidence.


> That said, I changed my mind and agree that something inherently
> Linux-only is probably not a long-term solution, so we definitely do
> need some vendor-agnostic vulkan-based sharing API. Whether it's based
> on sharing VkImages directly or by sharing vulkan memory objects
> indirectly is not as important as that it works. But I guess let's not
> confuse the issue of how to fix hwcontext_vulkan with the issue of
> whether it's okay to merge this patch as-is for the time being.
>

It's not okay to merge as long as it has software frame support still enabled.
diff mbox series

Patch

diff --git a/configure b/configure
index d11942fced..859f9de93b 100755
--- a/configure
+++ b/configure
@@ -1794,6 +1794,7 @@  EXTERNAL_LIBRARY_LIST="
     libopenmpt
     libopenvino
     libopus
+    libplacebo
     libpulse
     librabbitmq
     librav1e
@@ -3574,6 +3575,7 @@  interlace_filter_deps="gpl"
 kerndeint_filter_deps="gpl"
 ladspa_filter_deps="ladspa libdl"
 lensfun_filter_deps="liblensfun version3"
+libplacebo_filter_deps="libplacebo"
 lv2_filter_deps="lv2"
 mcdeint_filter_deps="avcodec gpl"
 movie_filter_deps="avcodec avformat"
@@ -6406,6 +6408,7 @@  enabled libopus           && {
         require_pkg_config libopus opus opus_multistream.h opus_multistream_surround_encoder_create
     }
 }
+enabled libplacebo        && require_pkg_config libplacebo "libplacebo >= 3.116.0" libplacebo/vulkan.h pl_vulkan_create
 enabled libpulse          && require_pkg_config libpulse libpulse pulse/pulseaudio.h pa_context_new
 enabled librabbitmq       && require_pkg_config librabbitmq "librabbitmq >= 0.7.1" amqp.h amqp_new_connection
 enabled librav1e          && require_pkg_config librav1e "rav1e >= 0.4.0" rav1e.h rav1e_context_new
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index b2c254ea67..aed1abb374 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -310,6 +310,7 @@  OBJS-$(CONFIG_KIRSCH_FILTER)                 += vf_convolution.o
 OBJS-$(CONFIG_LAGFUN_FILTER)                 += vf_lagfun.o
 OBJS-$(CONFIG_LENSCORRECTION_FILTER)         += vf_lenscorrection.o
 OBJS-$(CONFIG_LENSFUN_FILTER)                += vf_lensfun.o
+OBJS-$(CONFIG_LIBPLACEBO_FILTER)             += vf_libplacebo.o
 OBJS-$(CONFIG_LIBVMAF_FILTER)                += vf_libvmaf.o framesync.o
 OBJS-$(CONFIG_LIMITER_FILTER)                += vf_limiter.o
 OBJS-$(CONFIG_LOOP_FILTER)                   += f_loop.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 0872c6e0f2..07a411f19e 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -294,6 +294,7 @@  extern AVFilter ff_vf_kirsch;
 extern AVFilter ff_vf_lagfun;
 extern AVFilter ff_vf_lenscorrection;
 extern AVFilter ff_vf_lensfun;
+extern AVFilter ff_vf_libplacebo;
 extern AVFilter ff_vf_libvmaf;
 extern AVFilter ff_vf_limiter;
 extern AVFilter ff_vf_loop;
diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c
new file mode 100644
index 0000000000..583def9a1c
--- /dev/null
+++ b/libavfilter/vf_libplacebo.c
@@ -0,0 +1,627 @@ 
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "libavutil/file.h"
+#include "libavutil/opt.h"
+#include "internal.h"
+#include "scale_eval.h"
+
+#include <libplacebo/renderer.h>
+#include <libplacebo/utils/libav.h>
+#include <libplacebo/vulkan.h>
+
+typedef struct LibplaceboContext {
+    const AVClass *class;
+
+    /* libplacebo */
+    struct pl_context *ctx;
+    const struct pl_vulkan *vulkan;
+    const struct pl_gpu *gpu;
+    struct pl_renderer *renderer;
+    const struct pl_tex *image_tex[4];
+    const struct pl_tex *target_tex[4];
+
+    /* settings */
+    char *out_format_string;
+    char *w_expr;
+    char *h_expr;
+    AVRational target_sar;
+    float pad_crop_ratio;
+    int force_original_aspect_ratio;
+    int force_divisible_by;
+    int normalize_sar;
+    int skip_av1_grain;
+    int colorspace;
+    int color_range;
+    int color_primaries;
+    int color_trc;
+
+    /* pl_render_params */
+    char *upscaler;
+    char *downscaler;
+    int lut_entries;
+    float antiringing;
+    int sigmoid;
+    int skip_aa;
+    float polar_cutoff;
+    int disable_linear;
+    int disable_builtin;
+    int force_3dlut;
+    int force_dither;
+    int disable_fbos;
+
+    /* pl_deband_params */
+    int deband;
+    int deband_iterations;
+    float deband_threshold;
+    float deband_radius;
+    float deband_grain;
+
+    /* pl_color_adjustment */
+    float brightness;
+    float contrast;
+    float saturation;
+    float hue;
+    float gamma;
+
+    /* pl_peak_detect_params */
+    int peakdetect;
+    float smoothing;
+    float scene_low;
+    float scene_high;
+    float overshoot;
+
+    /* pl_color_map_params */
+    int intent;
+    int tonemapping;
+    float tonemapping_param;
+    float desat_str;
+    float desat_exp;
+    float desat_base;
+    float max_boost;
+    int gamut_warning;
+    int gamut_clipping;
+
+     /* pl_dither_params */
+    int dithering;
+    int dither_lut_size;
+    int dither_temporal;
+
+    /* pl_cone_params */
+    int cones;
+    float cone_str;
+
+    /* custom shaders */
+    char *shader_path;
+    void *shader_bin;
+    int shader_bin_len;
+    const struct pl_hook *hooks[2];
+    int num_hooks;
+} LibplaceboContext;
+
+static void pl_av_log(void *log_ctx, enum pl_log_level level, const char *msg)
+{
+    int av_lev;
+
+    switch (level) {
+    case PL_LOG_FATAL:  av_lev = AV_LOG_FATAL;   break;
+    case PL_LOG_ERR:    av_lev = AV_LOG_ERROR;   break;
+    case PL_LOG_WARN:   av_lev = AV_LOG_WARNING; break;
+    case PL_LOG_INFO:   av_lev = AV_LOG_VERBOSE; break;
+    case PL_LOG_DEBUG:  av_lev = AV_LOG_DEBUG;   break;
+    case PL_LOG_TRACE:  av_lev = AV_LOG_TRACE;   break;
+    default: return;
+    }
+
+    av_log(log_ctx, av_lev, "%s\n", msg);
+}
+
+static int parse_shader(AVFilterContext *avctx, const void *shader, size_t len)
+{
+    LibplaceboContext *s = avctx->priv;
+    const struct pl_hook *hook;
+
+    hook = pl_mpv_user_shader_parse(s->gpu, shader, len);
+    if (!hook) {
+        av_log(s, AV_LOG_ERROR, "Failed parsing custom shader!\n");
+        return AVERROR(EINVAL);
+    }
+
+    s->hooks[s->num_hooks++] = hook;
+    return 0;
+}
+
+#define RET(x)                                                                 \
+    do {                                                                       \
+        if ((err = (x)) < 0)                                                   \
+            goto fail;                                                         \
+    } while (0)
+
+static int init(AVFilterContext *avctx)
+{
+    int err;
+    LibplaceboContext *s = avctx->priv;
+    uint8_t *buf = NULL;
+    size_t buf_len;
+
+    /* Create the main libplacebo context */
+    s->ctx = pl_context_create(PL_API_VER, &(struct pl_context_params) {
+        .log_cb = pl_av_log,
+        .log_priv = s,
+        .log_level = PL_LOG_DEBUG,
+    });
+
+    if (!s->ctx)
+        return AVERROR(ENOMEM);
+
+    /* Create the libplacebo GPU context */
+    s->vulkan = pl_vulkan_create(s->ctx, &pl_vulkan_default_params);
+    if (!s->vulkan) {
+        av_log(s, AV_LOG_ERROR, "Failed creating vulkan device!\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    /* Create the renderer */
+    s->gpu = s->vulkan->gpu;
+    s->renderer = pl_renderer_create(s->ctx, s->gpu);
+
+    /* Parse the user shaders, if requested */
+    if (s->shader_bin_len)
+        RET(parse_shader(avctx, s->shader_bin, s->shader_bin_len));
+
+    if (s->shader_path && s->shader_path[0]) {
+        RET(av_file_map(s->shader_path, &buf, &buf_len, 0, s));
+        RET(parse_shader(avctx, buf, buf_len));
+    }
+
+    err = 0;
+    // fall through
+
+fail:
+    if (buf)
+        av_file_unmap(buf, buf_len);
+    return err;
+}
+
+static void uninit(AVFilterContext *avctx)
+{
+    LibplaceboContext *s = avctx->priv;
+
+    for (int i = 0; i < 4; i++) {
+        pl_tex_destroy(s->gpu, &s->image_tex[i]);
+        pl_tex_destroy(s->gpu, &s->target_tex[i]);
+    }
+    for (int i = 0; i < s->num_hooks; i++)
+        pl_mpv_user_shader_destroy(&s->hooks[i]);
+    pl_renderer_destroy(&s->renderer);
+    pl_vulkan_destroy(&s->vulkan);
+    pl_context_destroy(&s->ctx);
+    s->gpu = NULL;
+}
+
+static int query_formats(AVFilterContext *avctx)
+{
+    LibplaceboContext *s = avctx->priv;
+    AVFilterFormats *formats = NULL;
+    const AVPixFmtDescriptor *desc = NULL;
+    int err;
+
+    while ((desc = av_pix_fmt_desc_next(desc))) {
+        enum AVPixelFormat pixfmt = av_pix_fmt_desc_get_id(desc);
+        if (pl_test_pixfmt(s->gpu, pixfmt)) {
+            if ((err = ff_add_format(&formats, pixfmt)) < 0)
+                return err;
+        }
+    }
+
+    return ff_set_common_formats(avctx, formats);
+}
+
+static int find_scaler(AVFilterContext *avctx,
+                       const struct pl_filter_config **opt,
+                       const char *name)
+{
+    const struct pl_named_filter_config *preset;
+    if (!strcmp(name, "help")) {
+        av_log(avctx, AV_LOG_INFO, "Available scaler presets:\n");
+        for (preset = pl_named_filters; preset->name; preset++)
+            av_log(avctx, AV_LOG_INFO, "    %s\n", preset->name);
+        return AVERROR_EXIT;
+    }
+
+    preset = pl_find_named_filter(name);
+    if (!preset) {
+        av_log(avctx, AV_LOG_ERROR, "No such scaler preset '%s'.\n", name);
+        return AVERROR(EINVAL);
+    }
+
+    *opt = preset->filter;
+    return 0;
+}
+
+static int process_frames(AVFilterContext *avctx, AVFrame *out, AVFrame *in)
+{
+    int err = 0;
+    LibplaceboContext *s = avctx->priv;
+    struct pl_render_params params;
+    struct pl_frame image, target;
+
+    /* Update render params */
+    params = (struct pl_render_params) {
+        .lut_entries = s->lut_entries,
+        .antiringing_strength = s->antiringing,
+
+        .deband_params = !s->deband ? NULL : &(struct pl_deband_params) {
+            .iterations = s->deband_iterations,
+            .threshold = s->deband_threshold,
+            .radius = s->deband_radius,
+            .grain = s->deband_grain,
+        },
+
+        .sigmoid_params = s->sigmoid ? &pl_sigmoid_default_params : NULL,
+
+        .color_adjustment = &(struct pl_color_adjustment) {
+            .brightness = s->brightness,
+            .contrast = s->contrast,
+            .saturation = s->saturation,
+            .hue = s->hue,
+            .gamma = s->gamma,
+        },
+
+        .peak_detect_params = !s->peakdetect ? NULL : &(struct pl_peak_detect_params) {
+            .smoothing_period = s->smoothing,
+            .scene_threshold_low = s->scene_low,
+            .scene_threshold_high = s->scene_high,
+            .overshoot_margin = s->overshoot,
+        },
+
+        .color_map_params = &(struct pl_color_map_params) {
+            .intent = s->intent,
+            .tone_mapping_algo = s->tonemapping,
+            .tone_mapping_param = s->tonemapping_param,
+            .desaturation_strength = s->desat_str,
+            .desaturation_exponent = s->desat_exp,
+            .desaturation_base = s->desat_base,
+            .max_boost = s->max_boost,
+            .gamut_warning = s->gamut_warning,
+            .gamut_clipping = s->gamut_clipping,
+        },
+
+        .dither_params = s->dithering < 0 ? NULL : &(struct pl_dither_params) {
+            .method = s->dithering,
+            .lut_size = s->dither_lut_size,
+            .temporal = s->dither_temporal,
+        },
+
+        .cone_params = !s->cones ? NULL : &(struct pl_cone_params) {
+            .cones = s->cones,
+            .strength = s->cone_str,
+        },
+
+        .hooks = s->hooks,
+        .num_hooks = s->num_hooks,
+
+        .skip_anti_aliasing = s->skip_aa,
+        .polar_cutoff = s->polar_cutoff,
+        .disable_linear_scaling = s->disable_linear,
+        .disable_builtin_scalers = s->disable_builtin,
+        .force_3dlut = s->force_3dlut,
+        .force_dither = s->force_dither,
+        .disable_fbos = s->disable_fbos,
+    };
+
+    RET(find_scaler(avctx, &params.upscaler, s->upscaler));
+    RET(find_scaler(avctx, &params.downscaler, s->downscaler));
+
+    /* Prepare and upload input frames */
+
+    pl_upload_avframe(s->gpu, &image, s->image_tex, in);
+
+    if (s->skip_av1_grain)
+        image.av1_grain = (struct pl_av1_grain_data) {0};
+
+    /* Prepare textures for target frame */
+
+    pl_frame_recreate_from_avframe(s->gpu, &target, s->target_tex, out);
+    if (s->target_sar.num) {
+        float aspect = pl_rect2df_aspect(&target.crop) * av_q2d(s->target_sar);
+        pl_rect2df_aspect_set(&target.crop, aspect, s->pad_crop_ratio);
+    }
+
+    /* Perform the actual rendering */
+    if (pl_frame_is_cropped(&target))
+        pl_frame_clear(s->gpu, &target, (float[3]) {0});
+    pl_render_image(s->renderer, &image, &target, &params);
+    pl_download_avframe(s->gpu, &target, out);
+
+    /* Flush the command queues for performance */
+    pl_gpu_flush(s->gpu);
+    return 0;
+
+fail:
+    return err;
+}
+
+static int filter_frame(AVFilterLink *link, AVFrame *in)
+{
+    int err;
+    AVFilterContext *ctx = link->dst;
+    LibplaceboContext *s = ctx->priv;
+    AVFilterLink *outlink = ctx->outputs[0];
+
+    AVFrame *out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
+    if (!out) {
+        err = AVERROR(ENOMEM);
+        goto fail;
+    }
+
+    err = av_frame_copy_props(out, in);
+    if (err < 0)
+        goto fail;
+
+    out->width = outlink->w;
+    out->height = outlink->h;
+
+    if (s->colorspace >= 0)
+        out->colorspace = s->colorspace;
+    if (s->color_range >= 0)
+        out->color_range = s->color_range;
+    if (s->color_trc >= 0)
+        out->color_trc = s->color_trc;
+    if (s->color_primaries >= 0)
+        out->color_primaries = s->color_primaries;
+
+    RET(process_frames(ctx, out, in));
+
+    if (!s->skip_av1_grain)
+        av_frame_remove_side_data(out, AV_FRAME_DATA_FILM_GRAIN_PARAMS);
+
+    av_frame_free(&in);
+
+    return ff_filter_frame(outlink, out);
+
+fail:
+    av_frame_free(&in);
+    av_frame_free(&out);
+    return err;
+}
+
+static int config_props(AVFilterLink *outlink)
+{
+    int err;
+    AVFilterContext *avctx = outlink->src;
+    LibplaceboContext *s   = avctx->priv;
+    AVFilterLink *inlink   = outlink->src->inputs[0];
+    AVRational scale_sar;
+
+    RET(ff_scale_eval_dimensions(s, s->w_expr, s->h_expr, inlink, outlink,
+                                 &outlink->w, &outlink->h));
+
+    ff_scale_adjust_dimensions(inlink, &outlink->w, &outlink->w,
+                               s->force_original_aspect_ratio,
+                               s->force_divisible_by);
+
+    scale_sar = (AVRational){outlink->h * inlink->w, outlink->w * inlink->h};
+    if (inlink->sample_aspect_ratio.num)
+        scale_sar = av_mul_q(scale_sar, inlink->sample_aspect_ratio);
+
+    if (s->normalize_sar) {
+        /* Apply all SAR during scaling, so we don't need to set the out SAR */
+        s->target_sar = scale_sar;
+    } else {
+        /* This is consistent with other scale_* filters, which only
+         * set the outlink SAR to be equal to the scale SAR iff the input SAR
+         * was set to something nonzero */
+        if (inlink->sample_aspect_ratio.num)
+            outlink->sample_aspect_ratio = scale_sar;
+    }
+
+    if (s->out_format_string) {
+        outlink->format = av_get_pix_fmt(s->out_format_string);
+        if (outlink->format == AV_PIX_FMT_NONE) {
+            av_log(avctx, AV_LOG_ERROR, "Invalid output format.\n");
+            return AVERROR(EINVAL);
+        }
+    }
+
+    return 0;
+
+fail:
+    return err;
+}
+
+#define OFFSET(x) offsetof(LibplaceboContext, x)
+#define STATIC (AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM)
+#define DYNAMIC (STATIC | AV_OPT_FLAG_RUNTIME_PARAM)
+
+static const AVOption libplacebo_options[] = {
+    { "w", "Output video width",  OFFSET(w_expr), AV_OPT_TYPE_STRING, {.str = "iw"}, .flags = STATIC },
+    { "h", "Output video height", OFFSET(h_expr), AV_OPT_TYPE_STRING, {.str = "ih"}, .flags = STATIC },
+    { "format", "Output video format", OFFSET(out_format_string), AV_OPT_TYPE_STRING, .flags = STATIC },
+    { "force_original_aspect_ratio", "decrease or increase w/h if necessary to keep the original AR", OFFSET(force_original_aspect_ratio), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 2, STATIC, "force_oar" },
+        { "disable",  NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 0 }, 0, 0, STATIC, "force_oar" },
+        { "decrease", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 1 }, 0, 0, STATIC, "force_oar" },
+        { "increase", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 2 }, 0, 0, STATIC, "force_oar" },
+    { "force_divisible_by", "enforce that the output resolution is divisible by a defined integer when force_original_aspect_ratio is used", OFFSET(force_divisible_by), AV_OPT_TYPE_INT, { .i64 = 1 }, 1, 256, STATIC },
+    { "normalize_sar", "force SAR normalization to 1:1", OFFSET(normalize_sar), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, STATIC },
+    { "pad_crop_ratio", "ratio between padding and cropping when normalizing SAR (0=pad, 1=crop)", OFFSET(pad_crop_ratio), AV_OPT_TYPE_FLOAT, {.dbl=0.0}, 0.0, 1.0, DYNAMIC },
+
+    {"colorspace", "select colorspace", OFFSET(colorspace), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_SPC_NB-1, DYNAMIC, "colorspace"},
+    {"auto", "keep the same colorspace",  0, AV_OPT_TYPE_CONST, {.i64=-1},                          INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"gbr",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_RGB},               INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"bt709",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT709},             INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"unknown",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_UNSPECIFIED},       INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"bt470bg",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT470BG},           INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"smpte170m",                  NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_SMPTE170M},         INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"smpte240m",                  NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_SMPTE240M},         INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"ycgco",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_YCGCO},             INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"bt2020nc",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT2020_NCL},        INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"bt2020c",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT2020_CL},         INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"ictcp",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_ICTCP},             INT_MIN, INT_MAX, STATIC, "colorspace"},
+
+    {"range", "select color range", OFFSET(color_range), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_RANGE_NB-1, DYNAMIC, "range"},
+    {"auto",  "keep the same color range",   0, AV_OPT_TYPE_CONST, {.i64=-1},                       0, 0, STATIC, "range"},
+    {"unspecified",                  NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_UNSPECIFIED},  0, 0, STATIC, "range"},
+    {"unknown",                      NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_UNSPECIFIED},  0, 0, STATIC, "range"},
+    {"limited",                      NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, "range"},
+    {"tv",                           NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, "range"},
+    {"mpeg",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, "range"},
+    {"full",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, "range"},
+    {"pc",                           NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, "range"},
+    {"jpeg",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, "range"},
+
+    {"color_primaries", "select color primaries", OFFSET(color_primaries), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_PRI_NB-1, DYNAMIC, "color_primaries"},
+    {"auto", "keep the same color primaries",  0, AV_OPT_TYPE_CONST, {.i64=-1},                     INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"bt709",                           NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT709},        INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"unknown",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_UNSPECIFIED},  INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"bt470m",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT470M},       INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"bt470bg",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT470BG},      INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte170m",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE170M},    INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte240m",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE240M},    INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"film",                            NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_FILM},         INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"bt2020",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT2020},       INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte428",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE428},     INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte431",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE431},     INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte432",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE432},     INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"jedec-p22",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_JEDEC_P22},    INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"ebu3213",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_EBU3213},      INT_MIN, INT_MAX, STATIC, "color_primaries"},
+
+    {"color_trc", "select color transfer", OFFSET(color_trc), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_TRC_NB-1, DYNAMIC, "color_trc"},
+    {"auto", "keep the same color transfer",  0, AV_OPT_TYPE_CONST, {.i64=-1},                     INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt709",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT709},        INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"unknown",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_UNSPECIFIED},  INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt470m",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_GAMMA22},      INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt470bg",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_GAMMA28},      INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"smpte170m",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE170M},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"smpte240m",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE240M},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"linear",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_LINEAR},       INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"iec61966-2-4",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_IEC61966_2_4}, INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt1361e",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT1361_ECG},   INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"iec61966-2-1",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_IEC61966_2_1}, INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt2020-10",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT2020_10},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt2020-12",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT2020_12},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"smpte2084",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE2084},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"arib-std-b67",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_ARIB_STD_B67}, INT_MIN, INT_MAX, STATIC, "color_trc"},
+
+    { "upscaler", "Upscaler function", OFFSET(upscaler), AV_OPT_TYPE_STRING, {.str = "spline36"}, .flags = DYNAMIC },
+    { "downscaler", "Downscaler function", OFFSET(downscaler), AV_OPT_TYPE_STRING, {.str = "mitchell"}, .flags = DYNAMIC },
+    { "lut_entries", "Number of scaler LUT entries", OFFSET(lut_entries), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 256, DYNAMIC },
+    { "antiringing", "Antiringing strength (for non-EWA filters)", OFFSET(antiringing), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 1.0, DYNAMIC },
+    { "sigmoid", "Enable sigmoid upscaling", OFFSET(sigmoid), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, DYNAMIC },
+    { "skip_av1_grain", "Disable AV1 grain application", OFFSET(skip_av1_grain), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+
+    { "deband", "Enable debanding", OFFSET(deband), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "deband_iterations", "Deband iterations", OFFSET(deband_iterations), AV_OPT_TYPE_INT, {.i64 = 1}, 0, 16, DYNAMIC },
+    { "deband_threshold", "Deband threshold", OFFSET(deband_threshold), AV_OPT_TYPE_FLOAT, {.dbl = 4.0}, 0.0, 1024.0, DYNAMIC },
+    { "deband_radius", "Deband radius", OFFSET(deband_radius), AV_OPT_TYPE_FLOAT, {.dbl = 16.0}, 0.0, 1024.0, DYNAMIC },
+    { "deband_grain", "Deband grain", OFFSET(deband_grain), AV_OPT_TYPE_FLOAT, {.dbl = 6.0}, 0.0, 1024.0, DYNAMIC },
+
+    { "brightness", "Brightness boost", OFFSET(brightness), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, -1.0, 1.0, DYNAMIC },
+    { "contrast", "Contrast gain", OFFSET(contrast), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 0.0, 16.0, DYNAMIC },
+    { "saturation", "Saturation gain", OFFSET(saturation), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 0.0, 16.0, DYNAMIC },
+    { "hue", "Hue shift", OFFSET(hue), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, -M_PI, M_PI, DYNAMIC },
+    { "gamma", "Gamma adjustment", OFFSET(gamma), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 0.0, 16.0, DYNAMIC },
+
+    { "peak_detect", "Enable dynamic peak detection for HDR tone-mapping", OFFSET(peakdetect), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, DYNAMIC },
+    { "smoothing_period", "Peak detection smoothing period", OFFSET(smoothing), AV_OPT_TYPE_FLOAT, {.dbl = 100.0}, 0.0, 1000.0, DYNAMIC },
+    { "scene_threshold_low", "Scene change low threshold", OFFSET(scene_low), AV_OPT_TYPE_FLOAT, {.dbl = 5.5}, -1.0, 100.0, DYNAMIC },
+    { "scene_threshold_high", "Scene change high threshold", OFFSET(scene_high), AV_OPT_TYPE_FLOAT, {.dbl = 10.0}, -1.0, 100.0, DYNAMIC },
+    { "overshoot", "Tone-mapping overshoot margin", OFFSET(overshoot), AV_OPT_TYPE_FLOAT, {.dbl = 0.05}, 0.0, 1.0, DYNAMIC },
+
+    { "intent", "Rendering intent", OFFSET(intent), AV_OPT_TYPE_INT, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 3, DYNAMIC, "intent" },
+        { "perceptual", "Perceptual", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_PERCEPTUAL}, 0, 0, STATIC, "intent" },
+        { "relative", "Relative colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
+        { "absolute", "Absolute colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_ABSOLUTE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
+        { "saturation", "Saturation mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_SATURATION}, 0, 0, STATIC, "intent" },
+    { "tonemapping", "Tone-mapping algorithm", OFFSET(tonemapping), AV_OPT_TYPE_INT, {.i64 = PL_TONE_MAPPING_BT_2390}, 0, PL_TONE_MAPPING_ALGORITHM_COUNT - 1, DYNAMIC, "tonemap" },
+        { "clip", "Hard-clipping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_CLIP}, 0, 0, STATIC, "tonemap" },
+        { "mobius", "Mobius tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_MOBIUS}, 0, 0, STATIC, "tonemap" },
+        { "reinhard", "Reinhard tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_REINHARD}, 0, 0, STATIC, "tonemap" },
+        { "hable", "Hable/Filmic tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_HABLE}, 0, 0, STATIC, "tonemap" },
+        { "gamma", "Gamma tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_GAMMA}, 0, 0, STATIC, "tonemap" },
+        { "linear", "Linear tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_LINEAR}, 0, 0, STATIC, "tonemap" },
+        { "bt.2390", "ITU-R BT.2390 tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_BT_2390}, 0, 0, STATIC, "tonemap" },
+    { "tonemapping_param", "Tunable parameter for some tone-mapping functions", OFFSET(tonemapping_param), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, .flags = DYNAMIC },
+    { "desaturation_strength", "Desaturation strength", OFFSET(desat_str), AV_OPT_TYPE_FLOAT, {.dbl = 0.75}, 0.0, 1.0, DYNAMIC },
+    { "desaturation_exponent", "Desaturation exponent", OFFSET(desat_exp), AV_OPT_TYPE_FLOAT, {.dbl = 1.5}, 0.0, 10.0, DYNAMIC },
+    { "desaturation_base", "Desaturation base", OFFSET(desat_base), AV_OPT_TYPE_FLOAT, {.dbl = 0.18}, 0.0, 10.0, DYNAMIC },
+    { "max_boost", "Tone-mapping maximum boost", OFFSET(max_boost), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 1.0, 10.0, DYNAMIC },
+    { "gamut_warning", "Highlight out-of-gamut colors", OFFSET(gamut_warning), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "gamut_clipping", "Enable colorimetric gamut clipping", OFFSET(gamut_clipping), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, DYNAMIC },
+
+    { "dithering", "Dither method to use", OFFSET(dithering), AV_OPT_TYPE_INT, {.i64 = PL_DITHER_BLUE_NOISE}, -1, PL_DITHER_METHOD_COUNT - 1, DYNAMIC, "dither" },
+        { "none", "Disable dithering", 0, AV_OPT_TYPE_CONST, {.i64 = -1}, 0, 0, STATIC, "dither" },
+        { "blue", "Blue noise", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_BLUE_NOISE}, 0, 0, STATIC, "dither" },
+        { "ordered", "Ordered LUT", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_ORDERED_LUT}, 0, 0, STATIC, "dither" },
+        { "ordered_fixed", "Fixed function ordered", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_ORDERED_FIXED}, 0, 0, STATIC, "dither" },
+        { "white", "White noise", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_WHITE_NOISE}, 0, 0, STATIC, "dither" },
+    { "dither_lut_size", "Dithering LUT size", OFFSET(dither_lut_size), AV_OPT_TYPE_INT, {.i64 = 6}, 1, 8, STATIC },
+    { "dither_temporal", "Enable temporal dithering", OFFSET(dither_temporal), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+
+    { "cones", "Colorblindness adaptation model", OFFSET(cones), AV_OPT_TYPE_FLAGS, {.i64 = 0}, 0, PL_CONE_LMS, DYNAMIC, "cone" },
+        { "l", "L cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_L}, 0, 0, STATIC, "cone" },
+        { "m", "M cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_M}, 0, 0, STATIC, "cone" },
+        { "s", "S cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_S}, 0, 0, STATIC, "cone" },
+    { "cone-strength", "Colorblindness adaptation strength", OFFSET(cone_str), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 10.0, DYNAMIC },
+
+    { "custom_shader_path", "Path to custom user shader (mpv .hook format)", OFFSET(shader_path), AV_OPT_TYPE_STRING, .flags = STATIC },
+    { "custom_shader_bin", "Custom user shader as binary (mpv .hook format)", OFFSET(shader_bin), AV_OPT_TYPE_BINARY, .flags = STATIC },
+
+    /* Performance/quality tradeoff options */
+    { "skip_aa", "Skip anti-aliasing", OFFSET(skip_aa), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 0, DYNAMIC },
+    { "polar_cutoff", "Polar LUT cutoff", OFFSET(polar_cutoff), AV_OPT_TYPE_FLOAT, {.i64 = 0}, 0.0, 1.0, DYNAMIC },
+    { "disable_linear", "Disable linear scaling", OFFSET(disable_linear), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "disable_builtin", "Disable built-in scalers", OFFSET(disable_builtin), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "force_3dlut", "Force the use of a full 3DLUT", OFFSET(force_3dlut), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "force_dither", "Force dithering", OFFSET(force_dither), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "disable_fbos", "Force-disable FBOs", OFFSET(disable_fbos), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { NULL },
+};
+
+AVFILTER_DEFINE_CLASS(libplacebo);
+
+static const AVFilterPad libplacebo_inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .filter_frame = &filter_frame,
+    },
+    { NULL }
+};
+
+static const AVFilterPad libplacebo_outputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = &config_props,
+    },
+    { NULL }
+};
+
+AVFilter ff_vf_libplacebo = {
+    .name           = "libplacebo",
+    .description    = NULL_IF_CONFIG_SMALL("Apply various GPU filters from libplacebo"),
+    .priv_size      = sizeof(LibplaceboContext),
+    .priv_class     = &libplacebo_class,
+    .inputs         = libplacebo_inputs,
+    .outputs        = libplacebo_outputs,
+    .init           = &init,
+    .uninit         = &uninit,
+    .query_formats  = &query_formats,
+    .process_command = &ff_filter_process_command,
+};