GSoC'26 VideoLAN, Final Report and Wrap-up

GSoC 2026 with VideoLAN Final Report: zero-copy GPU video in VLC

By Ahmed Sobhy12 min read51 views

Teaching VLC to stop copying every frame

Playing a video comes down to two main jobs. First, the decoder unpacks the highly compressed video file into raw picture frames. Then, the renderer takes those raw frames, scales them to fit your window, adjusts the colors, and actually draws them on your screen.

Your graphics card can decode video on its own. There's a dedicated block on the chip for it, separate from the part that draws things, and VLC uses it wherever it can: NVDEC on NVIDIA, VA-API on Intel and AMD, MediaCodec on Android. When the decoder finishes, the frame is already sitting in graphics memory, a few millimeters from the hardware that's about to draw it.

VLC copied it off the card anyway. Down into system memory, back up onto the card, and only then did the renderer draw it. Nothing touched the pixels in between. They took that round trip because the renderer at the other end couldn't read them where they already were, and they took it for every frame of every video you played.

Eliminating that round trip, getting the decoder and the renderer to share the same memory directly, a process known as interop (interoperability), was my Google Summer of Code project with VideoLAN, mentored by Thomas Guillem. It works now, and the code is linked at the end.

The round trip through system memory, before and after before: a hardware decoded frame decoded frame (GPU) system memory GPU texture screen download upload after decoded frame (GPU) GPU texture screen the frame stays where the decoder left it
The whole project in one picture

Who I am and why this project

I'm Ahmed Sobhy, a computer engineering student at Cairo University. I like low-level systems code in C, which is a polite way of saying I enjoy reading specifications and staring at memory layouts.

Like a lot of people, VLC has been on my screen for almost as long as I've had a computer. It's that familiar orange cone I used as a kid whenever I needed to play some random video file that nothing else could open. So when I was looking through GSoC organizations this year, VideoLAN was at the top of my list.

The funny thing is, the project I ended up spending my summer on isn't actually the one I applied for. I originally wrote a proposal to add HDR tonemapping to an experimental Vulkan output. I spent weeks before the deadline practically living in that branch, sending small patches just to learn my way around the codebase.

But right after I submitted, the team made an architectural shift: Vulkan rendering was going to go through a different route (libplacebo), meaning my original tonemapping project didn't really fit anymore. Instead, the project refocused on a lower-level, missing piece for that new route: hardware decoder interop.

I was actually thrilled. Interop was the exact part of the codebase I'd been reading up on, and it sat perfectly at the intersection of my weird love for hardware APIs and memory management. If you want the full backstory on that pivot and how I prepped for the summer, I wrote all about it in the first post of this series.

What was missing

VLC doesn't draw the picture itself. It hands the finished frame to a renderer, and the renderer scales it, fixes the colors, and gets it on screen.

The renderer VLC has shipped for years is built on OpenGL, and it solved this problem a long time ago. It has a set of small modules, one per decoder, that take the frame where it lies and show it to OpenGL without moving it anywhere. The newer renderer is built on libplacebo, the GPU rendering library that came out of mpv, and it draws through Vulkan. It had none of those modules. It knew about exactly one kind of picture, pixels in system memory, and everything else took the long way round. That's what I spent the summer writing.

What I built

The project had four parts, and they didn't all end the same way:

  • NVDEC interop (the NVIDIA decoder): built
  • VA-API interop (the Intel and AMD one): built
  • Software pictures: built
  • MediaCodec on Android interop: not built

So, three modules, one for each place a frame can come from, and the layer they plug into. The layer is a small contract: a module says whether it can handle the picture it's offered, agrees on a format, and then hands over a frame whenever one arrives. Where that frame came from is the module's problem, and the renderer never has to ask.

Architecture diagram showing how the interop layer connects the NVDEC, VA-API, and software modules to the libplacebo renderer through a single contract
How the three modules plug into one contract
modulethe frame arrives ashow it reaches the renderer
NVDEC memory owned by NVIDIA's decoder handed over without leaving the card
VA-API a handle owned by the graphics driver pointed at where it lies, no copy
software pixels in system memory uploaded, the way it always was

The software module is the odd one out, since software pictures already worked. It's there for another GSoC project on VLC's video output this summer, Abdelrahman Gamal's, which adds filters that run on the card, deinterlacing among them, to the same display code. A filter shouldn't have to know whether its frame came off the card or out of the CPU, so his code only has to learn one way in. Probably my favorite thing that happened all summer.

Why do we need separate hardware modules at all? Because GPU manufacturers never agreed on a single, universal way to handle hardware decoding. They built their own APIs, and they exist for different reasons:

  • NVDEC is NVIDIA's proprietary API. Because NVIDIA controls both the hardware decoder and the graphics driver, getting them to talk is relatively smooth. NVDEC hands you CUDA memory, and CUDA and Vulkan have a private arrangement for passing memory between them: no file descriptors, no negotiating, nobody else involved.
  • VA-API is an open standard, primarily used by Intel and AMD on Linux. It exists to be compatible across different hardware, but that wide compatibility makes it much messier to work with. VA-API hands you an opaque surface, and to get anything out of it you have to ask the driver to export a "dma-buf", a file descriptor the kernel gives to whoever needs to see that memory. Then you and Vulkan have to agree on how the bytes are actually arranged in there.

Then there's timing. The decoder and the renderer are two pieces of hardware writing and reading the same picture, and nothing was making them take turns. They now share a counter that only goes up: the copy waits for a number, does its work, raises the number, and the renderer waits for that number before it samples anything. It's the part I was least sure I could pull off, and it's the part I'm happiest with.

What was hard

Not the parts I expected. Getting the decoders and Vulkan to hand memory over was mostly careful reading.

Get stuck on a web framework and a hundred Stack Overflow answers are waiting for you. Get stuck importing a decoder's surface into Vulkan and you have the specification, two or three other implementations to read, and whoever wrote the library. My documentation for most of the summer was VLC's own OpenGL interop, my mentor's experimental branch, and the chapters of the Vulkan spec about external memory. That's slower, and I ended up liking it more than I expected: you read primary sources because there's nothing else to read.

Then there's the fact that the screen lies to you. Video can play perfectly from code that breaks the rules, because a driver is allowed to accept things the specification forbids, and NVIDIA's is relaxed about it. The only ground truth is Vulkan's validation layers, a mode that checks every call you make against the spec and complains in paragraphs.

And the layers only tell you about the machine you're sitting at. Vendors disagree about how many buffers come back from an export, about which formats can be shared at all, and about how strictly any of this gets enforced. Code that's fine on my NVIDIA card can be illegal on the AMD one two slots down, and the reverse happens too.

The code, and where it stands

None of the interop work is merged yet. Everything went up as a merge request on my own fork first, and Thomas reviewed it there. Only once he was happy with the shape of it did it go onto VLC's own repository, where the interop merge request now waits for review from the wider team.

  • Upstream, open: MR !9856, the interop layer itself plus the NVDEC and software modules
  • Upstream, merged: MR !9328, the missing Wayland Vulkan platform in VLC's meson build. Without it the Vulkan renderer doesn't start on Wayland, so everything above depends on it
  • My fork: MR !2, where it was written and where Thomas's first review happened

To run it, build the branch below (or VLC's master, once the merge request lands) and ask for the libplacebo renderer:

# NVDEC
vlc --vout=libplacebo --pl-gpu=placebo_vk --dec-dev=nvdec video.mkv

# VA-API (also _x11 and _drm)
vlc --vout=libplacebo --pl-gpu=placebo_vk --dec-dev=decdev_vaapi_wl video.mkv

# no hardware decoder at all
vlc --vout=libplacebo --pl-gpu=placebo_vk --dec-dev=none video.mkv

--dec-dev picks which hardware decoder device VLC uses, and both the decoder and the interop module follow from that choice. Leave it off and VLC chooses for you.

With -vv the log tells you which module it picked, or that it gave up and fell back to uploading through system memory. --vk-debug turns on the validation layers.

What's left

  • Review: both merge requests still have to get through it.
  • MediaCodec interop: needs a libplacebo change before any VLC code makes sense, and I don't have a Vulkan-capable Android device to test on. It was in the accepted proposal, and cutting it was a decision I took with Thomas rather than on my own.
  • The external renderer, where another application hands VLC its own Vulkan device and gets video drawn into it: also cut for scope, again in agreement with Thomas. It needs a libvlc API that doesn't exist for the other outputs either, so it's a project of its own rather than a loose end.

What the summer taught me

I wrote this up week by week while it was happening, wrong turns included, in a series of posts with a lot more detail than this one. That series ended up being the most useful thing I did for myself, not for anyone reading it. Writing a week down forces you to find out whether you actually understood it, and more than once I sat down to explain something and found out halfway through the paragraph that I couldn't. Some weeks the honest post was that a thing had failed and here's why, and those were the ones worth writing.

VLC is old and enormous, and finding my way around it was a skill of its own: which of the video output files actually matter, how modules find each other at runtime, what the git history says about why something is the shape it is. It also runs on a few billion devices, and that changes how you write code.

Mostly, though, I understand graphics hardware far better than I did before. Before this, I could have told you a GPU draws triangles quickly. Now I know what its memory is actually made of, why an image isn't laid out the way you'd assume, what a driver is allowed to do behind your back, and how two engines on the same chip agree about who's allowed to touch a buffer.

Thanks

Thank you to my mentor, Thomas Guillem. I was lucky to get him on this one. He gave me a lot of his time over the summer, and he was responsive to my questions. Most of what I now understand about how this kind of code should be written, I understand because he explained the reasoning instead of just telling me what to change. The project is in far better shape for it, and so am I.

And to VideoLAN, for being VideoLAN. Keeping something this widely used free, open source and genuinely pleasant to use isn't the easy path, and they've held to it for over twenty years. VLC has been my default player for as long as I've had a computer of my own, so going from someone who only used it to someone who works on it is easily the thing I'm happiest about this year.

It isn't finished, and I don't want the end of GSoC to be the end of it. Both merge requests still have to get through review. The two pieces that didn't fit this summer are the ones I most want to come back to: they were cut for scope, not because they stopped being interesting.

If you've been here since the first post, thank you for staying. And if this is the first one you've found, the whole summer is written up in the series, wrong turns and all. Go read it. Either way, I'm glad I got to leave something in VLC that millions of people will be able to use.

Did you enjoy this article?

Share this article

Comments(2)

Leave a comment

H
HabibaAug 22, 2026

Such an inspiring journey! Super nice that you documented it too. Good luck ๐Ÿ‘๐Ÿป๐ŸŽ‰

L
LoayAug 22, 2026

What a journey! Thank you so much for sharing it with us! Your posts were always really interesting and genuinely got me excited about so many topics.๐Ÿ‘โค๏ธ The project itself looks really interesting and meaningful too. You should be really proud of what youโ€™ve accomplished.โค๏ธ Keep going and I hope the best for you๐Ÿ™

ยฉ 2026 Ahmed Sobhy. All rights reserved.