GSoC: Krita AVX mask optimizations, setting up the environment.

Hi! GSoC student here :]. This first weeks coding for Krita have been so busy I forgot to write about them. So I’ll start to sum everything up in short posts about each step of the project implementation process.

First Steps, setting up a dev environment

I followed the steps in the 3rdparty to compile the base krita system on OSX. This easy to follow instructions helped me get a basic Krita installation in a short time. However not everything worked for me quite easily and most tests did not work or run at all on OSX with the message.

QFATAL : FreehandStrokeBenchmark::testDefaultTip() Cannot calculate the bundle path from the app path

After some digging I found out that no program that uses a GUI can run outside of an app bundle. So while not a future proof, to start working on the code I made a quick script to install the tests I’m interested inside Krita.app folder. To allow tests to run. By default all tests are linked to libraries in the build dir, but because this wont work on OSX one approach would be to install also the tests in the bundle and link to the install libraries or, another approach could be to generate an app bundle for each test.

In any case the tests could run so It was time to start working on the unit test.

Implementing Mask Similarity Test

Phabricator task: Base unit test kis_mask_similarity_test
This unit test intention is to compare the correctness of the new vectorized mask rendering by comparing it to the same settings Mask produced by the previous engine. The new versions have to be as identical as possible to ensure the painting effects the user is expecting does not change between engines (The user can’t change how the mask is produced, but we use the scalar version for smaller brush dab sizes).

In this sense the Test had to create a maskGenerator, duplicate it, make one use the vectorized engine and the other the scalar version. In short this is done with the following code.

  QRect bounds(0,0,500,500);
  KisCircleMaskGenerator circVectr(500, 1.0, 0.5, 0.5, 2, true);
  KisCircleMaskGenerator circScalar(circVectr);

  // Force usage of scalar backend
  circScalar.resetMaskApplicator(true);
  KisMaskSimilarityTester(circScalar.applicator(),
                          circVectr.applicator(), bounds);

For this we had to implement the method resetMaskApplicatorwhich resets the mask applicator engine to Scalar if the input boolean is true. The method itself is just a wrapper that return a Scalar Factory if the bool is true, if not it uses the old function.

  template<class FactoryType>
  typename FactoryType::ReturnType
  createOptimizedClass(typename FactoryType::ParamType param,
                       bool forceScalarImplemetation)
  {
    if(forceScalarImplemetation){
      return FactoryType::template create<Vc::ScalarImpl>(param);
    }
    return createOptimizedClass<FactoryType>(param);
  }

This ensures the mask created with both engines is generated from the same parameters and reduces the variability only to the output of each engine.

Comparing masks

Mask rendering problem


After generating the masks we need a way to compare them, and because the mask is in essence a map for color application, the best way to compare the effective similarity of both implementations is to generate an image from the mask. After that we can compare the images to look for differences.

At first I decided to allow a very small percentage of the image to be different (about .03%) but this turned out to be a problem. While in theory it sounds ok, since all values are generated from similar logic, in practice it was a disaster. Accepting even one pixel difference could result in letting pass as OK a mask with a completely masked value in the middle (which happened). So a best strategy is to allow a small deviation but do not allow any difference outside of that deviation.

Mask minimal difference
Even one pixel ruins the mask

To test for difference in the images produced by the mask we used the function compareQImages. The three numerical arguments in the end determine how the test will behave. The first number two numbers define the fuzziness for considering something similar or not, corresponding to color channel and alpha channel, because the mask information is stored in the Alpha channel we just set the alpha fuzziness. The las argument will determine the number of differences allowed.

QVERIFY(
    TestUtil::compareQImages(tmpPt,scalarImage,vectorImage,
                             0, 2, 0));

Things to improve

The test works ok to test the similarity of one particular mask variation, which is fine for the initial implementation. however it will be needed a way to test more mask shape variations. Specifically changing the softness and mask ratio.

Things to consider:
+ Produce reference images to compare to: Images would have to be generated before hand and shipped with the code. This is to protect any future modification to alter the looks of the mask. The static mask would need to be compared for both models, the vectorized and the scalar one.
+ Mask Deformations: Masks can be generated with different proportions and softness values. Some combinations could generate differences if the implementation is wrong. We could iterate on every single combination, this is effective but costly. Another possibility is to identify what values are more likely to introduce errors, and only test for those cases.

Next: Implementing circular Gauss Mask optimization!

With the basics of the test ready, its time to begin implementation of a mask generator. We will start with Gauss Mask as I have some code I did for the proposal. And we can use this to see the test is working properly in this case.

********* Start testing of KisMaskSimilarityTest *********
Config: Using QtTest library 5.10.0, Qt 5.10.0 (x86_64-little_endian-lp64 shared (dynamic) release build; by Clang 9.0.0 (clang-900.0.39.2) (Apple))
PASS   : KisMaskSimilarityTest::initTestCase()
PASS   : KisMaskSimilarityTest::testCircleMask()
QPoint(50,13) source 0 0 0 169 dest 0 0 0 171 fuzzy 0 fuzzyAlpha 1 ( 1 of 4 allowed )
QDEBUG : KisMaskSimilarityTest::testGaussCircleMask()  Different at QPoint(33,15) source 0 0 0 84 dest 0 0 0 86 fuzzy 0 fuzzyAlpha 1 ( 2 of 4 allowed )
QDEBUG : KisMaskSimilarityTest::testGaussCircleMask()  Different at QPoint(67,15) source 0 0 0 84 dest 0 0 0 86 fuzzy 0 fuzzyAlpha 1 ( 3 of 4 allowed )
QDEBUG : KisMaskSimilarityTest::testGaussCircleMask()  Different at QPoint(33,85) source 0 0 0 84 dest 0 0 0 86 fuzzy 0 fuzzyAlpha 1 ( 4 of 4 allowed )
QDEBUG : KisMaskSimilarityTest::testGaussCircleMask()  Different at QPoint(67,85) source 0 0 0 84 dest 0 0 0 86 fuzzy 0 fuzzyAlpha 1 ( 5 of 4 allowed )
FAIL!  : KisMaskSimilarityTest::testGaussCircleMask()
'TestUtil::compareQImages(tmpPt,scalarImage, vectorImage, 0, 1, 4)' returned FALSE. ()

The differences are so big that even a simple look can tell both mask are different!

In the next post I will show how the I implemented the Gauss Mask Generator on Vc, what problems We found and how We solved them.

Until next time!

2 thoughts on “GSoC: Krita AVX mask optimizations, setting up the environment.

Leave a reply to Scott Petrovic Cancel reply