返回介绍

Strokes queue

发布于 2024-06-23 21:36:11 字数 28212 浏览 0 评论 0 收藏 0

Strokes, jobs… What it is all about? (theory)

Structure of a stroke

An abstraction of a stroke represents a complete action performed by a user. This action can be canceled when it has not been finished yet, or can be undone after it’s undo data has been added to the undo stack. Every stroke consists of a set of stroke jobs. Every job sits in a queue and does a part of work that the stroke as a whole must perform on an image. A stroke job cannot be canceled while execution and you cannot undo a single job of the stroke without canceling the whole stroke.

Example: Lets look at how the Freehand Tool works. Every time the user paints a single line on a canvas it creates a stroke. This stroke consists of several stroke jobs: one job initializes indirect painting device and starts a transaction, several jobs paint dabs of a canvas and the last job merges indirect painting device into the canvas and commit the undo information.

The jobs of the stroke can demand special order of their execution. That is the way how they will be executed on a multi-core machine. Every job can be either of the type:

CONCURRENT

concurrent job may be executed in parallel with any other concurrent job of the stroke as well as with any update job executed by the scheduler

Example: in Scale Image action each job scales its own layer. All the jobs are executed in parallel.

SEQUENTIAL

if the job is sequential, no other job may interleave with this one. It means that when the scheduler encounters a sequential job, it waits until all the other stroke jobs are done, starts the sequential job and will not start any other job until this job is finished. Note that a sequential job can be executed in parallel with update jobs those merge layers and masks.

Example: All the jobs of the Freehand Tool are sequential because you cannot rearrange the painting of dabs. And more than that, you cannot mix the creation of the transaction with painting of anything on a canvas.

BARRIER

barrier jobs are special. They created to allow stroke jobs to synchronize with updates when needed. A barrier job works like a sequential one: it does not allow two stroke jobs to be executed simultaneously, but it has one significant addition. A barrier job will not start its execution until all the updates (those were requested with setDirty() calls before) has finished their execution. Such behavior is really useful for the case when you need to perform some action after the changes you requested in previous jobs are done and the projection of the image does now correspond the changes you’ve just done.

Example: in Scale Image action the signals of the image like sigSizeChanged should be emitted after all the work is done and all the updates are finished. So it runs as a barrier job. See KisProcessingApplicator class for details.

Besides one of the types above a job may be defined as EXCLUSIVE. Exclusive property makes the job to be executed on the scheduler exclusively. It means that there will be no other jobs (strokes or updates) executed in parallel to this one.

The queue of strokes

The strokes themselves are stored in a queue and executed one by one. This is important to know that any two jobs owned by different strokes cannot be executed simultaneously. That is the first job of a stroke starts its execution only after the last job of the previous stroke has finished.

The stroke is just a container for jobs. It stores some information about the work done, like id() and name(). Alongside storing this information it can affect the order of execution of jobs as well. The stroke can be defined exclusive. The meaning of this resembles the behavior of stroke job’s exclusive property. Exclusive stroke is a stroke that executes its jobs with all the updates blocked. The execution of updates will start only after the stroke is finished.

Implementation (practice)

Implementation of a stroke

Overview of stroke classes

Overview of stroke classes

The key class of the strokes’ execution is KisStrokesQueue. The most important method that is responsible for applying all the rules about interleaving of jobs mentioned above is KisStrokesQueue::processOneJob. This method is called by the update scheduler each time a free thread appears. First it gets the number of merge and stroke jobs currently executing in the updater context. Then it checks all the rules one by one.

Canceling and undo information trick

It was stated above that a stroke can be canceled in each moment of time. That happens when a user calls KisStroke::cancelStroke() method. When it is requested the stroke drops all the jobs those are present in its queue and has not been started yet. Then it enqueues a special kind of job named cancel job that reverts all the work done by the stroke. This is used for interactive canceling of tools’ strokes.

Taking into account that the strokes can be reverted, we cannot use QUndoStack capabilities directly. We should add commands to the stack after they have been executed. This resembles the way how KisTransactionData works: its first redo() method doesn’t do anything because everything has already been painted on a device. Here in strokes this “after-effect-addition” is implemented in general way. Strokes work with a special kind of undo adapter: KisPostExecutionUndoAdapter. This adapter wraps the commands in a special wrapper that puts them into the stack without calling redo() and controls their threaded undo() and redo() operations. See information about KisPostExecutionUndoAdapter in a separate document.

Queues balancing

So we ended up with a solution where our scheduler has two queues that it should spread between limited amount of threads. Of course there should be some algorithm that balances the queues. Ideally, we should balance them by the total area of image the queue should process. But we cannot achieve that currently. So the formula for size metrics is quite simple:

updatesMetric = <number of update jobs in the queue>

strokesMetric = <number of strokes> * <jobs in the first stroke>

Balancing formula:

balancingRatio = <updatesMetric> / <strokesMetric>

Starting a stroke

The main entry point to strokes for the user is KisStrokesFacade interface. This interfaces provides four methods: startStroke(), addJob(), endStroke() and cancelStroke(). So every time you work with strokes you should work using this interface.

Note: KisImage and KisUpdateScheduler both implement this interface, so you can use them as a strokes facade. But please try not to store pointers to the whole image. Try store a link to interface only, if possible.

So if you want to start a stroke you should do the following:

  1. Create a stroke strategy

  2. Start a stroke with:

    KisStrokeId strokeId = strokesFacade->startStroke(myStrategy);

    Note: you’ll get a KisStrokeId handle for the stroke you created. This handle will be used in all the other methods for controlling the stroke. This handle is introduced, because several users can access the strokes facade simultaneously, so there may be several strokes opened simultaneously. It’s important to understand that even when several strokes are opened simultaneously, only one of them executes on the cpu. All the other strokes will be delayed until it is finished.

  3. Create a data for your stroke job

  4. Add a job to the execution queue:

    strokesFacade->addJob(strokeId, myData);

  5. You may add as many jobs as you wish

  6. End or cancel the stroke:

    strokesFacade->endStroke(strokeId);

    or

    strokesFacade->cancelStroke(strokeId);

Strokes public API

Simplified stroke classes

As you might noticed the internal strokes API is quite complex. If you decide to create your own stroke you need to create at least six new classes:

  • stroke strategy class

  • four stroke jobs strategies (init, finish, cancel, dab)

  • data that will be passes to a dab-strategy-based job

That is not really a good solution for a public API, so we introduced an adapter that simplifies all these stuff. The class is called KisSimpleStrokeStrategy. It allows you to define all the jobs you need in a single class.

Simple stroke classes

Simple stroke classes

One of the benefits of using the strokes is that you are able to test them separately from the UI using a common infrastructure.

utils::StrokeTester class

That is a really simple class that you can use to test your own stroke. It test the following aspects of your stroke:

  • canceling of the stroke

  • working with indirect painting activated

  • testing updates of the image projection after your stroke

  • working with a layer that is not connected to any image

The result of the execution is compared against the reference png files those you create manually while writing your test.

How to write your own test

You can check examples in MoveStrokeTest and FreehandStrokeTest tests.

  1. You need to inherit your tester class from utils::StrokeTester. The constructor of that class accepts the name of your stroke (it’ll be used for generating filenames), size of the image and a filename of the preset for the paintOp.

    StrokeTester(const QString &name, const QSize &imageSize,
                 const QString &presetFileName = "autobrush_300px.kpp");
    
  2. Then you need to override at least two methods:

    KisStrokeStrategy* createStroke(bool indirectPainting,
                                    KisResourcesSnapshotSP resources,
                                    KisPainter *painter,
                                    KisImageWSP image);
    void addPaintingJobs(KisImageWSP image,
                         KisResourcesSnapshotSP resources,
                         KisPainter *painter);
    

    If you thing you need it you may do some corrections for the image and active node in the following method:

    void initImage(KisImageWSP image, KisNodeSP activeNode);
    
  3. Run your test in a testing slot:

    void MyStrokeTest::testStroke()
    {
        MyTester tester();
        tester.test();
    }
    
  4. During the first run the test will report you many fails and will generate you several files with actual result of the test. You need to check these files, then move them into the tests’ data folder: tests/data/<your_stroke_name>/

  5. After you copied the files the tester will compare the actual result against these very files. That means it’ll catch all the changes in the work of your stroke, so you’ll be able to catch all the regressions automatically.

Predefined classes for usage as base classes

KisPainterBasedStrokeStrategy

This class can be used for the strokes those work with the node using a painter (or painters like in KisToolMultihand). This class accepts resources snapshot (KisResourcesSnapshot) and a painter (painters). Initialization, finishing and canceling callbacks of this class do all the work for dealing with indirect painting support, creation of transaction, reverting the stroke on canceling. This base class is used for FreehandStroke mostly.

KisStrokeStrategyUndoCommandBased

It is obvious from the name of the class that it works with undo commands. In constructor you define which method of undo command should be used undo() or redo(). Afterwards, you just add commands to the stroke and they are executed with any the sequentiality constraints. This stroke strategy does all the work for adding the commands to the undo adapter and for canceling them if needed.

Example classes

  • KisPainterBasedStrokeStrategy

  • FreehandStrokeStrategy

  • KisStrokeStrategyUndoCommandBased

  • MoveStrokeStrategy

Internals of the freehand tool

Freehand tool classes

Freehand tool classes

We need to share the codebase between at least four classes: KisToolFreehand, KisToolMultihand, KisScratchPad. All these classes paint on a canvas with KisPainter, so they share quite much common code.

KisResourcesSnapshot

After we introduced the strokes, the moments of time when user paints with mouse and when the line is actually painted on the canvas do not coincide. It means that by the time a thread starts actual changing the device, the contents of KoCanvasResourceProvider might have already changed. So before we start a stroke we should create a snapshot of all the resources we have and pass this snapshot to the stroke.

For this purpose we introduced KisResourcesSnapshot class. It solves two problems at the same time: first it stores all the resources we might have and second it encapsulates the algorithm of loading these resources into a KisPainter object. So this class is really easy to use. You just create the snapshot and then just load all the resources to the painter when needed.

KisResourcesSnapshotSP resources =
    new KisResourcesSnapshot(image,
                             undoAdapter,
                             resourceManager);
KisPainter painter;
painter.begin(device, selection);
resources->setupPainter(&painter);

// paint something

painter.end();

In our implementation this class is usually created by KisToolFreehandHelper and passed to the KisPainterBasedStrokeStrategy class. The latter one creates painters and initializes them using setupPainter().

KisToolFreehand and KisScratchPad

The freehand tool is split into four classes:

KisToolFreehand

highlevel tool class that get the mouse events from the Ko-classes and distributes events among internal classes.

KisToolPaintingInformationBuilder

converts mouse events represented by KoPointerEvent objects into KisPaintInformation objects.

KisRecordingAdapter

stays in charge of adding recording information into the image’s action recorder. This class has two purposes: first we need to be able to disable recording for the scratch pad (then we just pass NULL instead of a recording adapter), second when the strokes are able to do their own recording, it’ll be easier to port the freehand tool to it.

KisToolFreehandHelper

this is the main class that combines all the classes we were talking above. It accepts a mouse event, converts it using a painting information builder into the paint information, notifies recording adapter, takes the snapshot of resources and finally starts a stroke. Then it populates the stroke with stroke jobs, when the user moves the mouse (paint(event) method) and finishes the stroke in the end.

Such splitting allows us to use the same classes in both KisToolFreehand and KisScratchPad. The only difference between them is that the scratch pad doesn’t have a recording adapter at all, and uses base class KisPaintingInformationBuilder instead of KisToolPaintingInformationBuilder. The latter differs from the former one in a way that it supports painting assistants (adjustDocumentPoint() method), complex coordinate transformations with KisCoordinatesConverter (documentToImage() method) and perspective painting (calculatePerspective() method). The rest of the code is shared.

KisToolMultihand

Multihand tool uses the same classes. The only difference, it has a couple of modifications in its helper (KisToolMultihandHelper), those allow it to have several painters at the same time. The tool’s class inherits the freehand tool’s class and just substitutes the helper with its own (with resetHelper() method).

Scheduled Undo/Redo

Two ways of working with undo commands

The key problem of designing the undo system for strokes was that there are two ways of working with undo commands. That is we have two types of commands actually:

  • Qt-like command - command’s redo() method is executed while the command is added into the undo stack

  • Transaction-like command - the command is added to the stack after its action has already been performed. It means that the first redo() of this command (the one that is called by undo stack) does nothing. That is a transaction-like command just saves undo data for the future and does not perform anything on addition.

You already know that our strokes can be reverted on the go, it means that the stroke’s undo command should be added to the undo stack only after all the actions of the stroke have been performed. So it looks like the stroke’s commands are transaction-like.

But there is another problem: the stroke should be able to execute regular undo commands those are not transaction-like (like is it done in KisStrokeStrategyUndoCommand). More than that, undo and redo of for such strokes should be performed with the same sequentiality properties (read “undo/redo operations should be threaded as well”).

It follows that the undo commands generated by the stroke should be wrapped in a special wrapper command, lets call it KisSavedCommand, that hold the following properties:

  • the wrapper skips the first redo(). It means the wrapped command’s redo() method will not be called on its addition to the stack. Obviously, it is not needed, because the action has already been performed by the stroke itself.

  • when undo stack calls to undo/redo methods of the wrapper-command, the command creates a stroke (KisStrokeStrategyUndoCommandBased) and runs the wrapped command in a context of this stroke.

  • a special macro wrapper command, lets call is KisSavedMacroCommand, should be able to save all the commands executed by a stroke and undo/redo all of them in the original order with original sequentiality properties (concurrent, sequential, barrier, exclusive).

That is exactly what we have: KisSavedUndoCommand skips the first redo and runs undo()/redo() of an internal command in a separate stroke. We have KisSavedMacroCommand as well to save the contents of the whole stroke.

Scheduled commands

Scheduled commands

Well, it would be quite insane to ask all the users of strokes to wrap their commands into wrapper, so we introduced a separate undo adapter for strokes: KisPostExecutionUndoAdapter. This adapter wraps your command and puts it into the undo stack automatically. This is the only adapter we can use inside strokes, that is why all the strokes accept the pointer to it.

For the legacy code we still have KisUndoAdapter, but now we call it “legacy undo adapter”. It works as usual: it adds a command to undo stack directly, so it gets executed right in the moment of addition. But there still is one trick. Stroke’s commands come to the undo stack asynchronously, so if we try to simply add a command to the stack, we can catch a race condition easily. That’s why the legacy undo adapter must guard itself from strokes with locking the strokes system. That is done with a special kind of lock barrierLock(). This barrier lock differs from a regular lock in a way that it ways for all the running strokes are finished, while a regular lock waits for all the running stroke jobs are done. That’s the only difference.

The same race conditions problem applies to the undo()/redo() signals from the UI. The user may request the undo operation while the stroke is adding its commands. This will surely lead to a crash. We solved this problem in a bit hacky way: we hacked QUndoStack and made it’s undo()/redo() slots virtual. After that we overridden the stack with our own, and changed these methods to block the strokes while undo()/redo() is happening. We use tryBarrierLock() there, because it is easier to cancel the undo than to wait until all the strokes are finished.

Undo Adapters and Undo Stores

Well, we have two types of undo adapters now (not counting KisSurrrogateUndoAdapter). It’s obvious that they should share some code. That is why we split the work with the actual undo stack into a separate class KisUndoStore. So now the undo store defines “where to store the undo data”, and undo adapter defines “how to adapt krita’s commands to qt’s stack”. There are additional types of store classes for using in tests and for special purposes.

Undo Adapter vs Undo Store

Undo Adapter vs Undo Store

Motivation

In Krita we have many actions which have common structure of execution. Take a look at actions like Scale Image, Rotate Image, Change Color Space - all of them have common phases:

  1. Lock the image

  2. Do the processing of nodes

  3. Unlock the image

  4. Emit setDirty() calls and update the projection of the nodes

  5. Wait until all the setDirty()’es are finished

  6. Emit image’s signals like sigImageSizeChanged

More than that, you should pay attention to the fact that all these actions should support undo/redo operations. And the last two phases cannot be implemented as usual qt-commands inside a usual macro, because they should always be executed in the end of the action (in qt commands are executed in reverse order during undo operations, that is not what we want).

And, btw, it would be really good idea to have multithreading support for such actions, because some of them (like Scale Image) may be quite slow.

KisNodeVisitor cannot fit all these requirements, because it has important design limitations: first, walking through nodes is implemented inside the visitor itself and, second, emitting signals is put into visitors as well. These two limitations prevent the code to be shared between actions. That is why we introduced new shiny KisProcessingVisitor and a separate framework for them.

Processing visitors

Processing framework

Processing framework

The applicator is based on the “undo command”-based stroke (KisStrokeStrategyUndoCommandBased). It starts the stroke in the constructor and adds undo commands to it on every user request. The processings are inernally wrapped into a special command (KisProcessingCommand). This command has its own undo stack that collects the transactions executed by the processing. This can be easily achieved with our undo adapters interface. The command just defines its own KisSurrogateUndoAdapter and passes it to the processing. Processing adds its transactions to the fake adapter. And later, the command just uses the undo stack to undo/redo actions executed by the transaction.

The applicator defines several internal commands as well: UpdateCommand and EmitSignalsCommand. These commands are added to the beginning and to the end of every stroke, so that they can be executed in the end of both undo and redo operations. The parameter finalUpdate controls whether the command is executed during its redo() or undo() operation.

Emission of signals trick

After actions have been moved to separate threads, problems with image signals appeared. When everything was executed in a single thread the connection of signals like sigAboutToAddNode and sigNodeHasBeenAdded worked as Qt::DirectConnection. So these signals were effectively function calls. After we moved the actions to a separate thread, all of them became Qt::QueuedConnection. I guess you know what it means. They simply lost all their sense. So we had to start to use Qt::BlockingQueuedConnection. But there is another problem with it. Some of the (old) code is still executed in a context of the UI thread and they emit signals as well. So all that code causes deadlocks when using Qt::BlockingQueuedConnection. That is why we had to introduce KisImageSignalRouter. This class checks which thread emits the signal and emits it either using Qt::DirectConnection or Qt::BlockingQueuedConnection. So no deadlocks are possible.

Progress reporting

The fact that a processing visitor does a really simple task (processes a single node) that is very easy to report progress using progress bars in the layer box. We just need to use progress proxy of the node we process (KisNodeProgressProxy). Our processings framework provides an even easier way of doing this. You just need to instantiate a ProgressHelper object and ask it to create a KoUpdater object for you. And all is done. You can see an example in KisTransformProcessingVisitor class.

Testing

Usage of a common framework makes testing really simple. There is a separate unittest in image’s tests folder: KisProcessingsTest. To test a processing you need to write just a couple of lines. Everything is done by BaseProcessingTest helper class. This class will run your processing and compare results against reference png files those are stored in data folder. If there are some problems found, it’ll dump result files to the current directory.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文