Image tiling bug with setFillType() on macOS (with workaround and proposed fix)

The bug:

I’m using an image to tile the background of a Component in its paint() method, like this:

g.setFillType (FillType (image, AffineTransform::scale (0.75f)));
g.fillAll ();

This worked as expected until we connected a new 2K display to a macMini, where it paints the background with these black lines around the tiles, which are completely unintended (the picture is very dark but if you look closely you will see both the tiles and the black lines):

The issue seems hardware/driver related to that specific display, because moving the very same window to another screen connected to the same macMini, yields a flawsless tiling there, i.e. without the black grid between tiles.

Workaround:

After countless hours of debugging, I’ve found that the following workaround works, for reasons that I’m about to explain in a moment:

g.addTransform (AffineTransform::scale (0.75f));
g.setFillType (FillType (image, AffineTransform()));
g.fillAll ();

I simply moved the transform outside of the FillType, applying it to the Graphics in advance.
This shouldn’t make a difference, but it does.

Explaination:

The actual tiling is performed by the following function in juce_mac_CoreGraphicsContext.mm:

void CoreGraphicsContext::drawImage (const Image& sourceImage, const AffineTransform& transform, bool fillEntireClipAsTiles)
{
    auto iw = sourceImage.getWidth();
    auto ih = sourceImage.getHeight();

    auto colourSpace = sourceImage.getFormat() == Image::PixelFormat::SingleChannel ? greyColourSpace.get()
                                                                                    : rgbColourSpace.get();
    auto image = detail::ImagePtr { CoreGraphicsPixelData::getCachedImageRef (sourceImage, colourSpace) };

    ScopedCGContextState scopedState (context.get());
    CGContextSetAlpha (context.get(), state->fillType.getOpacity());

    flip();
    applyTransform (AffineTransform::verticalFlip (ih).followedBy (transform));
    auto imageRect = CGRectMake (0, 0, iw, ih);

    if (fillEntireClipAsTiles)
    {
      #if JUCE_IOS
        CGContextDrawTiledImage (context.get(), imageRect, image.get());
      #else
        // There's a bug in CGContextDrawTiledImage that makes it incredibly slow
        // if it's doing a transformation - it's quicker to just draw lots of images manually
        if (&CGContextDrawTiledImage != nullptr && transform.isOnlyTranslation())
        {
            CGContextDrawTiledImage (context.get(), imageRect, image.get());
        }
        else
        {
            // Fallback to manually doing a tiled fill
            auto clip = CGRectIntegral (CGContextGetClipBoundingBox (context.get()));

            int x = 0, y = 0;
            while (x > clip.origin.x)   x -= iw;
            while (y > clip.origin.y)   y -= ih;

            auto right  = (int) (clip.origin.x + clip.size.width);
            auto bottom = (int) (clip.origin.y + clip.size.height);

            while (y < bottom)
            {
                for (int x2 = x; x2 < right; x2 += iw)
                    CGContextDrawImage (context.get(), CGRectMake (x2, y, iw, ih), image.get());

                y += ih;
            }
        }
      #endif
    }
    else
    {
        CGContextDrawImage (context.get(), imageRect, image.get());
    }
}

This is called with fillEntireClipAsTiles = true and transform is a reference to the AffineTransform that has been set for the FillType object.

When the FillType transform is a scale, as in my case, the “fallback” code kicks in, and it does the tiling “manually”. That produces the grid which, in my opinion, is caused by rounding errors at a lower level (hardware or driver).

My workaround factored the scale transform away from the FillType object and set it for the whole Graphics instead.
In this scenario, the FillType object passes only an identity transform as argument to the above function, with the result that the tiling is performed calling CGContextDrawTiledImage.
With that, the lines around the tiles disappear.

Proposed long term fix to the JUCE code:

In my opinion, a long term fix could be to remove the fallback code altogether:
That fallback code was initially necessary to support older macOS versions because CGContextDrawTiledImage has been introduced in 10.5, but now the minimum requirement is 10.7.

Also, it has been kept around to deal with the non-translations because of a performance hit that was present 10+ years ago, but I have tested with various combinations of scales, rotations and translations, and found no evidence of performance issues.

1 Like

Hi @yfede

I have been confronted to the same issue recently, and came to the same conclusion: Problem with image fill on MacOS

Your workaround is very clever and gets the job done, but in my case I would like to keep the fill type completely separated from the paint: I don’t want the paint method to have to go trough different properties to know if the fill type is a colour, or an image that has to be scaled, etc.

I hope the development team can have a look at this problem and get rid of the manual workaround if it is not needed anymore on currently supported systems.

2 Likes

Hi, yes, I have noticed your recent deep dive in rectangle drawing and I really appreciate that you found this issue as well, that gives a little more weight to my findings above.
I hope that raises the precedence of this issue in the todolist of the JUCE team.

Also, the resulting visual artifacts are very similar to what appears in the other topic: Int vs float rectangles with scaling: a proposal for Windows/MacOS consistency for JUCE 8 ,
but it seems to me that the problem described in this topic is of a different nature and doesn’t have anything to do with the int/float differences that come into play in that other topic. Do you confirm?

If so, then the issue described in this topic can be resolved for macOS, independently of what’s decided for the other one.

1 Like

I hope @reuk can chime in, as looking at the log he his the last person to have modified this particular method.

1 Like