---
title: "GSoC 2026: The Parts That Don't Go in the Report"
description: "Three widgets merged into Kdenlive, and everything around them. Being wrong twice about one bug, building a widget twice to avoid asking a question, and the weeks I went quiet because internship applications were eating my head."
date: 2026-08-20
tags:
  - gsoc
  - open-source
  - kde
  - kdenlive
  - life
---

> the actual technical wrap-up is [here](https://yashbavadiya.bearblog.dev/gsoc-2026-wrap-up), with all three MRs and what the widgets do. this post is the other half. the stuff that doesn't really fit in a final report but is honestly most of what the summer was.

Three widgets. <span class="good">All merged.</span> Shipping in 26.08.

That's the one-line version. The longer version has me being confidently wrong about the same bug twice in a row, in public, in front of my mentor.

If you want the part before this, how I got selected at all, that's [over here](/blogs/gsoc-selection). This is what happened after.

| | |
|---|---|
| **Project** | Improving Effect Widgets for Kdenlive |
| **Org** | KDE Community |
| **Mentor** | Jean-Baptiste Mardelle |
| **Reviewers** | Julius Künzel, Bernd Jordan |
| **Shipped** | <span class="good">Curves !887</span>, <span class="good">Gradient Map !911</span>, <span class="good">Speed Ramp !928</span> |
| **Still open** | <span class="warn">requestRemapResize fix</span>, <span class="warn">#2231 mode split</span> |

---

## Community bonding was the actually useful part

I had <span class="key">12 MRs merged</span> before the coding period even started. That sounds like a lot but <span class="key">the real value wasn't the count</span>, it was finding out how these people read code while nothing was riding on it yet.

Like on [!827](https://invent.kde.org/multimedia/kdenlive/-/merge_requests/827), which was a duplicate profiles bug in the New Project dialog. I built a spec string with percent args, normal thing to do. JB told me a `QStringList` with a `join()` is about <span class="key">10 to 15 percent faster</span>.

This is a helper lambda in a dedup path that runs when you open a dialog. <span class="key">He still benchmarked it.</span>

That MR also had <span class="warn">two root causes stacked on top of each other</span>, which I found out by fixing one and watching the bug keep happening. MLT ships duplicate profile files, and `ProfileRepository::refresh()` was getting called twice. Fixing either one on its own does nothing visible, so you sit there wondering what you broke. JB also caught that I'd left `colorspace()` out of the spec key, which would've quietly merged two profiles that only differ by colorspace. I'd tested it and it worked. <mark>It was still wrong in a case I hadn't thought to check</mark>, which honestly happened a lot this summer.

The other one was [!837](https://invent.kde.org/multimedia/kdenlive/-/merge_requests/837), Identify Gaps. Before writing the detection logic I stopped and asked what should have been an obvious question, does a gap mean any track is empty or all tracks are empty.

Bernd said <span class="good">all tracks</span>, Eugen agreed, and that turned the whole thing into a <span class="key">union of intervals</span> across tracks instead of the per-track loop I was about to write. Because a gap on track 2 isn't really a gap if track 3 has a clip sitting underneath it.

<mark>Asking cost me a day. Getting it wrong would've cost a rewrite.</mark> I did not remember this lesson consistently, as will become clear.

<blockquote class="pull">
Reading code tells you what it looks like it does. Running it tells you what it does.
</blockquote>

Bernd filed that bug, [#352409](https://bugs.kde.org/show_bug.cgi?id=352409), back in 2015, and called the finished feature a great new function. <span class="muted">Eleven years between the report and the fix.</span> That one felt good.

---

## Curves: my code worked and was still wrong

[MR !887](https://invent.kde.org/multimedia/kdenlive/-/merge_requests/887), branch `work/curves-per-channel-tabs`, 17 files touched. Per-channel tabs so you get All/R/G/B in one effect instead of stacking the Curves effect three times.

Bit of context, my proposal originally said to do this with `frei0r.curves`. JB's feedback back in April was to use `avfilter.curves` instead since it handles multiple channels in one filter. That redirect is basically why the widget works at all.

So my first version put all four channels into one compound XML parameter. <span class="good">It worked fine.</span> I pushed it feeling pretty good about it.

Next morning JB pointed out that combining them like that makes the widget hard to reuse for other curves later, and he'd rather each channel was its own parameter. Then he wrote out the XML he wanted and pointed at `m_mainKeyframeWidget` in `AssetParameterView` as the pattern to copy, and asked what I thought.

He'd pretty clearly already decided. <mark>It was still framed as a question</mark>, which I've thought about since.

So `serializeAllChannels()` went away entirely, each channel got its own `QPersistentModelIndex` and fires `valueChanged` on its own. First `av_curve` param builds the `CurveParamWidget`, and every one after that just calls `addAvCurveTab()` on the widget that already exists. It's better in every way and I wouldn't have gotten there myself, because my version worked, and <span class="key">working is usually where you stop looking</span>.

Then there was <span class="bad">the segfault</span>.

If you put two curve points too close together on the x-axis, <span class="key">under about 0.003 apart</span>, libavfilter just dies. It logs something about key points being too close or not strictly increasing and then takes the whole thing down, and sometimes the filter stays broken after.

My fix was to catch that state and not send it to MLT at all. <span class="warn">Problem solved, sort of.</span>

Bernd's response was, why not snap the points instead. Because letting someone place a point and then silently not applying it is confusing.

Which, yeah. Obviously. <mark>From the user's side my version looks like the app randomly deciding your click doesn't count.</mark> Snapping means you always get something, just moved over by about 0.00266. I'd been so focused on not crashing that I forgot someone would actually be dragging that point around.

He also said to label the tabs All/R/G/B instead of Master/R/G/B so it matches other effects that deal with channels. <span class="muted">Small thing, but correct.</span>

JB came back later with more, color code each channel so you can tell what you're editing, add a reset button, show the input/output values of the selected point like GIMP does, and draw the other channels faintly behind the active one. All four went in. The ghost curves are my favourite part of the widget and they exist because he screenshotted GIMP into the thread.

<span class="good">Merged June 26th, 48 assertions passing.</span>

---

## Gradient map: I built it twice so I wouldn't have to ask

[MR !911](https://invent.kde.org/multimedia/kdenlive/-/merge_requests/911). Most work, least visible result, and I still think it went the right way in the end.

Starting point was that `gradientmap` supports <span class="key">32 stops internally</span> and Kdenlive was showing <span class="bad">exactly two</span> of them. So the feature was already sitting in MLT with no UI attached to it.

Julius suggested building the stop editor by borrowing from [Qt-Color-Widgets](https://github.com/mattbas/Qt-Color-Widgets) rather than doing everything from scratch, with the idea that it might get upstreamed to KDE Frameworks at some point.

Now the sensible move here is to send one message asking whether borrow means look at it or actually link against it.

<span class="bad">I didn't do that. I built both versions instead.</span>

One fully custom widget from scratch, and one wrapper around the vendored `color_widgets::GradientEditor` that MLT's glaxnimate module already ships, CMake plumbing and everything. Then I showed JB both and asked which one he wanted.

<span class="bad">He said we're definitely not taking on another library dependency for this</span>, which is fair and about what I expected.

But then he checked with Julius and came back with <span class="good">a third option that wasn't either of my branches</span>. Copy the parts of QtColorWidgets we actually need into Kdenlive, keep their UI and UX ideas, own the code, and then it can go upstream to Frameworks later as its own library. Not use it, not write your own from nothing. <span class="key">Lift and adapt.</span>

The slightly annoying part is that building both is the only reason I could answer his follow-up properly. He asked what my scratch version was missing compared to the vendored one and I could tell him exactly, alpha checkerboard preview, native frame styling, and the built-in colour dialog, because I had them side by side. If I'd only built one I'd have been guessing.

So it worked out, and the branch is still called `work/gradient-qtcolorwidgets` which is a nice little leftover of a direction that didn't happen. But the actual lesson isn't don't over-build. It's that I sat on an ambiguity for weeks because asking felt like admitting I couldn't figure it out myself. <mark>Weeks, to avoid sending one message.</mark>

### The bug that only existed on other people's machines

JB during review said the gradient looked broken and posted a screenshot, and guessed I might need to draw the rect after the primitive.

For him the gradient bar rendered as <span class="bad">a completely flat empty rectangle</span>. <span class="good">On my machine it looked fine.</span>

It was <span class="key">paint order</span>. I was drawing the native QStyle frame last, and Breeze's frame primitive fills its own interior, so it was painting over the gradient I'd already drawn. Draw the frame first, then fill inside its content rect. The commit is `gradientmap: draw gradient inside native frame instead of under it`, which is one of those commit messages that is the entire bug.

<mark>His guess in the parentheses was right before he'd even opened the file.</mark>

Same review he asked for a 32 stop cap, so I put that at the model layer in `assetparametermodel.cpp` rather than only in the widget, so a hand-edited or broken project file can't get around it. He also wanted the stop handles outlined in the palette text colour since the black stop was almost invisible on a dark theme, plus an rgba tooltip on hover. Both went in.

<div class="callout warn">
Then near the end of review he noticed the `gradientmap` filter doesn't support alpha at all. That one's not mine, it's MLT. My editor lets you set alpha and MLT ignores it. We merged anyway so it'd make 26.08 and wrote it down as something to fix or disable later.
</div>

Also GitLab shows !911 as closed rather than merged, because GitLab wouldn't merge it and JB did it manually as `b0b678c6`. So if you go count my merged MRs and get two instead of three, that's why.

---

## Speed ramp: the feature I proposed doesn't exist

[MR !928](https://invent.kde.org/multimedia/kdenlive/-/merge_requests/928), branch `work/speed-ramp-curve`.

My accepted proposal says, in writing, that I'd add bezier curve handles to the RemapView connector lines and reuse `BezierSplineEditor`.

<span class="bad">MLT does not have a bezier keyframe type.</span> It has preset easing types and that's the whole list. <span class="bad">The main feature of my own proposal could not be built the way I'd described it.</span> So the main feature of my own proposal, the thing I got picked for, couldn't be built the way I'd described it.

I found this <span class="good">before writing any code</span>, which is probably the best decision I made all summer, and mostly happened because the Identify Gaps thing had taught me to go look first.

So I laid out both options for JB. Presets only, or free bezier handles, where the second one means either baking the curve down into a pile of linear keyframes on save or adding a new keyframe type to MLT itself, plus refactoring a `QMap<int,int>` that's used inline in something like 40 places and is baked into the undo signals.

He told me bezier in MLT had been looked at a few years ago and turned out to be hard to fit into MLT's animation feature, and that his idea had always been to use keyframes like volume and brightness already do, where the user picks a type from MLT's existing list.

So I'd walked into <span class="warn">a dead end someone else already found years earlier</span>. Which sounds discouraging but was actually reassuring, being wrong in the same direction as someone with way more context means you're at least reading it right.

New direction was reusing Kdenlive's existing keyframe types, which the effect stack already understands as `KeyframeType::KeyframeEnum`. <span class="muted">Less exciting than what I'd promised, but shippable.</span> I opened [#2188](https://invent.kde.org/multimedia/kdenlive/-/work_items/2188) for it so the discussion happened in public instead of in DMs, which JB had asked for early on and was good advice.

### Actually testing it instead of assuming

This next bit is my favourite thing I did all summer.

I wrote a standalone C program against real MLT to check whether non-linear keyframe types are actually honoured during playback, versus just parsing as valid syntax. Those are very different questions and the XML looks identical either way. So I sampled the interpolated values and checked them against the real easing formulas.

Good thing too, because there's a trap in there. <mark>The type suffix has to be on the keyframe that starts a segment, not the one that ends it.</mark> Put it on the wrong end and MLT silently treats the segment as linear. <span class="bad">No error, no warning, XML looks completely fine, playback is just wrong.</span>

<span class="good">That would've cost me days if I'd hit it later. Instead it cost an afternoon writing a test program.</span> Turns out the timeline QML already does it that way, so at least the convention was consistent once I knew what to look for.

I also found Kdenlive already had `KeyframeCurveEditor`, a QWidget that draws real keyframe curves by sampling interpolated MLT values per pixel. JB had forgotten it existed, it was written by a student a long time ago and nobody had looked at it since.

### Two reviewers disagreeing: and me not jumping in

After keyframe types landed, Julius tested it and raised something real. The UI was starting to mix up time remapping and speed keyframing, which are two different ways of thinking about roughly the same thing, and blending them makes it more confusing. He also pointed out that what the old UI treats as linear is, from a speed point of view, actually discrete.

Bernd went further and said the whole time remapping widget needs reworking, that it's confusing and not up to par with other widgets, and then mocked up a speed based graph with frames on X and speed on Y.

Julius pushed back that Bernd's version shows speed and not remapping, which is exactly the mixing he'd flagged in the first place.

Then Bernd suggested maybe we need both, a proper time remapping mode for people who need specific frames at specific positions, and a separate speed ramping mode for smooth fast and slow transitions.

<div class="callout warn">
So at this point two people who know this codebase far better than I do are disagreeing about UI direction, in my merge request, about my feature.
</div>

The instinct is to grab one of the suggestions and start implementing so you look responsive. I read through it a few times instead and then said what I actually thought, that this felt like a product decision above what I could settle inside this MR, and asked whether we could merge what was correct and tested and treat the rework as a follow-up.

Julius said there probably wasn't much more that could be done in this change and was fine merging.

JB later laid out the full two mode design, Time Remap and Speed Ramp sharing one widget with a switch between them, passing `speed_map` to MLT in one mode and `time_map` in the other, roughly how Shotcut splits it across two effects. I did the two smaller pieces that fit in the time left, expanding to 13 keyframe types and moving the type selector into the toolbar next to the keyframe buttons, and told him straight that the mode split was a real new feature I couldn't scope properly before the deadline. That's tracked as [#2231](https://invent.kde.org/multimedia/kdenlive/-/work_items/2231).

<blockquote class="pull">
Saying I couldn't finish something well was harder than any of the code was.
</blockquote>

---

## Being wrong twice about the same bug

Near the end JB reported that keyframe types were getting silently lost when you resize a clip.

Took me <span class="bad">three tries</span>.

First attempt, reasonable theory, backed by reading the code, <span class="bad">wrong</span>.

Second attempt, also reasonable, also backed by reading the code, <span class="bad">also wrong</span>.

Both of them died the same way, by actually reproducing the bug and tracing the real call sites instead of reasoning about it. Reading code tells you what it looks like it does, running it tells you what it does, and I keep having to learn that again.

<span class="good">Third time I stopped theorising and just instrumented it:</span>

```
loadKeyframes IN  mapData= "0=0;171t=5.6;232q=10.4333;373=15.1333;519=19.9667"
loadKeyframes OUT gotTypes= 2

(drag right edge)

checkClipUpdate STORED= "...=0;...=5.6;...=10.4333;...=15.1333;...=21.8"   <- markers already gone
checkClipUpdate OURTYPES= 2                                                <- panel still had them
loadKeyframes IN  mapData= "...=5.6;...=10.4333..."
loadKeyframes OUT gotTypes= 0
```

The `t` and `q` are the keyframe types, and <span class="bad">they're already gone before the panel gets to reload anything</span>. <mark>So the panel was fine the whole time, it was faithfully reloading a string that had already lost the data upstream.</mark>

It was `ClipModel::requestRemapResize()` in `clipmodel.cpp`, a file this MR never touched. It reads the animation with `serialize_cut`, which does include the type markers, and then parses it by splitting on `;` and `=` and keeping only position and value, around lines 509 to 518. The type suffix sits on the position token so it gets dropped right there. Then it rebuilds with `"%1=%2"` pairs at line 625, which can't carry a type it never parsed in the first place.

<div class="callout">
It's not even a regression. That function was written when every remap keyframe was linear so there was nothing to preserve. My MR is just what made it visible.
</div>

I wrote all that up and asked about scope instead of fixing it, because that function threads a `QMap<int,int>` through about 15 insert, remove and move sites, and it's timeline code I hadn't worked in. Bernd said do what I'm comfortable with, JB said merge as is and do the fix in a separate MR.

<div class="callout good">
I like this one more than the clean stories. Wrong twice, publicly, in front of the person evaluating me, and the fix was just dropping the theory and going to get real evidence.
</div>

---

## The part I'd rather not write about

Somewhere in the middle of all this I was applying for internships.

Sending them out, hearing nothing back. <span class="muted">Still hadn't heard anything by the time GSoC ended.</span>

And that leaked into everything. <span class="bad">There were stretches, more than one, where I wasn't giving this project proper time or focus.</span> Blog posts that should have gone up on a Sunday went up <span class="warn">nine days later</span>. There were evenings where I opened the editor and just had nothing. There were mornings where I worked out how long it had been since my last commit and felt genuinely bad about it.

None of that is visible in the MR threads. The threads look steady. <mark>All the gaps are between the messages and nobody reads those.</mark>

What got me back wasn't ever some big reset. I never had the night where I made a plan and woke up sorted. What actually worked, every time, was <span class="key">picking the smallest possible next thing</span> and only doing that. Send one message. Reply to one review comment. Push one commit. Write one paragraph.

<mark>Trying to catch up on everything at once was the thing keeping me stuck</mark>, because the pile was the problem and looking at the pile made it worse. One small thing doesn't clear the pile, it just gets you moving again, and moving is the only state where anything actually gets fixed.

<div class="callout good">
So if you're in the middle of GSoC and you've gone quiet for a week and can't face the backlog, don't face the backlog. Just reply to one comment. That's the whole thing.
</div>

---

## Where it ended up

<span class="good">All three widgets are in master and shipping in 26.08.</span> Midterm passed, final evaluation submitted. The `requestRemapResize` fix is still open as a follow-up, scoped that way by JB so it isn't blocking anything. The mode split is [#2231](https://invent.kde.org/multimedia/kdenlive/-/work_items/2231), still waiting on someone, possibly me.

I also had to pull out of going to [Akademy](https://akademy.kde.org/) in Graz next month, because my semester exams land in the same week, which was disappointing since JB had offered me a spot in the team's Airbnb. Attending online instead. Going to try again next year, assuming I get a passport sorted.

<span class="good">My mentors said they'd recommend me to other contributors</span>, which is the outcome I care about most, because it's about how I worked rather than just what got merged.

The honest summary is that <span class="key">the code was the easy part.</span> <mark>Almost every hard moment this summer was a communication problem dressed up as a technical one.</mark> Asking what borrow meant before building two of everything. Saying I couldn't finish something properly instead of shipping it badly. Admitting the theory was wrong, twice. Reading two people who know more than me disagree and not pretending I could settle it.

And the review comments I kept aren't the approvals, they're the ones where someone told me the thing I'd built was wrong, and was right about it, and was decent about how they said it. There are a lot of those up there.

The technical write-up is [here](https://yashbavadiya.bearblog.dev/gsoc-2026-wrap-up) if you want to know what the widgets actually do.
