Vulkan Modules for JUCE | 0.5.0 beta now on Github!

Quadrilateral Data structure to hold simple sprites.


struct Quadrilateral final
{
	/** Vertex */
	struct Vertex final
	{
		float x, y; // vk::Format::eR32G32Sfloat
		
		float s, t; // vk::Format::eR32G32Sfloat

		uint32_t colour; // vk::Format::eA8B8G8R8UnormPack32
	};

	enum
	{
		numVertices = 4
	};

	//==============================================================================
	// Position X,Y
	forcedinline void setPosition(float x, float y, float w, float h) noexcept
	{
		vertices[0].x = vertices[2].x = x;
		vertices[0].y = vertices[1].y = y;
		vertices[1].x = vertices[3].x = x + w;
		vertices[2].y = vertices[3].y = y + h;
	}

	forcedinline void setPosition(juce::Point<float> position, juce::Point<float> extent) noexcept
	{
		setPosition(position.x, position.y, extent.x, extent.y);
	}

	forcedinline void setPosition(juce::Rectangle<float> area) noexcept
	{
		setPosition(area.getX(), area.getY(), area.getWidth(), area.getHeight());
	}
		
	//==============================================================================
	// Texture S,T
	forcedinline void setTextureCoordinates(float x, float y, float w, float h) noexcept
	{
		vertices[0].s = vertices[2].s = x; // A, C
		vertices[0].t = vertices[1].t = y; // A, B
		vertices[1].s = vertices[3].s = x + w; // B, D
		vertices[2].t = vertices[3].t = y + h; // C, D
	}

	forcedinline void setTextureCoordinates(juce::Point<float> position, juce::Point<float> extent) noexcept
	{
		setTextureCoordinates(position.x, position.y, extent.x, extent.y);
	}

	// Texture S,T | But rotated by 90 degree clockwise
	forcedinline void setTextureCoordinatesRotated(float x, float y, float w, float h) noexcept
	{
		vertices[0].s = vertices[1].s = x;
		vertices[0].t = vertices[2].t = y;
		vertices[2].s = vertices[3].s = x + w;
		vertices[1].t = vertices[3].t = y + h;
	}

	forcedinline void setTextureCoordinatesRotated(juce::Point<float> position, juce::Point<float> extent) noexcept
	{
		setTextureCoordinatesRotated(position.x, position.y, extent.x, extent.y);
	}

	// Texture S,T | Rotate clockwise by 90 degrees
	void rotateTextureCoordinates() noexcept
	{
		vertices[1].s = vertices[0].s;
		vertices[2].t = vertices[0].t;
		vertices[2].s = vertices[3].s;
		vertices[1].t = vertices[3].t;
	}

	void flipTextureCoordinatesHorizontal(bool isRotated = false) noexcept
	{
		if (isRotated)
		{
			const auto t = vertices[0].t;
			vertices[0].t = vertices[2].t = vertices[1].t;
			vertices[1].t = vertices[3].t = t;
		}
		else
		{
			const auto s = vertices[0].s;
			vertices[0].s = vertices[2].s = vertices[1].s;
			vertices[1].s = vertices[3].s = s;
		}	
	}

	void flipTextureCoordinatesVertical(bool isRotated = false) noexcept
	{
		if (isRotated)
		{
			const auto s = vertices[0].s;
			vertices[0].s = vertices[1].s = vertices[2].s;
			vertices[2].s = vertices[3].s = s;
		}
		else
		{
			const auto t = vertices[0].t;
			vertices[0].t = vertices[1].t = vertices[2].t;
			vertices[2].t = vertices[3].t = t;
		}
	}

	forcedinline void scaleTextureCoordinates(float sx, float sy) noexcept
	{
		for (auto& vertex : vertices)
		{
			vertex.s *= sx;
			vertex.t *= sy;
		}
	}

	forcedinline void scaleTextureCoordinates(juce::Point<float> scale) noexcept
	{
		scaleTextureCoordinates(scale.x, scale.y);
	}

	forcedinline void applyTexelScale(int textureWidth, int textureHeight) noexcept
	{
		scaleTextureCoordinates(1.0f / static_cast<float>(textureWidth), 1.0f / static_cast<float>(textureHeight));
	}

	//==============================================================================
	// Colour
	forcedinline void setColour(uint32_t colour) noexcept
	{
		for (auto& vertex : vertices)
			vertex.colour = colour;
	}

	forcedinline void setColour(juce::Colour colour) noexcept
	{
		const auto packedColour = VulkanConversion::toPackedColour(colour.getPixelARGB());
		setColour(packedColour);
	}

	forcedinline void setColour(float red, float green, float blue, float alpha) noexcept
	{
		const auto packedColour = juce::Colour::fromFloatRGBA(red, green, blue, alpha).getARGB();
		setColour(packedColour);
	}

	Vertex vertices[numVertices];
};

Textured.vert - Vertex Shader to draw textured quads.

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec2 position;
layout(location = 1) in vec2 imagePos;
layout(location = 2) in vec4 colour;

layout(push_constant) uniform PushConsts {
	vec4 screenBounds;
	float matrix[6];
} pc;

layout(location = 0) out vec4 frontColour;
layout(location = 1) out vec2 texturePos;

void main() {
	frontColour = colour;
	texturePos = imagePos;

	mat2 transform = mat2 (pc.matrix[0], pc.matrix[3], pc.matrix[1], pc.matrix[4]);
	vec2 offset = vec2 (pc.matrix[2], pc.matrix[5]);

	vec2 newPos = transform * position + offset;
	vec2 adjustedPos = newPos - pc.screenBounds.xy;

	vec2 scaledPos = adjustedPos / pc.screenBounds.zw;
	gl_Position = vec4 (scaledPos.x - 1.0, 1.0 - scaledPos.y, 0, 1.0);
}

Textured.frag - Fragment shader to draw textured quads.

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec4 frontColour;
layout(location = 1) in vec2 texturePos;

layout(binding = 0) uniform sampler2D imageTexture;

layout(location = 0) out vec4 outColour;

void main() { 
	outColour = frontColour.a * texture (imageTexture, texturePos); 
}

Class Declaration for the custom context.

class VulkanGraphics
{
public:
    VulkanGraphics();
    virtual ~VulkanGraphics();

    float getPhysicalPixelScaleFactor() const noexcept { return physicalPixelScaleFactor; }

    void begin(juce::Graphics& g);

	void end(juce::Graphics& g);

    void setTransform(const juce::AffineTransform& newTransform);

    void drawQuadrilateral(const juce::Image& image, const Quadrilateral& quad);

	void drawQuadrilaterals(const juce::Image& image, const juce::Array<Quadrilateral>& quads);

private:
	struct Implementation;
	std::unique_ptr<Implementation> implementationHolder;

    Implementation& implementation;

    float physicalPixelScaleFactor = 1.0f;

	JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VulkanGraphics)
};

Later you could use it in a component like this:

void paint(Graphics& g) override
{
  VulkanGraphics vkg;

  vkg.begin(g);

  vkg.drawQuadrilaterals(myTexture, mySprites); // Draw THOUSANDS of textured quads

  vkg.end(g);
}

Now the implementations.


namespace GraphicsHelper
{

//==============================================================================
/** Holds references to commonly used state objects for rendering */
struct DeviceState
{
    DeviceState(VulkanRenderer& renderer) : 
        device(renderer.getDevice()),
        commandBuffer(renderer.getCommandBuffer()),
        vertexPool(renderer.getVertexMemoryPool())
    {

    }

    const VulkanDevice& device;
    const VulkanCommandBuffer& commandBuffer;

    VulkanMemoryPool& vertexPool;
};

//==============================================================================
struct GraphicsPipelineCreateInfo : public VulkanGraphicsPipeline::CreateInfo
{
    using VertexType = Quadrilateral::Vertex;

    struct PushConstants
    {
        VulkanUniform::ScreenBounds screenBounds;
        VulkanUniform::Matrix matrix;
    };

    GraphicsPipelineCreateInfo(const VulkanPipelineLayout& pipelineLayout, const VulkanRenderPass& renderPass)
        : VulkanGraphicsPipeline::CreateInfo(pipelineLayout, renderPass)
    {
        vertexInputState
                .setVertexBindingDescriptions(bindings)
                .setVertexAttributeDescriptions(attributes);

        setPremultipliedAlphaBlending(blendAttachmentState);
        //setAlphaBlending(blendAttachmentState);

        colorBlendState
            .setAttachments(blendAttachmentState);
    }

    void setShaders(VulkanRenderer& renderer, const char* vertShaderName, const char* fragShaderName)
    {
        const auto vertShader = renderer.getShaderModule(vertShaderName);
        const auto fragShader = renderer.getShaderModule(fragShaderName);

        jassert(vertShader != nullptr && fragShader != nullptr);
        
        setShaderStages(*vertShader, *fragShader);
    }

    vk::PipelineColorBlendAttachmentState blendAttachmentState;

    std::array<vk::VertexInputBindingDescription, 1> bindings =
    {
        vk::VertexInputBindingDescription(0, sizeof(VertexType), vk::VertexInputRate::eVertex)
    };

    std::array<vk::VertexInputAttributeDescription, 3> attributes
    {
        vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32Sfloat, offsetof(VertexType, x)),
        vk::VertexInputAttributeDescription(1, 0, vk::Format::eR32G32Sfloat, offsetof(VertexType, s)),
        vk::VertexInputAttributeDescription(2, 0, vk::Format::eA8B8G8R8UnormPack32, offsetof(VertexType, colour))
    };
};

//==============================================================================
struct PipelineLayoutInfo : vk::PipelineLayoutCreateInfo
{
    PipelineLayoutInfo()
    {
        setPushConstantRanges(pushConstantRanges);
    }

    std::array<vk::PushConstantRange, 1> pushConstantRanges =
    {
        vk::PushConstantRange(vk::ShaderStageFlagBits::eVertex, 0, sizeof(GraphicsPipelineCreateInfo::PushConstants))
    };
};

//==============================================================================
class TexturedProgram
{
private:
    struct PipelineLayoutInfo : public vk::PipelineLayoutCreateInfo
    {
        PipelineLayoutInfo(const VulkanDescriptorSetLayout& descriptorSetLayout)
        {
            descriptorSetLayouts[0] = descriptorSetLayout.getHandle();
            setSetLayouts(descriptorSetLayouts);

            setPushConstantRanges(pushConstantRanges);
        }

        std::array<vk::DescriptorSetLayout, 1> descriptorSetLayouts;

        std::array<vk::PushConstantRange, 1> pushConstantRanges =
        {
            vk::PushConstantRange(vk::ShaderStageFlagBits::eVertex, 0, sizeof(GraphicsPipelineCreateInfo::PushConstants))
        };
    };

    struct PipelineInfo : public GraphicsPipelineCreateInfo
    {
        PipelineInfo(VulkanRenderer& renderer, const VulkanPipelineLayout& pipelineLayout, const VulkanRenderPass& renderPass)
            : GraphicsPipelineCreateInfo(pipelineLayout, renderPass)
        {
            setShaders(renderer, "Textured.vert", "Textured.frag");

            finish();
        }
    };

public:
    TexturedProgram(VulkanRenderer& renderer, VulkanDevice& device, const VulkanDescriptorSetLayout& descriptorSetLayout, const VulkanRenderPass& renderPass) :
        pipelineLayout(device, PipelineLayoutInfo(descriptorSetLayout)),
        pipeline(device, PipelineInfo(renderer, pipelineLayout, renderPass)) { }

    ~TexturedProgram() = default;

    const VulkanPipelineLayout pipelineLayout;
    const VulkanPipeline pipeline;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TexturedProgram)
};

//==============================================================================
struct VertexQueue
{
    enum 
    { 
        maxNumQuads = 1024,
        maxNumInidces = maxNumQuads * 6
    };

    using VertexType = Quadrilateral::Vertex;

    //==============================================================================

    VertexQueue(DeviceState& state_) : 
        state(state_),
        commandBuffer(state.commandBuffer),
        indices(state.vertexPool, VulkanMemoryBuffer::CreateInfo()
            .setSize<uint16_t>(maxNumInidces).setDeviceLocal().setIndexBuffer().setTransferDst()) 
    {
        VulkanIndexBuffer<uint16_t>::generateQuadrilateralIndices(indices, state.device, state.vertexPool, maxNumInidces);

        auto numQuads = std::min (static_cast<int>(maxNumQuads), static_cast<int>(maxNumInidces) / 6);
        maxVertices = numQuads * 4 - 4;
    }

    void bindIndexBuffer() noexcept
    {
        commandBuffer.bindIndexBuffer(indices.getBuffer());
    }

    void add(const Quadrilateral& quad) noexcept
	{
		static_assert(sizeof(VertexType) == sizeof(Quadrilateral::Vertex), "Sanity check Vertex size");

		auto* v = vertexData + numVertices;

		constexpr auto vertexPerQuad = Quadrilateral::numVertices;
		for (int i = 0; i < vertexPerQuad; ++i)
		{
			auto& src = quad.vertices[i];
			auto& dst = v[i];

			dst.x = src.x;
			dst.y = src.y;
			dst.s = src.s;
			dst.t = src.t;
			dst.colour = src.colour;
		}
		
		numVertices += vertexPerQuad;

		if (numVertices > maxVertices)
			draw();
	}

    void flush() noexcept
    {
        if (numVertices > 0)
            draw();
    }

    void draw() noexcept
    {
        jassert(commandBuffer.getHandle());

        const auto createInfo = VulkanMemoryBuffer::CreateInfo()
            .setSize<VertexType>(numVertices).setHostVisible().setVertexBuffer();

        auto vertexBuffer = vertexBuffers.add(new VulkanMemoryBuffer(state.vertexPool, createInfo));
        vertexBuffer->write(vertexData, static_cast<vk::DeviceSize>(numVertices * sizeof(VertexType)));
        vertexBuffer->setDefragmentOnRelease(false);
        
        commandBuffer.bindVertexBuffer(vertexBuffer->getBuffer());
       
        const auto numIndices = static_cast<uint32_t>((numVertices * 3) / 2);
        commandBuffer.drawIndexed(numIndices);

        numVertices = 0;
    }

    void reset()
    {
        // Before we release all framebuffes we turn on defragmentation
        // With this we avoid a constantly growing memory allocations for vertex buffers

        for (auto& vertexBuffer : vertexBuffers)
            vertexBuffer->setDefragmentOnRelease(true);

        vertexBuffers.clearQuick(true);
    }

private:
    DeviceState& state;

    const VulkanCommandBuffer& commandBuffer;
    
    VulkanMemoryBuffer indices;

    VertexType vertexData[maxNumQuads * 4];
    juce::OwnedArray<VulkanMemoryBuffer> vertexBuffers;

    int numVertices = 0;
    int maxVertices = 0;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VertexQueue)
};

//==============================================================================
struct CachedPipelines : public juce::ReferenceCountedObject
{   
    struct ShaderLoader
    {
        ShaderLoader(VulkanRenderer& renderer)
        {
            renderer.loadShaderModule("Textured.vert", vertTextured, vertTexturedSize);
            renderer.loadShaderModule("Textured.frag", fragTextured, fragTexturedSize);
        }
    };

    using Ptr = juce::ReferenceCountedObjectPtr<CachedPipelines>;

    CachedPipelines(VulkanRenderer& renderer) : 
        shaderLoader(renderer),
        texturedProgram(renderer, renderer.getDevice(), renderer.getTextureDescriptorLayout(), renderer.getRenderPass()) {}

    ~CachedPipelines() = default;

    static CachedPipelines* get(VulkanRenderer& renderer)
    {
        auto& device = renderer.getDevice();

        static constexpr char objectID[] = "VulkanGraphicsPipelines";
        auto pipelines = static_cast<CachedPipelines*>(device.getAssociatedObject (objectID));
        if (pipelines == nullptr)
        {
            pipelines = new CachedPipelines(renderer);
            device.setAssociatedObject(objectID, pipelines);
        }

        return pipelines;
    }

    ShaderLoader shaderLoader;
    TexturedProgram texturedProgram;
};

} // namespace GraphicsRenderHelper

//==============================================================================
struct VulkanGraphics::Implementation : public VulkanRenderer::Listener
{
    using PushConstants = GraphicsHelper::GraphicsPipelineCreateInfo::PushConstants;
    using Pipelines = GraphicsHelper::CachedPipelines;

    struct State : public GraphicsHelper::DeviceState
    {
        State(VulkanRenderer& renderer, Pipelines& pipelines_) :
            GraphicsHelper::DeviceState(renderer),
            vertexQueue(*this),
            pipelines(pipelines_)
        {

        }

        void begin()
        {
            vertexQueue.reset();
            activeTextures.clearQuick();
        }

        GraphicsHelper::VertexQueue vertexQueue;
        Pipelines& pipelines;

        juce::ReferenceCountedArray<VulkanTexture> activeTextures;
    };

    Implementation(VulkanGraphics& owner_) : owner(owner_) {}

    ~Implementation()
    {
        resetCache();
    }

    bool isInitialised() const noexcept
    {
        return currentRenderer != nullptr;
    }

    void resetCache()
    {
        if (! isInitialised())
            return;

        state.reset();

        currentRenderer->removeListener(this);
        currentRenderer = nullptr;
    }

    void requireCache(juce::Graphics& g)
    {
        if (isInitialised())
        {
            state->begin();
        }
            
		if (auto renderer = VulkanRenderer::get(g))
		{
            currentRenderer = renderer;
            renderer->addListener(this);

            Pipelines::Ptr cache = Pipelines::get(*renderer);
            state.reset(new State(*renderer, *cache));
		}
    }

	void rendererClosing(VulkanRenderer& /*target*/) override
	{
        resetCache();
	}

    void setTransform(const juce::AffineTransform& newTransform)
    {
        transform = newTransform;
    }

    /*
    void setPipeline(const VulkanPipeline& newPipeline)
	{

	}
    */

    /*
	void setDescriptorSet(const VulkanPipelineLayout& newPipelineLayout, const VulkanDescriptorSet& newDescriptorSet) noexcept
	{

	}
    */

    template<typename ProgramType>
    void setShaderProgram(ProgramType& shaderProgram)
    {
        state->commandBuffer.bindGraphicsPipeline(shaderProgram.pipeline);
    }

    template<typename ProgramType>
    void setShaderImage(ProgramType& shaderProgram, const juce::Image& image)
    {
        auto texture = currentRenderer->getTextureFor(image);
        state->activeTextures.add(texture);

        const auto& descriptorSet = currentRenderer->getTextureDescriptorSet(*texture, juce::Graphics::lowResamplingQuality);
        state->commandBuffer.bindDescriptorSet(shaderProgram.pipelineLayout, descriptorSet);
    }

    template<typename ProgramType>
    void setShaderPushConstants(ProgramType& shaderProgram, const juce::AffineTransform& renderTransform, const juce::Rectangle<float>& renderArea)
    {
        using Parameters = PushConstants;

        Parameters values;

        values.matrix.set(renderTransform);
        values.screenBounds.set(renderArea);
       
        state->commandBuffer.pushVertexConstants(shaderProgram.pipelineLayout, &values, sizeof(Parameters));
    }

    template<typename QueueType>
    void addQuadrilateral(QueueType& queue, const Quadrilateral& quad)
    {
        queue.add(quad);
    }

    template<typename QueueType>
    void addQuadrilaterals(QueueType& queue, const juce::Array<Quadrilateral>& quads)
    {
        for(const auto& quad : quads)
            queue.add(quad);
    }

    void drawQuadrilateral(const juce::Image& image, const Quadrilateral& quad)
    {
        if (auto s = state.get())
        {
            const auto& shaderProgram = s->pipelines.texturedProgram;
            
            setShaderProgram(shaderProgram);
            setShaderImage(shaderProgram, image);
            setShaderPushConstants(shaderProgram, transform, currentRenderer->getRenderBounds().toFloat());

            auto& queue = s->vertexQueue;

            queue.bindIndexBuffer();
            addQuadrilateral(queue, quad);
            queue.flush();
        }
    }

    void drawQuadrilaterals(const juce::Image& image, const juce::Array<Quadrilateral>& quads)
    {
        if (auto s = state.get())
        {
            const auto& shaderProgram = s->pipelines.texturedProgram;
           
            setShaderProgram(shaderProgram);
            setShaderImage(shaderProgram, image);
            setShaderPushConstants(shaderProgram, transform, currentRenderer->getRenderBounds().toFloat());

            auto& queue = s->vertexQueue;

            queue.bindIndexBuffer();
            addQuadrilaterals(queue, quads);
            queue.flush();
        }
    }

private:
    VulkanGraphics& owner;

    VulkanRenderer* currentRenderer = nullptr;
    std::unique_ptr<State> state;

    juce::AffineTransform transform;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Implementation)
};

//==============================================================================
VulkanGraphics::VulkanGraphics() : 
    implementationHolder(std::make_unique<Implementation>(*this)), implementation(*implementationHolder) {}

VulkanGraphics::~VulkanGraphics() = default;

void VulkanGraphics::setTransform(const juce::AffineTransform& newTransform)
{
    implementation.setTransform(newTransform);
}

void VulkanGraphics::begin(juce::Graphics& g)
{
    physicalPixelScaleFactor = g.getInternalContext().getPhysicalPixelScaleFactor();
    implementation.requireCache(g);
}

void VulkanGraphics::end(juce::Graphics& g)
{
    if (auto renderer = VulkanRenderer::get(g))
        renderer->restoreRenderState();
}

void VulkanGraphics::drawQuadrilateral(const juce::Image& image, const Quadrilateral& quad)
{
    implementation.drawQuadrilateral(image, quad);
}

void VulkanGraphics::drawQuadrilaterals(const juce::Image& image, const juce::Array<Quadrilateral>& quads)
{
    implementation.drawQuadrilaterals(image, quads);
}

It worked at the time and did draw thousands of sprites at 60 fps. Could potentially be used to write a 2D game or stuff like that. But I didn’t test this particular version. But it should give a good idea of what is necessary to set up a custom shader.

Ideally all of this would be hidden away from the normal JUCE user, and would be offered as a simple tool to extend the graphic features.

1 Like

Wow man! That looks like fantastic help. I’m really itching to do some shaders in this.
I also want to do my distance-field line drawer as I’ve got a load of graphs to draw really quickly. I’ll share if it’s any good! :smiley:
Thanks again.
Dave H.

Hello again. Using your code, I can’t find the class ‘VulkanRenderer’ other than in a CPP file of your modules.

Is there something missing, or how do how do I reference it please?

Cheers,
Dave H.

I see, yes this is what I meant by “it’s necessary to provide some form of interface in pw_vulkan_graphics…”.

I know it’s a bit hacky, but for now you can just use,
#include <pw_vulkan_graphics/contexts/pw_VulkanRenderer.cpp>

You’ll see that VulkanRenderer is an interface that provides access to some of the implementation data.
It’s for testing how a public interface could look like. So perhaps it will be moved to a header in the future. But go on. You can shuffle around things, it’s still a beta ; )

Also, forgot to mention it. The shaders have to be provided as SPV binary. So you’ll have to compile them using the VulkanSDK\1.2.170.0\Bin\glslc.exe, more about this in the tutorial Shader modules - Vulkan Tutorial ← Compiling the shaders.

OK. Thanks for the information.

@parawave
OK I’m doing your quad example, but jumping into the code I see that ‘state’ is not set, so nothing is drawn. I thought ‘state’ was set internally, out of my control, is there anything else you can think of I need to do?

! EDIT ! . . . OK I’ve got it to work, I had to create a context and aslo add have vkg as a member variable.
So my contructor for your example goes something like:

	VulkanPanel()
	{
		vkg = std::make_unique<VulkanGraphics>();
		context.setDefaultPhysicalDevice(instance);
		context.attachTo(*this);
. . .

It doesn’t seem to matter in what order I create vgk here, it can also be a simple member variable. But it can’t be in ‘paint.’

What state do you mean? The one std::unique_ptr state; in VulkanGraphics::Implementation?

It is created in void requireCache(juce::Graphics& g);, which is called in begin(g). A full Component setup should look like this. VulkanGraphics is intended as stack object. All resources are cached in the underlying context.

class MainComponent : public juce::Component
{
public:
    MainComponent()
    {
        context.setDefaultPhysicalDevice(instance);
        context.attachTo(*this);
    }
    
    ~MainComponent() override
    {
        context.detach();
    }
    
    void paint (juce::Graphics& g) override
    {
        VulkanGraphics vkg;

		vkg.begin(g);

		vkg.drawQuadrilaterals(...);

		vkg.end(g);
    }

    parawave::VulkanInstance instance;
    parawave::VulkanContext context;
};

Don’ forget begin() and end(). Yes, I know, this API is not really good. Ideally it should be RAII. But again, it was test code :upside_down_face:

To clarify how this works. The important part is:

if (auto renderer = VulkanRenderer::get(g))
{

It’s a static method that exposes the VulkanRenderer Interfaces, and so access to Texture, Vertex and Shader caches. Pipelines are also cached, by using the exposed setAssociatedObject(), similar to how the OpenGLContext offers a setAssociatedObject, which hold reference counted resource data in the context.

All is good to be fair. I was saying I didn’t have the context like you showed just there, but it’s all fine now.

I can’t declare ‘vkg’ in paint, because I get an exception thrown with an error in VulkanCommandBuffer::end() :

“Failed Failed to end command buffer recording.” [including the two 'failed’s]
I’ll play with it some more…

Hi, sorry to be a pain. In your test code, how do I set fragment constants?
I tried adding this, but it’s clearly not enough:

            std::array<vk::PushConstantRange, 2> pushConstantRanges =
            {
                vk::PushConstantRange(vk::ShaderStageFlagBits::eVertex, 0, sizeof(GraphicsPipelineCreateInfo::PushConstants)),
                vk::PushConstantRange(vk::ShaderStageFlagBits::eFragment , 0, sizeof(GraphicsPipelineCreateInfo::PushConstants))
            };

For sanity, I’m using an exact copy of the shader constants in the ‘vert’ & ‘frag’ shader.

I’m getting this back:

 "[ VUID-VkGraphicsPipelineCreateInfo-layout-00756 ] Object 0: handle = 0xf8c81f0000000143, type = VK_OBJECT_TYPE_SHADER_MODULE; Object 1: handle = 0xf775d00000000144, type = VK_OBJECT_TYPE_PIPELINE_LAYOUT; | MessageID = 0x45717876 | Push constant is used in VK_SHADER_STAGE_FRAGMENT_BIT of VkShaderModule 0xf8c81f0000000143[]. But VkPipelineLayout 0xf775d00000000144[] doesn't set VK_SHADER_STAGE_FRAGMENT_BIT. The Vulkan spec states: layout must be consistent with all shaders specified in pStages (https://vulkan.lunarg.com/doc/view/1.2.189.2/windows/1.2-extensions/vkspec.html#VUID-VkGraphicsPipelineCreateInfo-layout-00756)>"

So, I tried adding this to ‘setShaderPushConstants’…

state->commandBuffer.pushFragmentConstants(shaderProgram.pipelineLayout, &values, sizeof(Parameters));

…but it just failed to setup pipeline with an exception and a massive error listing.

Unfortunately for me, your GraphicsHelper is quite obfuscated, with some hard coded values it seems. For example, why does a quad need 6 vertex indices? I realise it’s two triangles, but can’t we do a triangle strip instead?

Ah yes, the joy of setting up Vulkan.

Not sure about the push constants thing, but I avoided the problem by using the same PushConstant structure for both vertex and fragment shader. You can take a look at the LinearGradient2 shader.


std::array<vk::PushConstantRange, 1> pushConstantRanges =
{
	vk::PushConstantRange(vk::ShaderStageFlagBits::eAllGraphics, 0, sizeof(LinearGradientPushConstants))
};

vk::ShaderStageFlagBits::eAllGraphics flag, and you can access it in both shader modules. Then:

commandBuffer.pushConstants(state.pipelines.radialGradient.pipelineLayout, &values, sizeof(Parameters));

There was some odd reason, the specs will tell you more about when to use the separate flags,
vk::ShaderStageFlagBits::eFragment.

Oh I didn’t see that you had a load of binary shader files in pw_vulkan_graphics.
Thanks again, that should help a bit. I just tried putting eAllGraphics into your graphicsHelper but it gave a long error list with things like:

‘must contain all stages in overlapping VkPushConstantRange stageFlags
VK_SHADER_STAGE_VERTEX_BIT|VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT|VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT|VK_SHADER_STAGE_GEOMETRY_BIT|VK_SHADER_STAGE_FRAGMENT_BIT), offset (0), and size (40) in VkPipelineLayout’

But I think I’ll get there if I follow your gradient shader C++ code.

Am I really the only person here to try your code?
Dave

I’m intending to give it a serious go in the new year, even if only to plot some kind of rough roadmap for transitioning from OpenGL, in whichever cases (if any) where it’s clear it’s especially critical to do so.

Does anyone ITT have some sense of the top reasons why you might want to consider migrating and/or starting a new project with this beta vulkan module rather than OpenGL (or CPU-bound JUCE drawing)?

My goal for a side-project would be to try and determine optimal quality/richness of a realtime audio-visualization (something practical for audio purposes) by detecting the user’s GPU and deciding which of a set of quality modes to use, and see how much further I could take Vulkan rather than OpenGL. Something like that might be really illustrative, in how much you could take advantage of modern GPUs, and also overall GPU/CPU performance.

Apple dropping OpenGL might persuade you a bit.
GPUs are amazingly fast these days. On the surface it looks like GPU and CPU rendering are fairly equal in speeds, caused mainly by unmatched rendering techniques between Juce and GPU manufacturers. I would rather fix things BEFORE they break myself.

Oh yeah, I meant other than the Apple deprecation - it just doesn’t seem like they can practically break OpenGL for quite a few more years, until there’s been enough time to migrate. I wonder if there are any precedents for dropping a graphics standard and how long it took that time.

I”m not sure Apple really cares - Metal has been out 7 years now.

Yeah that kind of illustrates the point I’m trying to make - they didn’t actually cut people off in those 7 years. So, how to pick which year to actually do it? “Deprecated” is one thing, but “doesn’t work” is another. I guess I’m actually more curious about what limitations they’d even face continuing to support OpenGL in a perma-deprecated state indefinitely. I am saying that as a former graphics major that is curious about things from an internal perspective.

If you’re curious then download the module, and add a context as shown. It’s all you have to do for now.
It might be worth noting it’s cool that ANGLE graphics layer has conversions for Metal and Vulkan now, so at least there’s no danger in losing WebGL:
https://chromium.googlesource.com/angle/angle/+/refs/heads/main/README.md

1 Like

My guess would be that since they’re moving towards using their own chips, apple want to be in full control of how those chips work. My deprecating OpenGL and going with their own native GPU library, they can guarantee they won’t ever have to make a compromise in their chips to accomodate for some potential future issue with OpenGL

1 Like

Thanks for sharing that link to ANGLE, that is something I am really interested in right now - interactive WebGL running at the lowest latency possible. I think I’ll look at this module when things quiet down a bit during the holidays.

The biggest advantage of Vulkan is the performance boost and new architectural possibilities due to the modern multithreaded design. Or at least the access to more “low level” API stuff. It’s not really present in the beta module, but you can essentially create your command buffers in parallel. With the right design it allows the preprocessing of drawing stuff in other threads. Plus, at least on Windows, heavy bottlenecks like wglMakeCurrent are absent. Offscreen rendering without heavy context acquisition is possible too.

But to be realistic. Whether GL, Vulkan or Metal. The main bottleneck and limiting factor of the graphics context is the single threaded scanline rasterization, which unfortunately seems unavoidable due to the clipping region and path feature.

There are alternative designs. Combinations of SDF, path to polygon triangulation and such stuff. But it’s complicated. At least if the current anti-aliasing quality is mandatory. To be honest, I’m not sure if it’s worth the effort. Perhaps the easy and future proof solution is just to rip out everything of the old graphics code and somehow embed Google SKIA. Then for JUCE 6, offer a compatibility implementation of the current context, but at the same time offer an alternative NEW context with the full and adjusted feature set for SKIA. Then deprecation in JUCE 7 and we can finally sail into the juicy glowing sunlight :watermelon: :sun_with_face:

6 Likes