Article about Krita's architecture
Closed, ResolvedPublic

Description

I had a conversation with Boud about that topic, but I will come straight to the point. We are writing an article about Krita's Architecture and we would like your help. At the end of the work, we are intending to make the article available to the KDE community.

We will generate some diagrams from the source code, but we need more information that maybe you could provide.

  1. We know about the microtile architecture, but how is that important to Krita? I couldn't find the mailing list archives where Patrick talk about that architecture and why did he choose it. Boud told me about Raph Levien who originally created the idea behind microtile, maybe you may provide some source for that.
  1. We need to understand the plugins mechanism at Krita, how are they loaded, why did you choose implementing that way and what the types of plugins?
  1. We will create a concurrency view of Krita, we need some insights about the concurrency at Krita and why did you implement that way, though.
  1. At last, You may talk about some important decisions to Krita, as tests, some important decision about dependencies(libs, frameworks, etc), other data structures, etc.

Even if you can't help out on that points, any help is welcome and I'm grateful just for take attention to that.

eliakinalmeida triaged this task as Normal priority.
rempt added a comment.Apr 10 2018, 7:49 AM
  1. This is, I think, where I read about microtiles for the first time: https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=13&ved=0ahUKEwjEzNOjma_aAhWCFCwKHS11C98QFghjMAw&url=http%3A%2F%2Fstatic.usenix.org%2Fevents%2Fusenix2000%2Ffreenix%2Ffull_papers%2Fquintero%2Fquintero.ps&usg=AOvVaw1KqUJCFUj_r-_Y2GpFbWhP
  1. When moving to Qt5, Krita moved from using KDE's plugin architecture that used .desktop files to Qt's plugin architecture that embeds json files describing the plugin into the binary. Check KoJsonTrader to see how plugins are located.

The plugin architecture looks a bit magical at first, I was certainly surprised myself the first time I saw it. So here's my understanding having gone through this before.

First, the reason some libraries are implemented as plugins is the same reason you do that for any project - it makes code clean and modular, and makes recompile/relinking times faster. Nothing earth shattering, things are simply a little nicer that way. (One other reason people use plugin architecture is if they want to let independent parties develop plugins and distribute them separately, though that doesn't apply to Krita at the moment.)

The reason things look weird is that Krita relies primarily on two interfaces, QPluginLoader and KPluginFactory, to do the heavy lifting. The classes that actually get used in Krita, KoPluginLoader and KoJsonTrader, mostly do a simple search on the filesystem to find where plugins are located, and then some validation. The Qt class QPluginLoader takes care of the nastier bits of loading dynamic libraries and reading plugin metadata in a cross-platform way. KPluginFactory provides an architecture to simplify plugin creation and storing metadat.

Let's focus on filter plugins to keep grounded. At a high level, the process is like this. When KisFilterRegistry is first instantiated, it asks KoPluginLoader to load all plugins that provide filters. It calls the Qt/KDE libraries to find the matching plugins, load them, and run the code in each plugin's constructor class. This code will inject new filters into the registry with a few lines like KisFilterRegistry::instance()->add(new KisBlurFilter()); Now those filters will be available in the registry whenever it's queried, so we'll see the filters in the menu, and the plugin has done its job.


Let's break down the process in more detail.

To create a plugin, you do two things. First, create a library, and make sure the install path in CMakeLists.txt is a valid Krita plugin folder. Then, use the macro K_PLUGIN_FACTORY_WITH_JSON(). I'm going to call it the KPF macro for short. You can find an example in krita/plugins/filters/blur/blur.cpp:

K_PLUGIN_FACTORY_WITH_JSON(BlurFilterPluginFactory, "kritablurfilter.json", registerPlugin<BlurFilterPlugin>();)

The KPF macro does two things: first, it embeds a bit of .json in the library when the library is built, in this case it embeds plugins/filters/blur/kritablurfilter.json. Importantly, that file specifies it's a plugin of type "Krita/Filter". This will let Krita know how the plugin is used. The second thing the KPF macro does is specify what happens when the plugin gets loaded: it will create a object factory constructing objects of type BlurFilterPlugin.

Now we know how the plugin files get written, but for now it's still sitting in the install folder, so let's see how Krita actually loads it. When we create the KisFilterRegistry, we want to load all the filter plugins that are available. To do this, when KisFilterRegistry is instantiated, it calls: KoPluginLoader::load("Krita/Filter", "Type == 'Service' and ([X-Krita-Version] == 28"); specifying the type of plugins it's looking for. KoPluginLoader passes these specifications to KJsonTrader, which will search and return plugins that match the criteria. The global list of plugin directories is stored in KJsonTrader. It scans these directories for library files and uses QPluginLoader to look for the .json metadata we embedded with the KPF macro. If you look at kritablurfilter.json, you'll see it satisfies the parameters. KoJsonTrader then returns the matching plugins. (Sometimes KoJsonTrader is used directly to do things like query information about plugins without loading them. For example, KisImportExportManager tries to figure out which mimetypes Krita read by browsing the plugins metadata, without actually loading the plugins.)

After getting the list of matching plugins, KoPluginLoader does a few validation steps, like removing duplicates and checking blacklists. Finally, the plugin load step happens in just a few short lines:

KPluginFactory *factory = qobject_cast<KPluginFactory *>(loader->instance());
QObject *plugin = 0;
if (factory) {
    plugin = factory->create<QObject>(owner ? owner : this, QVariantList());
}

The function loader->instance() is the point at which the library gets loaded from disk and launched. Because we've used the KPF macro, instance() will return a KPluginFactory. We then call KPluginFactory::create(), which creates a BlurFilterPlugin, as specified in the KPF macro. Finally, this runs the code in the blur.cpp constructor, where you'll find the calls to KisFilterRegistry::add(). At that point, we've successfully added our new filters!

eliakinalmeida closed this task as Resolved.Aug 14 2018, 8:05 AM