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 will guarantee 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 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 methods of KisStrokeStrategy. These methods work like callbacks. They are called when the event happens.
So what is happening when the tool starts a stroke? The constructor of KisStroke creates an InitStrokeJob object who's processDab() function is connected to the initialization method of KisStrokeStrategy. Then the job goes to the queue and stays there unti the scheduler runs it. When the scheduler reaches the job, the following chain happens KisToolJob::run()-> KisDabProcessingStrategy::processDab()-> KisStrokeStrategy::initStroke().
The same happens for finishing and cancelling a stroke.
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 - defines the preparational actions those have to be done by the tool on init/end/cancel
- 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 and 4 will be shared by huge groups of tools so noone will have to override them manually.