Problem with image fill on MacOS

I get visible seams between image titles when setting a scaled image fill.
This does occur only with the default Mac renderer (Juce 7.0.11), not with openGL one or with any renderer on Windows.

This occurs when the scale factor multiplied by the image dimension is not an integer, or when additional scaling (eg resizing) is applied to the component.

class FillTest : public juce::Component {
public:
	FillTest() {
		//openGL.attachTo(*this);
		image.clear(image.getBounds(), juce::Colours::black);
	}
	
	void paint(juce::Graphics& g) override {
		g.fillAll(juce::Colours::white);
		g.setFillType({ image, juce::AffineTransform().scaled(.1f) });
		g.fillAll();
	}
private:
	juce::Image image { juce::Image::RGB, 1001, 1001, false };
	juce::OpenGLContext openGL;
};

Here is what it looks like:

Looks like a bug to me: if rounding has to occur it should take pixels inside the image (like other renderer presumably) rather that leaving seems around tiles.

Looks like the problem boils down to CoreGraphicsContext::drawImage implementation in juce_CoreGraphicsContext_mac.mm

Specifically this part:

    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,
        // but we might not be able to draw the images ourselves if the clipping region is not
        // finite
        const auto doCustomTiling = [&]
        {
            if (transform.isOnlyTranslation())
                return false;

            const auto bound = CGContextGetClipBoundingBox (context.get());

            if (CGRectIsNull (bound))
                return false;

            const auto clip = CGRectIntegral (bound);

            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;
            }

            return true;
        };

        if (! doCustomTiling())
            CGContextDrawTiledImage (context.get(), imageRect, image.get());
      #endif
    }

When CGContextDrawTiledImage is used the filling is perfect (no seams and no overlap, which can be tested by setting the fill color to black.withAlpha(0.5f)), but it is replaced with manual image draw for the reason stated in the comment (bug/slowdown in CGContextDrawTiledImage with scaled images).

Running a few crude benchmarks on my machine (Intel i3 mac mini running Big Sur) it looks like the native code runs just fine (increased GPU time compared to the manual approach, but it does not look like it might become a bottleneck in practice) and produces correct results. Is there still a rational to keep a manual workaround today?

EDIT: last paragraph

1 Like