I found this Krita brush bundle: Memileo Impasto Brushes. It looks very realistic and from videos I've watched about it, it supposedly also feels natural.

Naturally, I downloaded it, improted it into Krita and tried to use it, but alas, it didn't work. Specifically, Krita loaded the bundle, but the brushes weren't in the bundle.
Act 1
This was a common issue, apparently. Looking in the forum, you'll find several others facing the same issue. And thus, my adventure begins.
I started by trying the solution that evidently worked for most people: clearing Krita's resource cache. There's a file located at ~/.local/share/krita/resourcecache.sqlite
that serves for what it says on the tin. The users in the forum said to perform some combination of "deactivating" the bundle, clearing the cache and reactivating the bundle again, and that that should pretty much fix things. I tried every permutation of this method to no avail, even including uninstalling and re-installing Krita, and deleting all Krita related files from my disk. It didn't work.
Act 2
I went back to the forum, further strengthening my bond with my scrollwheel, and found this peculiar comment:

Huh, one user who faced the issue was using the Arch package of Krita, but when they used the AppImage it worked. This would've been my cue to try a Krita AppImage with that specific version myself, but that felt disgusting. You see, I use Nix(OS)1, and this is a prime example of something needing nixification.
Since it was heresy to use an AppImage2, I decided to dive deeper in the forums in hopes that I find a more specific issue. I had to scroll for a bit before finding it:

I thought it was gonna be one of those things where I either spend the rest of my weekend trying to fix it, or accept defeat and move on with my life knowing that this experience will forever be unavailabe to me. However, it turns out, as it always does, that somebody much smarter than me already figured it out.
It turns out that the issue only exists in native Linux installs of Krita, with the culprit being libpng, or rather its usage. You can look at the linked issue, if you wanna know the specifics, but the general gist of it is that the brushes in this bundle are massive in size, and somehow there is a configuration in libpng that specifies some size of 8MB which truncates the bundle files when they're loaded, breaking them.
The solution? Patch the Krita package with its libpng dependency configured to not truncate any files.
Enter Nix
Alright. So we know that for some reason, the libpng config has these variables set
setting USER_CHUNK_CACHE_MAX default 1000 setting USER_CHUNK_MALLOC_MAX default 8000000
which means that it can't load any files greater than 8MB.
In the linked issue, there's a patch to fix this by setting these variables to 0
.
diff --git a/scripts/pnglibconf.dfa b/scripts/pnglibconf.dfa index fe8e481..ecaffe7 100644 --- a/scripts/pnglibconf.dfa +++ b/scripts/pnglibconf.dfa @@ -516,8 +516,8 @@ setting USER_WIDTH_MAX default 1000000 setting USER_HEIGHT_MAX default 1000000 # Use 0 for unlimited -setting USER_CHUNK_CACHE_MAX default 1000 -setting USER_CHUNK_MALLOC_MAX default 8000000 +setting USER_CHUNK_CACHE_MAX default 0 +setting USER_CHUNK_MALLOC_MAX default 0 # If this option is enabled APIs to set the above limits at run time are added; # without this the hardwired (compile time) limits will be used.
Now, how do I get this into my NixOS? Luckily there's a NixOS overlay right there in the same issue.
patched-krita = pkgs.replaceDependency { drv = pkgs.krita; oldDependency = pkgs.libpng; newDependency = pkgs.libpng.overrideAttrs (old: { patches = old.patches or [ ] ++ [ ./libpng.patch ]; }); };
I saved the patch, appended the nix expression to my config, and removed the old Krita installation and, it worked! The brushes were loaded, and I could use them no problem.
By the time it worked, I was very sleepy from hours of debugging, and didn't really have the energy to paint, but I was happy anyway. When I woke up the next day however, I kept thinking about how I had something I didn't understand in my NixOS config. Additionally, I saw in the same github issue that this approach was discouraged. So, I went back to my config to sort things out.
My questions are:
- What does the
replaceDependency
function actually do?, and - Why is it discouraged?
replaceDependency
So, what does the function really do? The only documentation I could find3, was in the source code:
# Replace some dependencies in the requisites tree of drv, propagating the change all the way up the tree, even within other replacements, without a full rebuild. # This can be useful, for example, to patch a security hole in libc and still use your system safely without rebuilding the world. # This should be a short term solution, as soon as a rebuild can be done the properly rebuilt derivation should be used. # Each old dependency and the corresponding new dependency MUST have the same-length name, and ideally should have close-to-identical directory layout. # # Example: safeFirefox = replaceDependencies { # drv = firefox; # replacements = [ # { # oldDependency = glibc; # newDependency = glibc.overrideAttrs (oldAttrs: { # patches = oldAttrs.patches ++ [ ./fix-glibc-hole.patch ]; # }); # } # { # oldDependency = libwebp; # newDependency = libwebp.overrideAttrs (oldAttrs: { # patches = oldAttrs.patches ++ [ ./fix-libwebp-hole.patch ]; # }); # } # ]; # }; # This will first rebuild glibc and libwebp with your security patches. # Then it copies over firefox (and all of its dependencies) without rebuilding further. # In particular, the glibc dependency of libwebp will be replaced by the patched version as well. # # In rare cases, it is possible for the replacement process to cause breakage (for example due to checksum mismatch). # The cutoffPackages argument can be used to exempt the problematic packages from the replacement process.
Okay. Pretty much self explanatory. Take a dependency of a package and replace it with a new one–usually a modified version of the old one. Convenient.
How does it work internally? The replaceDependencies
function, which replaceDependency
calls internally, allows the user to modify a dependency of a package without rebuilding the whole world. It does so by taking the closure of the package (files and directories in the Nix store "reachable" from the package), and making a new derivation by selecting and replacing every instance of oldDependency
with the store path of newDependency
. This process requires the new dependency to have the same exact name due to the renaming process. It's a fragile process, and I can see why its use is not recommended but for such a simple patch, I give myself a pass.
The alternative would've been to override pkgs.libpng
globally, which is a more robust approach, but I'm not really a fan because I can't possibly comprehend the implications of that on the other applications that use it. Another alternative would be to add a new Krita specific nixpkgs
as an input to my system configurations' nix flake. this way, I can override the libpng package globally, for that specific nixpkgs
from which I'm installing Krita. This approach is nice but it would mean I would have to build Krita from scratch.
So I decided to just go with the approach I'm using currently because hey, it's hacky, but it's quick, does the job, and if anything breaks in the future, I can just fall back to these more "proper" approaches.
Ending notes
So that was that. I learned a thing or two about Nix, Krita, and a bit more than I'd like about libpng. But in the end, I'm a more confident wielder of these weapons.
Credit to all those helpful people in the krita-artist.org forum. I've blurred their handles from my screenshots for privacy reasons, but I would like to say my thanks for the help anyway.
And of-course, credit to those in the linked Github issue for providing the code which I copy-pasted into my config and which turned out to be the topic of the whole post.
It's really cool how there are so many brainy people in FOSS who are so kind and helpful.