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