Krita/Strokes Framework
Scheduler System
Requirements
- The scheduler should serialize stroke and merge actions. The order of actions accessing the same pixels should be total.
- The stroke action should be incrementally modifiable, that is KisToolFreehand should be able to add jobs (dabs) to the stroke during the painting. Such strokes should guarantee a total order of execution of internal jobs.
- There should be a special type of stroke that executes internal jobs in parallel. This can be used by KisToolFill, KisToolGradiant for processing different parts of the image in parallel.
- The stroke and all its jobs should be cancellable. On cancel event the entire transaction of the stroke should be reverted.
- [future] Internal jobs of the stroke may accept the current zoom-level of the image to allow implementation of the mipmapping in the future.
- [future] The scheduler may prioritize the actions according to the ROI.
Description
Diagrams
Queues and balancing
In the center of the system KisUpdateScheduler stays. Now it has two queues: one for setDirty requests and the other for the jobs requested by tools. The presence of two queues makes us use some kind of balancing between them. So on every call to processQueue() the update scheduler checks the amount of work in each queue, then tries to run all the jobs from more loaded one. If after this action, there are still some free threads present it tries to process the other queue.
The amount of work in the queue is measured in queueSizeMetric() method. It uses an aproximate formula:
foreach(const QRect &rect, allRects) { sum += (rect.width() + rect.height()) / 2; }
Job execution
The actual work of every painting tool is encapsulated into three objects: KisToolJob, KisDabProcessingStrategy and DabProcessingData.
KisToolJob this is a general object to be executed inside KisUpdateJobItem thread. It works as an adaptor here.
KisDabProcessingStrategy encapsulates real behavior of the tool. It has a method processDab() that is supposed to paint one dab on the image. The position and parameters of this dab are supposed to be stored in a data parameter and passed during a call.
DabProcessingData is the data that is passed to KisDabProcessingStrategy when it is requested to paint a dab. This class is supposed to be reimplemented by the tool.
There is a special isExclusive() parameter that can be set by the tool to any particular value. When the job is defined "exclusive", it means that the job will be the only object accessing the image during it's execution. All the other threads will be stopped. This parameter is quite significant for such tools as KisToolMove, because you cannot change the offset of a device during any iterator running on it, you'll get a crash otherwise.
Strokes
All the jobs are grouped into strokes. Each stroke has it's own transaction, indirect painting device (if requested) and a set of jobs (say, "dabs") to execute (paint). These jobs can be added to a stroke incrementally while the user is painting on the canvas.
The stroke can be defined "sequential". If so the scheduler guarantees that at each moment of time only one job will be running and they will execute in FIFO order. If the stroke is not sequential, then all it's jobs will be executed simultaneously in different threads. Such behavior is useful for simple tools like Gradient or Fill Tool.
The strokes are queued into KisStrokesQueue. It is important to understand that the jobs from different strokes cannot be executed simultaneously. That is the scheduler will guarantee that the jobs of the next stroke will not start before current stroke is finished.
The stroke can be defined "exclusive" as well. This property of the stroke will guarantee that this stroke is the only object who has access to the image. And no other threads (currently, updater threads) will access the image while the stroke is running.
The difference between KisStrokeStrategy::isExclusive() and KisDabProcessingStrategy::isExclusive(). When you define a job as "exclusive" it guarantees that no other jobs (from the same stroke even) and updater threads will be running in parallel with it. When you define a stroke as "exclusive", it will block updater threads from running for the entire period of time when the stroke is running. But this will not (!) prevent the jobs of that stroke to be running at the same time.
You can define a stroke as both "sequential" and "exclusive", but that will still not be the same as defining its jobs as "exclusive". The first solution will block updater threads for the whole period of running a stroke, the second will allow updater's jobs to interleave (but not intersect) with stroke's jobs. So the latter solution is used in tools to make them more respositive and the former one in non-gui actions like resizing image and changing image's colorspace.
The class KisStroke is not polymorphic. So all the traits of the tool's stroke should be configured with the corresponding methods of the strategy (KisStrokeStrategy).
Initialization, finishing and cancelling of a stroke
The behaviour of the stroke on all these events is defined in corresponding strategies created by KisStrokeStrategy. These strategies are added as jobs to the jobs queue when an even happens/
So what is happening when the tool starts a stroke? The constructor of KisStroke asks KisStrokeStrategy to create an InitStrokeJob. This job goes to queue and waits for its turn. Then the scheduler reaches the job and starts it. Its processDab() does the preparational work for the stroke. The default implementation creates indirect painting device and/or starts a transaction.
The same happens for finishing and cancelling a stroke.
Recorder Entry Point
KisStrokeStrategyFactory is a class used by the recording system to create the strategies when replaying the stroke. Technically, this class will be a simple typedef in most of the case. Read about Recording System for more information.
Summary
So if you want to create a new tool, you need to override maximum 6 classes:
- KoInteractionTool - a main class for the tool
- KoInteractionStrategy - defines the style of human interactions with the tool.
- KoInteractionStrategy::Factory - technical artefact
- KisStrokeStrategy - describes the stroke of the tool and works as a factory for init/finish/cancel strategies
- Init/Finish/Cancel stroke startegies - they are giong to be the same for most of the tools
- KisDabProcessingStrategy - defines the style of painting of the tool.
- KisDabProcessingStrategy::DabProcessingData technical class for explaining what to do to KisDabProcessingStrategy.
I think (hope) that the classes 2, 3, 4 and 5 will be shared by huge groups of tools so noone will have to override them manually.