Skip to content

Monday, 20 July 2026

Read on for a look at development news and the Krita-Artists forum's featured artwork from last month.

Development Report

Developments in Krita Plus

Dependencies have been updated in the Qt5 builds (Krita 5.3.x). This updates many libraries Krita depends on for things like file format support. (change)

Luna and Agata have continued improving the Selection Actions Bar. Now it remembers the distance from the selection on restart, and there's a button to pin it to the bottom of the canvas widget instead of next to the selection. (change)

In the Python scripting API, the View class now has signals for 'currentToolChanged', 'currentBrushPresetChanged', 'foregroundColorChanged', and 'backgroundColorChanged'. (change by Moritz Staudinger)

Developments in Krita Next

Wolthera and Dmitry continued working on HDR improvements. CICP values can now be read from and written to ICC profiles, and a "Krita Rec. 2100 Perceptual Quantizer (80cd/m²)" profile has been added (change). PQ reference white can now be read from ICC profile (change).

The Line Tool nows show the angle alongside the length in the floating message popup. (change by Moritz Staudinger)

Future Developments

Testing Needed: Android Animation Export

Last month's new feature of rendering timelapses using Android's built-in MediaEncoder is now being brought to animation export. But it needs testing before it can be merged to the main builds, so so check out the animation export on Android forum topic for test packages if you want to help!

Community Report

June 2026 Monthly Art Challenge Results

The winner of the "Something Unexpected" challenge is…

Join This Month's Art Challenge!

For July's theme, last month's winner passed the choice of topic to runner-up Paint4Thought, who has chosen "An Imaginary Friend". The optional challenge is to place the character(s) in a well-developed environment that contributes to telling the story. Show us what lurks in the depths of your imagination!

This month's featured forum artwork, as voted in the Best of Krita-Artists - May/June 2026:

Participate in next month's nominations and voting to voice your opinion on the Best of Krita-Artists - June/July 2026.

Krita is Free - But You Can Contribute!

Krita is free to use and modify, but it can only exist with the contributions of the community. A small sponsored team alongside volunteer programmers, artists, writers, testers, translators, and more from across the world keep development going.

If this software has value to you, consider donating to the Krita Development Fund. Or Get Involved and put your skills to use making Krita and its community better!

Krita's mascot Kiki putting money in a piggy bank

Additional Changes

Krita Plus (Stable, 5.3.3/6.0.3-prealpha):

  • Text: Fix various font loading issues. Use default vertical caret in non-variable fonts. Fix Regular font style not showing and defaulting to Italic. Fix duplicate fonts causing fonts not to load. (bug 1, bug 2, bug 3; change by Wolthera van Hövell)
  • Animation Timeline: Switching between frames should always complete, rather than sometimes showing the new frame while leaving the old frame active. (bug; change by Elena Sagalaeva)
  • Edit Shapes Tool: After splitting a path, allow moving the new point immediately instead of needing to move the cursor to change state. (change by Luna Lovecraft)
  • Toolbars: Add icons for 'Quick Group', 'Quick Clipping Group', and 'Quick Ungroup' actions. (change by Arkady Flury)
  • Qt6: Transform Tool: Fix perspective transform getting stuck waiting to complete when the transform became concave. (bug; change by Luna Lovecraft)
  • Qt6: Windows: Fix the canvas widget being transparent and letting other open documents show through. (bug; change 1, change 2 by Dmitry Kazakov)

Krita Next (Unstable, 5.4.0/6.1.0-prealpha):

  • Toolbars: Add widgets for the 'Choose Foreground Color', 'Choose Background Color', 'Swap Foreground and Background Colors', and 'Reset Foreground and Background Colors' actions. (change by Arkady Flury)
  • General: Allow choosing any installed application style instead of only Fusion, macOS (on macOS), Haiku (on Haiku), and Breeze (on Linux distribution builds). This unrestricts using Windows style (not just on Windows), windowsvista/windows11 (on Windows), and styles available on Linux distro builds. (change by Halla Rempt)

Nightly Builds

These pre-release versions of Krita are built every day.

Note that there are currently no Qt6 builds for Android.

Get the latest bugfixes in Stable Krita Plus (5.3.3/6.0.3 prealpha): Linux Qt6 Qt5 — Windows Qt6 Qt5 — macOS Qt6 Qt5 — Android arm64 Qt5 – Android arm32 Qt5 – Android x86_64 Qt5

Or test out the latest Experimental features in Krita Next (5.4.0/6.1.0-prealpha). Feedback and bug reports are appreciated!: Linux Qt6 Qt5 — Windows Qt6 Qt5 — macOS Qt6 Qt5 — Android arm64 Qt5 – Android arm32 Qt5 – Android x86_64 Qt5

Sunday, 19 July 2026

People tend to think an icon is a single drawing.

One thing I think people miss (or at least some do) is that an icon isn't really one icon at all.

The KRecorder microphone above exists as several different versions depending on size. In total that's 16×16, 22×22, 32×32, 48×48, 64×64, 128×128 and 256×256 PNGs, plus a number of SVG versions (6 six more files ). And don't always stop at 128×128 for the vectors either. Sometimes I end up making much larger vector versions simply because I want the extra detail to be there. (I have issues )

22x22 and 16x16 still missing in the repo and the image above

Talking about "issues"...

I couldn't resist rendering a version at almost 1024×1024 just to see how far I could push it. Look at the metallic reflections in the bottom half of the microphone. I spent a completely unreasonable amount of time tweaking those and honestly... I regret nothing 😀

Which does make me wonder...

Should we ship those absurdly detailed versions too?

Probably not.

...but then again, if somebody wants to inspect an icon at 1024 pixels wide, who am I to stop them? 🙂

...but maybe. 🙂

A hotfix release of kf6-core24 to work around a Snapcraft 9 bug that was breaking Qt-based snaps, plus an update on current Debian packaging and snap development work.

Saturday, 18 July 2026

Last weekend I joined an in-person workshop at HTW Berlin for discussing topics around mapping indoor spaces in OpenStreetMap.

Indoor mapping

Mapping indoor spaces is a somewhat niche topic in the OSM community still, but something that is quite relevant for projects I’m involved in:

The term “indoor” isn’t strictly referring to “in a building” here, there’s many gray areas e.g. at train stations. What’s usually more important is that this introduces a third dimension into the originally mostly two-dimensional OSM data. Another challenge is the need for a very high level of detail, for wheelchair routing every single step matters for example.

Since the last workshop four years ago we had quarterly online meetups to discuss modeling and tagging questions, but for some topics an hour or two in an online meeting is just not enough to properly cover this, it needs locking people in a room for a day or two instead.

Balancing requirements

While it’s often not hard to find a solution for a modeling problem at hand, finding one that works well for all use-cases is hard:

  • Easy to explain, use and maintain. Don’t require deep domain knowledge of railway operations or civil engineering for mapping.
  • (Backward) compatible with existing data and existing semantics. OSM is a database with billions of objects built up over more than two decades, with countless users.
  • Usable for 2D rendering.
  • Usable for 2.5D or 3D rendering. This specifically benefits from a higher spatial resolution in the third dimension (which currently is essentially floor levels, compared to the centimeter resolution in the other dimensions), but also from the ability to map visuals of vertical features.
  • Usable for tactile rendering. This one is challenging as it’s actively harmed by a higher level of detail. On dynamic tactile displays you have to work with as little as 100 “pixels” in each direction, and possibly just one 1 bit of “pixel depth”, which requires very aggressive abstraction and simplification. 3D printed tactile maps have a higher spatial resolution and allow for some basic textures and symbols, but still way below the options you have with a visual display.
  • Usable for routing, both with graph-based and area-based algorithms. The main challenge here is completeness of the data, every barrier has to be there to avoid the router taking clever shortcuts.
  • Being able to model all accessibility-relevant properties. See the discussion on directional door attributes below, for example.
  • Compatibility for importing BIM data, ie. digital engineering models of buildings. That’s on the extreme end of the level of detail usually, and needs to be significantly reduced/simplified. But it’s an attractive source of high quality building geometries for say an entire university campus.

Fortunately we had people familiar with all those aspects present at the workshop, which helps to avoid easy one-sided compromises.

What even is a door?

There’s detailed notes on the discussion in the wiki, I’ll just pick one topic here to show how even seemingly easy and obvious things are surprisingly complicated when digging into the details, doors.

  • Is a door frame with the actual door removed still a door?
  • Is a multi-segment foldable door as e.g. found in shop fronts or between conference rooms a door? If so, does that mean doors can contain doors?
  • Doors are 0-dimensional features (ie. points) in OSM. That’s a useful simplification, but how do we define directional attributes in that case, e.g. in which direction does the door open?
  • Inside/outside is an intuitive way to do that, but that fails in more complex buildings. It would also mean evaluating door attributes requires determining an “inner-ness” hierarchy of all areas in a building, extremely unwieldy, if that would even be well-defined.
  • Modeling doors as 1-dimensional features (ie. lines) brings in an inherent direction, but that is perpendicular to the intuitive direction of standing in front of a door, requiring some mental gymnastics to make this work (“I am the door”).

This might seem pointlessly abstract and theoretical, but for e.g. wheelchair routing this is quite relevant. The opening mechanism and opening direction of a door have quite some impact on how easily you can get through.

Outlook

State of the Map 2026 logo

Indoor mapping will probably also be a topic at State of the Map 2026 in a few weeks in Paris. I’ll be speaking about Transitous there, one of the consumers of this data.

I spent the last 2 weeks mostly on fixing the bugs that were breaking font subsetting for annotations.

One such bug was about deleting the original font which we talked about in the last blog.
And there were some more edge-cases and type bugs.

For eg: An object could be a ref but I directly do obj->getStream() instead of obj.fetch(xref).getStream() which crashed the code.

I spent 3-4 days on writing some tests for the font subsetting. I just test 2 things for now.

  1. Single annotation test: Here we test if subsetting behaves well for a single annotation. We load a file, add an annotation, save it, re-open it, and check if the annotation is using a properly subsetted font which has glyphs only for the characters we added.

  2. Linked annotation test: We try to check if subsetting for a particular annotation leaves all the other annotations intact. So, we load a PDF file which has 2 annotations that use the same font, modify the 1st annotation, save and re-open, check if the subsetting works well for the 1st annotation, and check if the font binary data for the 2nd annotation is intact.

These tests required approximately 400 lines of code which is slightly surprising. This is almost the same amount of code as the subsetting logic in FontSubsetter.cc

I also used core poppler code for these tests and not an API like poppler-qt5 or poppler-qt6 because I needed a lot more control to test these things.

I also switched from Vim + terminal coding workflow to QtCreator. This was basically because I am not very good at GDB right now and wanted a visual debugger to make work easier. Switching to CLion just for it's debugger felt weird.

So I started using QtCreator and it feels okayish right now. I use it with it's FakeVim plugin.

Now, I need to work on:

  1. The extra font bug in poppler: Basically, when poppler tries adding fonts for a text, it seems to be including an extra wasteful font such as Cantarell in my case. I need to investigate why this happens.

  2. Make our own splitTextByFont: Right now, I just save the font-string mappings when the AP stream is generated in an annotation. This ensures consistency between what is being written to the AP stream and what is visible to the subsetter.
    However, this is not very good both performance-wise and code-wise. We store the mappings for annotations that we might never subset. And it requires change in the internal code.
    Therefore, I have to create my own splitTextByFont function inside my FontSubsetter class which takes the font and returns the appropriate font-string map.
    We need to be careful that we don't cause inconsistencies between what's visible to the subsetter and what actually gets added to the AP stream because that might cause rendering issues.

  3. Fixing more bugs: Because bugs never end in moving software, do they?

Thanks for reading! Have a great day ☺️

Thursday, 16 July 2026

Update: Automatic server-side drop shadows were reverted because that regressed the visuals of a reasonable number of apps. It appears like applications use normal toplevel windows to implement popups instead of relying on more appropriate window types. The window rule is still there. We’d like to revisit automatic SSD drop shadows, but at the moment, it’s on the back burner.

It’s been a long time since I wrote here last time. I would like to share a few details about a feature that I’m really excited about, which landed in KWin recently.

Drop shadows are drawn either by the compositor or the application. For example, a good chunk of GTK applications employ the latter strategy, the drop shadows are drawn on the client side; Qt applications usually ask the compositor to draw a window decoration plus the drop shadow. However, there are also applications that do neither. For the consistency sake, it will be nice if you could force the compositor to add drop shadows for those windows. This is the new feature that will come in the next release of Plasma — 6.8.

For example, consider Discord with the current defaults

It has square corners, there are no drop shadows, the titlebar buttons don’t look consistent, etc.

With the new changes, Discord will look as follows

There are still some inconsistency issues, e.g. the close, maximize and minimize buttons don’t look consistent, but still, now, Discord blends in better with the rest of Plasma, for example it casts a shadow, it has rounded corners and there is an outline drawn around the window.

How it works

You can already achieve similar visuals in 6.7. In order to do that, you need to create a window rule to force a server-side decoration, and then go to Breeze decoration settings and create a window-specific override to hide the titlebar.

For example, here are the required steps to add a server-side drop shadow around Visual Studio Code in Plasma 6.7

“No titlebar and frame” window rule
Breeze window-specific overrides

The changes in 6.8 rather automate those steps for you. Unfortunately, it doesn’t work with every available decoration in the wild. Decorations need to opt-in to providing only server-side drop shadows.

First, a decoration needs to declare that it supports both shadow-only and titled decorations in the metadata, e.g.

    "org.kde.kdecoration3": {
        "styles": ["shadow", "titled"]
    }

Then the decoration needs to adjust its visuals based on the value of KDecoration3::Decoration::style(). For example, hide the titlebar, etc.

How to add shadows around windows

If you would like a given window to have a server-side drop shadow, we added a new “Window manager draws titlebar, frame, and shadows” window rule that supersedes the old “No titlebar and frame” rule

Note that KWin will automatically add drop shadows to X11 windows that have neither a server-side decoration nor a client-side drop shadow. No such a thing will be done for Wayland windows though because of sub-surfaces. On Wayland, we may need a protocol to opt-in to such things, in meanwhile, you’ll need to use window rules.

Closing words

Anyway, it’s a rather quick development update. This is a small thing but I hope that people will find it useful for making their desktops look more eye-candy.

A summary of my three-part analysis of the EU Open Source Strategy on Tagoross. Real strengths, real design flaws. The sovereignty definition does not fit open source. The legislation, CADA and Chips Act 2.0, encourages or stays silent rather than requires.

Wednesday, 15 July 2026

The Akademy 2026 Program is now live!

This year’s Akademy will take place in Graz, hosted at the Graz University of Technology, both in person and online.

Akademy starts with a welcome event on Friday, 18 September, followed by two full days of talks on Saturday, 19 and Sunday, 20 September, then four days of dedicated BoFs, workshops, meetings, and training from Monday, 21, through Thursday, 24 September. Expect a community day trip midweek.

The schedule highlights:

  • Talks covering What's coming up in Qt, KDE Linux at 2, Beauty in Code, and many more.
  • More in-depth sessions on An Agency, A Need of Sovereignty, and KDE's Will to Conquer the Enterprise, Goals wrap-up and reveal of new goals, Are we really doing to use the same Desktop UX forever? and beyond.
  • Community-driven workshops and BoFs cultivate collaboration and project momentum throughout the week.

This hybrid event model continues to grow, embracing both onsite attendance and remote participation, allowing contributors from around the globe to connect and engage.

Venue & Registration Details:

  • Venue: Graz University of Technology
  • In-person + Online: 19–24 September (with the welcome event on 18 September).
  • Registration is open and free!
  • You can explore the full program on Akademy’s website. Stay tuned for our keynote announcement!

Tuesday, 14 July 2026

GSoC 2026 • digiKam • Post 2: Inference, Bugs, and the Build

In my first post, I introduced the goal: type a plain-English search into digiKam and have a local LLM translate it into structured search criteria. That post built the whole pipeline - prompt builder, JSON parser, intent resolver, against a mock backend that returned canned responses, so everything could be tested before a real model was wired in. This post is about swapping that mock for real llama.cpp inference, and everything that broke along the way, which was almost never the model.

At the end of my last post I promised that this one would be about the actual language model: which one, how fast, how accurate. I’ve been looking forward to writing it.

Here’s the thing I did not expect. The model works. It has essentially always worked. Almost every hard problem I hit over the past few weeks lived somewhere else: in a compiler flag, in a JSON type, in a git server’s opinions about submodules. This post is the honest version of what it takes to put a language model inside a desktop application, and the honest version is that the language model is the small part.

Natural language search demo

Actually running the thing

Last time the pipeline ran end-to-end against a mock backend: something that returned canned answers so I could build and test everything around it. Replacing that mock with a real model meant writing SearchLlamaBackend, which loads a quantized Qwen2.5 GGUF through llama.cpp and generates tokens.

Two decisions shaped it.

The first decision was that every single llama_* call happens on a worker thread. Loading a 1 GB model takes a few seconds; generating tokens takes a few more. If any of that ran on the GUI thread, digiKam would freeze every time you searched. So the backend owns a QThread, the worker lives on it, and everything crosses the boundary through queued signals - the UI stays responsive while the model thinks.

Here’s the shape of it (simplified from the real method, which has the error handling and tokenization removed for readability):

void SearchLlamaWorker::slotDoInference(const QString& prompt, int maxTokens, float temperature)
{
    Q_UNUSED(temperature);   // greedy decoding, determinism over creativity

    llama_context*     const ctx   = static_cast<llama_context*>(m_context);
    const llama_vocab* const vocab = llama_model_get_vocab(/* ... */);

    // Start each query from an empty context.
    llama_memory_clear(llama_get_memory(ctx), true);

    // Greedy sampler: always pick the single most likely next token.
    llama_sampler* smpl = llama_sampler_chain_init(llama_sampler_chain_default_params());
    llama_sampler_chain_add(smpl, llama_sampler_init_greedy());

    QString result;

    while (generated < maxTokens)
    {
        llama_decode(ctx, batch);
        const llama_token tok = llama_sampler_sample(smpl, ctx, -1);

        if (llama_vocab_is_eog(vocab, tok)) break;

        result += /* decoded token text */;

        // Stop as soon as the JSON object closes (balanced braces).
        if (jsonObjectComplete(result)) break;
    }

    llama_sampler_free(smpl);
    Q_EMIT signalOutputReady(result);   // back to the main thread, via a queued signal
}

Two things in there are deliberate. llama_memory_clear at the top wipes the context’s KV cache so every query starts fresh - I’ll come back to why that one line matters more than it looks. And the sampler is greedy: no temperature, no randomness, the model always takes its single most likely token. That’s the opposite of how you’d run an LLM writing prose, where a little randomness keeps it from sounding wooden. But I don’t want prose. I want the same query to give the same JSON every time - so a bug is reproducible, and so the query cache from the last phase stores a real answer instead of one of several possible ones. For structured output, determinism isn’t a limitation; it’s the whole point.

Knowing when to shut up

A small problem I enjoyed solving. The model is supposed to emit one JSON object and stop. Sometimes it does. Sometimes it emits the object, decides it’s on a roll, and keeps going, producing helpful commentary, a second example, and whatever else it feels like until it hits the token limit.

Generating tokens you’re going to throw away is pure waste, and on a CPU each one costs real time. So the decode loop watches the output as it accumulates and counts brace depth. The moment the braces balance, meaning the first complete JSON object has closed, generation stops. In practice this cut a typical query from a hundred-plus tokens down to about twenty-two.

It’s a heuristic, and I know its failure mode: a } inside a string value would fool it. My schema doesn’t have string values that contain braces, so it holds. If that ever changes, the honest fix is to attempt a real parse each iteration and stop when it succeeds. I’d rather ship the simple thing that works and know exactly where it breaks.

Natural language search demo

Three bugs, none of them the model’s

Once real queries started flowing, things broke. Every single time, I assumed the small model was being dumb. Every single time, I was wrong.

A rating of 5 kept vanishing. I’d ask for five-star photos, watch the model emit perfectly correct JSON with "value": 5 in it, and watch the rating field come out empty. The parser was calling QJsonValue::toString(), which returns an empty string when the value is a number rather than a string. Not an error. Not a warning. An empty string. The model had said 5; my code heard silence.

The fix was to stop assuming. Instead of blindly calling .toString(), the parser now checks the JSON value’s type first, string, number, or bool, and converts each properly (QString::number() for a number, and so on). One value arriving as 5 instead of "5" shouldn’t be able to silently erase a search constraint, and now it can’t.

Dates never populated. The model would emit a date. The date widget wanted a range, in the form start..end. Nobody had told the model that. This wasn’t a bug in the model so much as a bug in the instructions I’d given it.

The fix was in the prompt, not the code. I added an explicit instruction: dates must always be a range in the form 2023-01-01..2023-12-31, a whole year expands to its first and last day, a whole month to its month boundaries. Plus one worked example. Small models learn far more from a single concrete example than from three sentences of rules, and once the example was there, the ambiguity was gone.

And then: “last year” meant 2022.

This one is my favourite, because it’s structural rather than accidental. I typed “photos from last year,” expecting 2026. The model confidently produced 2022.

It wasn’t guessing badly. It has no clock. A language model’s sense of “now” is a fossil of whenever its training data was collected. It has no way to know what day it is, and this is the part that matters - no way to know that it doesn’t know. So it answers with total confidence, and it’s wrong, and nothing in its output looks any different from when it’s right.

The fix is embarrassingly simple: tell it the date. The prompt now includes today’s date and spells out the conversions explicitly: “last year” means such-and-such a range. It works.

But I keep turning the general shape of this over. An LLM’s confidence is uncorrelated with whether it has the information needed to answer. Every layer of validation in this project exists because of that, and I built those layers before I had a concrete example of why they mattered. Now I have one.

The practical lesson for digiKam is concrete: an LLM has no real-time awareness, and photo search is full of time-relative queries: “last year,” “last summer,” “two months ago.” Any of those is a landmine unless the prompt supplies the one thing the model can’t know on its own. So the current date now goes into every prompt, with the relative conversions spelled out. The model doesn’t need a clock; it needs to be told what time it is.

Where the code lives, or: the submodule that couldn’t

llama.cpp had to get into digiKam’s tree somehow. The obvious answer, and the one my mentor and I agreed on, was a pinned git submodule: reference a specific tag, build it in-tree, keep it clearly separate from digiKam’s own code.

I did that. I got it building. I pushed.

remote: Audit failure - Invalid filename: .gitmodules
remote: Push declined - commits failed audit

KDE’s git infrastructure does not permit submodules. The server rejects the push before it lands. My mentor’s response was immediate and pointed me at the right precedent: digiKam has vendored external code for years. libraw, libpgf, QtAVPlayer are all sitting in the tree as plain source. Copy llama.cpp in the same way, pin it to a tag, document where it came from.

So I vendored it. And pushed. And:

remote: Audit failure - Invalid filename:
  core/utilities/searchwindow/thirdparty/llama.cpp/.gitmodules

llama.cpp has its own submodules. Of course it does.

What followed was a trim. Out went the examples, the tools, the tests, the CI configuration, the Python conversion scripts, the web UI, the Swift bindings, the benchmark JSONs. What remained was src/, include/, ggml/, and the CMake files, the parts that actually build the library. Around 400 MB became 25 MB, the audit passed, and as a small bonus a CI job that had been failing (digiKam’s JSON validator choking on llama.cpp’s own tooling configs) started passing, because the files it was choking on no longer existed.

ApproachProsCons
Git submoduleEasy updates, clean separationRejected outright by KDE’s git server
VendoringFull control, self-containedManual updates, larger repository

For KDE’s infrastructure, vendoring wasn’t the better option so much as the only one that gets past the server. It’s worth being honest that it’s a workaround, not the ideal end state: the cleaner long-term answer is for llama.cpp to be available as a standard system package that digiKam can simply depend on, the way it does for most of its libraries. Until then, a trimmed, pinned, documented copy in the tree is the pragmatic choice.

There’s a manifest file now too, llama_cpp_manifest.txt, in the same one-line format digiKam uses for every other bundled library. It records the exact commit that’s vendored. At packaging time it’s parsed into the Help → Components Info dialog, so when a user reports a bug we know precisely which llama.cpp is running underneath. It has to be updated by hand on every upgrade, which is noted, loudly, in the README.

Seventy-eight seconds

The bug I’m most glad I chased.

Once everything built, a single query took over a minute. The log was blunt about it:

TIMING: generated 22 tokens in 78012 ms

Twenty-two tokens. Seventy-eight seconds. Roughly three and a half seconds per token, for a 1.5B model on a machine that should manage tens of tokens per second.

I went looking for the pathology. Was it swapping? A gigabyte of model plus KV cache on a 15 GB machine, plausible but free showed plenty of headroom and barely any swap in use. Was it thread contention, too many threads fighting over eight cores? I checked top while a query ran, expecting to see the process idle, blocked on something.

It was at 750% CPU. All eight cores, flat out, for seventy-eight seconds, to produce twenty-two tokens.

That’s not a process that’s stuck. That’s a process working extremely hard and getting nowhere, which is a much more specific symptom, and it pointed at exactly one thing:

CMAKE_BUILD_TYPE:STRING=Debug

I develop in Debug builds. Faster compiles, usable in a debugger, the sensible default. And llama.cpp, sitting in-tree, inherited that build type which meant ggml, the matrix-multiplication engine underneath everything, was compiled at -O0. No inlining, no vectorization. The SIMD instructions were available (-march=native was there); nothing was using them.

Reconfiguring with -DCMAKE_BUILD_TYPE=Release flipped ggml to -O3, and the same query dropped from 78 seconds to about 8.7. A bit under nine times faster, from one flag. It’s still not fast, because a 1.5B model on a CPU never will be, but usable is the bar that matters.

Build typeTokensTimeTokens/sec
Debug2278,012 ms~0.3
Release228,674 ms~2.5

Same query, same machine, same model. The only difference is the compiler optimization level of the bundled llama.cpp.

The proper fix isn’t “always build Release,” because I want to keep debugging my own code. It’s a few lines of CMake that force optimization onto the bundled llama and ggml targets specifically, even in a Debug build, leaving the rest of digiKam alone:

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    foreach(_llama_target llama ggml ggml-base ggml-cpu)
        if(TARGET ${_llama_target})
            target_compile_options(${_llama_target} PRIVATE $<$<CONFIG:Debug>:-O2>)
        endif()
    endforeach()
endif()

So my code stays debuggable, ggml stays fast, and the next person who builds digiKam in Debug doesn’t lose an evening the way I did.

The line I promised to come back to

Once inference was fast, the feature worked. I typed a query, got the right photos, typed another, got those too. I was ready to call it done.

Then I noticed that if I searched enough times, every search started failing. Not one bad query - all of them, from some point onward. The first few worked perfectly; then a wall, and after it, every single query came back with “could not interpret the model output,” permanently, until I restarted digiKam.

That “permanently until restart” is the tell. A bad query is one thing; a backend that works and then stops working forever is state gone wrong. Something was accumulating.

It was the KV cache. A language model’s context has a cache of the tokens it has already seen, and llama.cpp appends to it as you decode. My inference code decoded each new query’s prompt straight onto the end of that cache without ever clearing it. So query one ran at positions 0 to 40. Query two ran at positions 40 to 80 - stacked on top of query one, which was still sitting there. Every search pushed the position higher, and once the total crossed the context limit (n_ctx, 4096 tokens), llama_decode started failing and never recovered, because the cache stayed full.

The fix is the single line from the snippet earlier:

// Start each query from an empty context.
llama_memory_clear(llama_get_memory(ctx), true);

Clear the cache at the start of every inference, and each query is independent again.

What gets me about this one is why I didn’t catch it sooner. Every time I tested during development, I was restarting digiKam constantly - rebuilding, relaunching, running one query, rebuilding again. A fresh process has an empty cache, so the bug was invisible. It only appears when you do what an actual user does: open the app once and search several times in a row. My whole testing rhythm was hiding it.

That’s the second bug in this project that only showed up under repeated real use - the first being a compiler flag that would only misbehave on someone else’s CPU. Both are arguments for the same thing: a test that runs two queries back to back, which is exactly the kind of automated inference test my mentor asked about in review. A single-query test would have passed. The bug lives in the second query.

What I actually learned

I came into this project wanting to understand LLMs, and I have. But the thing I did not anticipate is how much of “put an LLM in an application” is not about the LLM.

It’s about whether a bundled CMake target can live in an exported target’s link interface. (It can’t, and the workaround is $<TARGET_FILE:> plus an explicit add_dependencies to restore the build ordering.) It’s about a recursive header glob quietly sweeping llama.cpp’s headers into every unrelated compilation unit in the project, breaking files that have nothing to do with any of this. It’s about your distribution shipping OpenCV 4.6 when the project needs 4.8. It’s about a git server’s twenty-year-old policy on submodules.

None of that is glamorous. All of it is the job. The model was the part I understood; everything wrapped around the model was the part I had to learn, and it’s the part I’m most glad to have learned, because it’s the part that makes a feature into something a project can actually ship and maintain.

Key takeaways

  • The model is the small part. The real work of putting an LLM in an application is integration: the build system, the packaging, the infrastructure. The inference was the piece I understood going in.
  • Determinism is a feature. For structured output that feeds a cache and has to be reproducible, greedy decoding beats anything with randomness in it.
  • Build flags decide whether a feature is usable. The same code went from 78 seconds to 9 with one optimization level. Always profile in Release.
  • Infrastructure has opinions. KDE’s git server rejects submodules outright, so vendoring wasn’t a preference, it was the only way in. Know your project’s constraints before you design around them.
  • An LLM’s confidence says nothing about whether it’s right. It called “last year” 2022 with total certainty. Every validation layer in this project exists because the model can be confidently wrong, and the output has to be checked against what the collection actually contains.

Where things stand

Natural language search runs end-to-end against a real, local Qwen2.5 model. You type “photos from 2023 rated 5 stars,” the model turns it into structured constraints, digiKam’s own search engine finds the photos. “Red label photos rated at least 3 stars” works. Date ranges work. Relative dates work.

The pipeline tests run against the mock backend and need no model, which keeps them CI-safe, and they now include regressions for both the numeric-value and the date-range bugs above. Neither of those would have been caught by a test of the model. Both were caught by a human typing a query and squinting at the result, which tells you something about where the bugs in this kind of system actually live.

What’s next

  • Real-inference test: an automated test that loads the actual model and runs a query, gated on the model being present so CI stays green when it isn’t. The KV cache bug above is exactly what this would catch, so it’s first.
  • Fix caching for relative dates: the query cache currently stores relative-date queries, so a cached “last year” quietly goes wrong once the year changes. Those simply shouldn’t be cached.
  • Ambiguity resolution: “landscape” is both an orientation and a subject, and the model hedges. The robust fix is validating values against the collection’s actual tags and people, which the resolver already has hooks for.
  • Prompt hardening: small models resist saying “I don’t know.” Prompt work has helped but not solved it.
  • Benchmarking: the comparison I promised, Qwen2.5 against TinyLlama on real digiKam queries.

Thanks for reading. If you’re curious about the project or working on something similar, you can email me at: srirupa.sps@gmail.com if you wanna discuss! :)

Monday, 13 July 2026

So… another missing icon gets some attention 🙂

This time it was Kommit.

The funny thing about Git related icons is that everybody eventually ends up drawing the same thing. A couple of connected nodes, some branches in a diamond shape....

I started pretty close to the existing Git visual language adjusted to Oxygen, but after a few iterations the icon slowly started drifting towards something that felt a bit more interesting. Softer shapes, more emphasis on the flow of the branches and less "K pasted into a square". (we love k)

So here is the story of how a icon is set to life I could say a ton more things but.... heeee

This was not working for me at all need to pivot....

ok there is somthing cool here lets explore more

shading lets do shading, (the old trick) oo and what if the line was a kinda of a flowing path?

wil never work as an icon, hummm.. need to make it more boxy!

hummm maybe less red??? the previous line art was kinda cooler loking... ;(

yeah!!!.... but what if we make the lines more chubby?

ooooo yeah...."that is the way"