FR: Creation of the OpenGL LowLevelGraphicsContext as an interface

I want to customize the OpenGL implementation of LowLevelGraphicsContext and the drawing of the components.

Unfortunately OpenGLRenderer and ::renderOpenGL() is not an option, since the resulting changes would be too drastic and also it’s too much work. Most of the stuff like OpenGLTexture and OpenGLFrameBuffer are already there and work fine. No need to reinvent the wheel! It’s only about the specific shader implementation and the goal is to use it for normal component drawing.

Some information in advance, so we get on the same page.

JUCE\modules\juce_opengl\opengl\juce_OpenGLContext.cpp

void paintComponent()
    {
    ...
    {
    	std::unique_ptr<LowLevelGraphicsContext> g (createOpenGLGraphicsContext (context, cachedImageFrameBuffer));
    	g->clipToRectangleList (invalid);
    	g->addTransform (transform);

    	paintOwner (*g);
    	JUCE_CHECK_OPENGL_ERROR
    }
    ...
    }

Here paintOwner(g) will later call component.paintEntireComponent (g, false);
and essentially paint the entire component tree, ending in void paint(Graphics& g);

That is the important place. The call to …

std::unique_ptr createOpenGLGraphicsContext (OpenGLContext&, OpenGLFrameBuffer&);

… will return the hidden implementation of the ShaderContext defined in
JUCE\modules\juce_opengl\opengl\juce_OpenGLGraphicsContext.cpp

=========

The PLAN.

Rip out, extend or customize the existing context and add more shader functionality and batching!

I’m aware that we can’t change the Graphics object to add features like:

// Draw Image within 256 pixel area, applying a blur shader with 20 px radius.
g.drawImageWithShader(image, Rectangle<int>(0, 0, 256, 256), BlurShader(20));

But how about another approach. In the end it will lead up to something like this:

void paint(Graphics& g)
{

	/** Get the hidden implementation, which is now the customized
		low level context and can be easily cast. */
	LowLevelGraphicsContext& lowLevelGraphicsContext = g.getInternalContext();

	if(auto context = dynamic_cast<CustomOpenGLGraphicsContext*>(&lowLevelGraphicsContext))
	{
		auto blurRadius = 20.0;
		
		BlurShade shader(blurRadius);
		
		shader.drawImage(context, image, Rectangle<int>(0, 0, 256, 256));
	}
}

Since CustomOpenGLGraphicsContext is our own and known implementation, its possible
to add more GLSL programs to ShaderPrograms, add custom texture sources an set up
the shader with other vertex buffers. An example…

struct Vertex
{
	int16 x;
	int16 y;

	float s;
	float t;

	uint32 rgba;
};

struct Quadrilateral
{
	enum
	{
		numVertices = 4
	};
	
	// Position X,Y
	void setPositionCoordinates(float x, float y, float w, float h) noexcept
	{
		vertices[0].x = vertices[2].x = static_cast<int16>(roundToInt(x));
		vertices[0].y = vertices[1].y = static_cast<int16>(roundToInt(y));
		vertices[1].x = vertices[3].x = static_cast<int16>(roundToInt(x + w));
		vertices[2].y = vertices[3].y = static_cast<int16>(roundToInt(y + h));
	}
	
	// Texture S,T
	void setTextureCoordinates(float x, float y, float w, float h) noexcept
	{
		vertices[0].s = vertices[2].s = x;
		vertices[0].t = vertices[1].t = y;
		vertices[1].s = vertices[3].s = x + w; 
		vertices[2].t = vertices[3].t = y + h;
	}
	
	...
	
	Vertex vertices[numVertices];
};


class QuadrilateralBatch
{
	...

	void addQuad(float sx, float sy, float sw, float sh, float dx, float dy, float dw, float dh, Colour colour = Colours::white) noexcept
	{
		auto& quad = enqueueQuad();

		quad.setPositionCoordinates(dx, dy, dw, dh);
		quad.setTextureCoordinates(sx, sy, sw, sh);
		quad.setColour(colour);
	}
	
	...
};

void paint(Graphics& g) override
{
	/**
		Still getting and casting the internal context implementation.
		auto context = dynamic_cast<CustomOpenGLGraphicsContext*>(&g.getInternalContext());
		...
	*/
	QuadrilateralBatch batch(g);
	
	batch.setTexture(image);

	// Probably thousands !
	for(...)
	{
		batch.add(Rectangle<float>(0, 0, 256, 256));
	}
	
	batch.flush();
}

With this it’s possible to draw thousands of images withouth the heavy overhead of the existing context implementation. I mean the splitting of one image into hundreds of vertices and one draw call for each image.

=========

Now. How do we do this? There are multiple ways, but it would be enough if we give OpenGLContext a new method that will optionally create the LowLevelGraphicsContext for us.

OpenGLContext openGLContext;
openGLContext.setCustomGraphicsContextCreator(creator)
...
// The creator implements it. Or alternatively via lamda ? 
std::unique_ptr<LowLevelGraphicsContext> createCustomContext(OpenGLContext& context, OpenGLFrameBuffer& frameBuffer)
{
	return std::make_unique<MyCustomContext>(context, frameBuffer);
}

Then.

JUCE\modules\juce_opengl\opengl\juce_OpenGLContext.cpp

void paintComponent()
{
...
{
	auto creator = getCustomContextCreator();
	
	std::unique_ptr<LowLevelGraphicsContext> g(creator.getNewContext(context, cachedImageFrameBuffer));
	
	// Fall back to JUCE default.
	if(g.get() == nullptr)
		g = createOpenGLGraphicsContext (context, cachedImageFrameBuffer);
	

	g->clipToRectangleList (invalid);
	g->addTransform (transform);

	paintOwner (*g);
	JUCE_CHECK_OPENGL_ERROR
}
...
}

Works and doesn’t expose too much. It’s really just replacing a small part of the whole OpenGL chain. Without changing any of the tedious setup process we got our own OpenGLGraphicsContext implementation and can cast it later to do more advanced stuff.

=========

Is this a good idea? Am I overlooking something and it is already possible somehow by overriding something in Component? Please tell me if.

If its not relevant enough, since we will eventually get Vulkan or Metal. How about the general idea of it? To name it: I’m fine leaving the OS specifc background setup to JUCE. But eventually we want and need more access to shader concepts and vertex buffers. They are kind of fundament for all kinds of visual effects that will probably
never be included into the regular Graphics class.

Opinions? Would you use it? Have you considered something similar or even already implemented it or a workaround? Thanks for any thoughts!

1 Like

tl;dr

Feature Request:
Implement the creation of the OpenGL LowLevelGraphicsContext as an interface.

====

JUCE/modules/juce_opengl/opengl/juce_OpenGLGraphicsContext.h

std::unique_ptr< LowLevelGraphicsContext > createOpenGLGraphicsContext (OpenGLContext &, unsigned int frameBufferID, int width, int height);

Instead of:

JUCE/modules/juce_opengl/opengl/juce_OpenGLGraphicsContext.cpp
std::unique_ptr<LowLevelGraphicsContext> createOpenGLGraphicsContext (OpenGLContext& context, unsigned int frameBufferID, int width, int height)
{
    return OpenGLRendering::createOpenGLContext (OpenGLRendering::Target (context, frameBufferID, width, height));
}

to something like this:

std::unique_ptr<LowLevelGraphicsContext> createOpenGLGraphicsContext (OpenGLContext& context, unsigned int frameBufferID, int width, int height)
{

	const OpenGLContextCreator& contextCreator = context.getDefaultContextCreator();
    return contextCreator.createOpenGLContext (context, frameBufferID, width, height);
}

So we can implement this interface:

class OpenGLContextCreator
{
public:
	OpenGLContextCreator() = default;
	virtual ~OpenGLContextCreator() = default;
	
	virtual std::unique_ptr<LowLevelGraphicsContext> createOpenGLGraphicsContext (OpenGLContext &, int width, int height) const = 0;
	
	virtual std::unique_ptr<LowLevelGraphicsContext> createOpenGLGraphicsContext (OpenGLContext &, OpenGLFrameBuffer &) const = 0;
	
	virtual std::unique_ptr< LowLevelGraphicsContext> createOpenGLGraphicsContext (OpenGLContext &, unsigned int frameBufferID, int width, int height) const = 0;
};

And then set it to the corresponding OpenGLContext like this:

class MyCustomContextCreator : public OpenGLContextCreator
{
	...
};

OpenGLContext context;
conext.setDefaultContextCreator(new MyCustomContextCreator());

Yes. Take my upvote, pal.