diff --git a/examples/QRhiVideoDemo/CMakeLists.txt b/examples/QRhiVideoDemo/CMakeLists.txt new file mode 100644 index 00000000..df50bfb0 --- /dev/null +++ b/examples/QRhiVideoDemo/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.27) +project(QRhiVideoDemo C CXX) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 6.9 COMPONENTS Gui GuiPrivate Quick ShaderTools REQUIRED) + +qt_standard_project_setup(REQUIRES 6.9) + +add_executable(${PROJECT_NAME} + clayimplementation.c + clayqquickitem.cpp + clay_renderer_qrhi_impl.cpp + main.cpp + videodemoitem.cpp +) + +target_include_directories(${PROJECT_NAME} PUBLIC + . + ../.. + ../../renderers/QRhi +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + Qt6::Gui + Qt6::GuiPrivate + Qt6::Quick +) + +qt_add_qml_module(${PROJECT_NAME} + URI com.${PROJECT_NAME} + VERSION 1.0 + QML_FILES + main.qml + NO_GENERATE_EXTRA_QMLDIRS +) + +qt6_add_shaders(${PROJECT_NAME} "shaders" + # gl_VertexId is in earliest OpenGL ES 3.00, and GLSL 130 + GLSL "300 es,130" + PREFIX + "/qt/qml/com/clay" + FILES + "../../renderers/QRhi/clayquad.vert" + "../../renderers/QRhi/clayquad.frag" +) diff --git a/examples/QRhiVideoDemo/clay_renderer_qrhi_impl.cpp b/examples/QRhiVideoDemo/clay_renderer_qrhi_impl.cpp new file mode 100644 index 00000000..5597ac2d --- /dev/null +++ b/examples/QRhiVideoDemo/clay_renderer_qrhi_impl.cpp @@ -0,0 +1,2 @@ +#define CLAY_RENDERER_QRHI_IMPLEMENTATION +#include "clay_renderer_qrhi.h" diff --git a/examples/QRhiVideoDemo/clayimplementation.c b/examples/QRhiVideoDemo/clayimplementation.c new file mode 100644 index 00000000..1ff0eb0d --- /dev/null +++ b/examples/QRhiVideoDemo/clayimplementation.c @@ -0,0 +1,2 @@ +#define CLAY_IMPLEMENTATION +#include diff --git a/examples/QRhiVideoDemo/clayqquickitem.cpp b/examples/QRhiVideoDemo/clayqquickitem.cpp new file mode 100644 index 00000000..b55565a4 --- /dev/null +++ b/examples/QRhiVideoDemo/clayqquickitem.cpp @@ -0,0 +1,51 @@ +#include "clayqquickitem.h" + +static inline void clayErrorHandler(Clay_ErrorData errorData) { + qCritical() << "Clay error:" << QString::fromUtf8(errorData.errorText.chars, errorData.errorText.length); +} + +ClayQQuickItem::ClayQQuickItem(QQuickItem *parent) : QQuickItem(parent), m_textures(), m_clayImageConfigs() { + setFlag(ItemHasContents, true); + + size_t clayRequiredMemory = Clay_MinMemorySize(); + m_clayMemory = (Clay_Arena){ + .capacity = clayRequiredMemory, + .memory = (char *)malloc(clayRequiredMemory), + }; + m_clayCtx = Clay_Initialize( + m_clayMemory, + Clay_Dimensions{}, + Clay_ErrorHandler{ + .errorHandlerFunction = clayErrorHandler, + }); +} + +QSGNode *ClayQQuickItem::updatePaintNode(QSGNode *old, UpdatePaintNodeData *) { + ClayFastQSGRenderNode *node = static_cast(old); + bool nodeCreated = false; + + if (!node) { + node = new ClayFastQSGRenderNode(window()); + nodeCreated = true; + } + + // Note that MeasureText has to be set after the Context is set! + Clay_SetCurrentContext(m_clayCtx); + Clay_SetMeasureTextFunction(clayRendererQRhiMeasureText, &m_fonts); + + Clay_SetLayoutDimensions((Clay_Dimensions){(float)width(), (float)height()}); + + bool texturesStale; + + node->setCmds(doLayout(m_textures, &texturesStale), *m_fonts); + + if (nodeCreated || texturesStale) { + node->setTextures(m_textures); + } + + return node; +} + +void ClayQQuickItem::setFontsPtr(QList *fonts) { + m_fonts = fonts; +} diff --git a/examples/QRhiVideoDemo/clayqquickitem.h b/examples/QRhiVideoDemo/clayqquickitem.h new file mode 100644 index 00000000..ac065278 --- /dev/null +++ b/examples/QRhiVideoDemo/clayqquickitem.h @@ -0,0 +1,40 @@ +#pragma once + +#ifdef __cplusplus +#include +#endif + +#include "clay_renderer_qrhi.h" + +typedef struct ImageConfig ImageConfig; + +typedef struct ImageConfigs { + size_t capacity; + size_t count; + ImageConfig *cfgs; +} ImageConfigs; + +#ifdef __cplusplus +class ClayQQuickItem : public QQuickItem { + Q_OBJECT + + public: + explicit ClayQQuickItem(QQuickItem *parent = nullptr); + + protected: + QSGNode *updatePaintNode(QSGNode *old, UpdatePaintNodeData *) override; + + void setFontsPtr(QList *fonts); + + virtual Clay_RenderCommandArray + doLayout(QRhiTexture *textures[CLAY_RENDERER_QRHI_TEX_COUNT], bool *texturesStale) = 0; + + QRhiTexture *m_textures[CLAY_RENDERER_QRHI_TEX_COUNT]; + + ImageConfigs m_clayImageConfigs; + + Clay_Arena m_clayMemory; + Clay_Context *m_clayCtx; + QList *m_fonts; +}; +#endif diff --git a/examples/QRhiVideoDemo/main.cpp b/examples/QRhiVideoDemo/main.cpp new file mode 100644 index 00000000..6bc030f0 --- /dev/null +++ b/examples/QRhiVideoDemo/main.cpp @@ -0,0 +1,11 @@ +#include +#include + +int main(int argc, char **argv) { + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:/qt/qml/com/QRhiVideoDemo/main.qml"))); + + return app.exec(); +} diff --git a/examples/QRhiVideoDemo/main.qml b/examples/QRhiVideoDemo/main.qml new file mode 100644 index 00000000..870f65d4 --- /dev/null +++ b/examples/QRhiVideoDemo/main.qml @@ -0,0 +1,13 @@ +import QtQuick.Window + +import com.QRhiVideoDemo + +Window { + visible: true + width: 640 + height: 480 + + VideoDemoItem { + anchors.fill: parent + } +} diff --git a/examples/QRhiVideoDemo/videodemoitem.cpp b/examples/QRhiVideoDemo/videodemoitem.cpp new file mode 100644 index 00000000..236f5211 --- /dev/null +++ b/examples/QRhiVideoDemo/videodemoitem.cpp @@ -0,0 +1,17 @@ +#include "videodemoitem.h" + +#include + +extern "C" { +#include "../shared-layouts/clay-video-demo.c" +} + +VideoDemoItem::VideoDemoItem(QQuickItem *parent) : ClayQQuickItem(parent), m_data(malloc(sizeof(ClayVideoDemo_Data)), free) { + *(ClayVideoDemo_Data*)m_data.get() = ClayVideoDemo_Initialize(); + setFontsPtr(&m_fonts); +} + +Clay_RenderCommandArray VideoDemoItem::doLayout(QRhiTexture *textures[CLAY_RENDERER_QRHI_TEX_COUNT], bool *texturesStale) { + *texturesStale = false; + return ClayVideoDemo_CreateLayout((ClayVideoDemo_Data*)m_data.get()); +} diff --git a/examples/QRhiVideoDemo/videodemoitem.h b/examples/QRhiVideoDemo/videodemoitem.h new file mode 100644 index 00000000..60e95190 --- /dev/null +++ b/examples/QRhiVideoDemo/videodemoitem.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "clayqquickitem.h" + +class VideoDemoItem : public ClayQQuickItem { + Q_OBJECT + QML_ELEMENT + + public: + explicit VideoDemoItem(QQuickItem *parent = nullptr); + + protected: + Clay_RenderCommandArray doLayout(QRhiTexture *textures[CLAY_RENDERER_QRHI_TEX_COUNT], bool *texturesStale) override; + + std::unique_ptr m_data; + + QList m_fonts = QList{QFont()}; +}; diff --git a/examples/shared-layouts/clay-video-demo.c b/examples/shared-layouts/clay-video-demo.c index b1f322ed..97eeba6a 100644 --- a/examples/shared-layouts/clay-video-demo.c +++ b/examples/shared-layouts/clay-video-demo.c @@ -11,9 +11,9 @@ void RenderHeaderButton(Clay_String text) { .cornerRadius = CLAY_CORNER_RADIUS(5) }) { CLAY_TEXT(text, CLAY_TEXT_CONFIG({ + .textColor = { 255, 255, 255, 255 }, .fontId = FONT_ID_BODY_16, .fontSize = 16, - .textColor = { 255, 255, 255, 255 } })); } } @@ -21,9 +21,9 @@ void RenderHeaderButton(Clay_String text) { void RenderDropdownMenuItem(Clay_String text) { CLAY_AUTO_ID({.layout = { .padding = CLAY_PADDING_ALL(16)}}) { CLAY_TEXT(text, CLAY_TEXT_CONFIG({ + .textColor = { 255, 255, 255, 255 }, .fontId = FONT_ID_BODY_16, .fontSize = 16, - .textColor = { 255, 255, 255, 255 } })); } } @@ -41,8 +41,8 @@ typedef struct { Document documentsRaw[5]; DocumentArray documents = { + .documents = documentsRaw, .length = 5, - .documents = documentsRaw }; typedef struct { @@ -103,20 +103,20 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { // Build UI here CLAY(CLAY_ID("OuterContainer"), { - .backgroundColor = {43, 41, 51, 255 }, .layout = { - .layoutDirection = CLAY_TOP_TO_BOTTOM, .sizing = layoutExpand, .padding = CLAY_PADDING_ALL(16), - .childGap = 16 - } + .childGap = 16, + .layoutDirection = CLAY_TOP_TO_BOTTOM, + }, + .backgroundColor = {43, 41, 51, 255 }, }) { // Child elements go inside braces CLAY(CLAY_ID("HeaderBar"), { .layout = { .sizing = { + .width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(60), - .width = CLAY_SIZING_GROW(0) }, .padding = { 16, 16, 0, 0 }, .childGap = 16, @@ -134,9 +134,9 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { .cornerRadius = CLAY_CORNER_RADIUS(5) }) { CLAY_TEXT(CLAY_STRING("File"), CLAY_TEXT_CONFIG({ + .textColor = { 255, 255, 255, 255 }, .fontId = FONT_ID_BODY_16, .fontSize = 16, - .textColor = { 255, 255, 255, 255 } })); bool fileMenuVisible = @@ -146,22 +146,22 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { if (fileMenuVisible) { // Below has been changed slightly to fix the small bug where the menu would dismiss when mousing over the top gap CLAY(CLAY_ID("FileMenu"), { + .layout = { + .padding = {0, 0, 8, 8 } + }, .floating = { - .attachTo = CLAY_ATTACH_TO_PARENT, .attachPoints = { .parent = CLAY_ATTACH_POINT_LEFT_BOTTOM }, + .attachTo = CLAY_ATTACH_TO_PARENT, }, - .layout = { - .padding = {0, 0, 8, 8 } - } }) { CLAY_AUTO_ID({ .layout = { - .layoutDirection = CLAY_TOP_TO_BOTTOM, .sizing = { - .width = CLAY_SIZING_FIXED(200) + .width = CLAY_SIZING_FIXED(200) }, + .layoutDirection = CLAY_TOP_TO_BOTTOM, }, .backgroundColor = {40, 40, 40, 255 }, .cornerRadius = CLAY_CORNER_RADIUS(8) @@ -185,16 +185,16 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { .layout = { .sizing = layoutExpand, .childGap = 16 } }) { CLAY(CLAY_ID("Sidebar"), { - .backgroundColor = contentBackgroundColor, .layout = { - .layoutDirection = CLAY_TOP_TO_BOTTOM, - .padding = CLAY_PADDING_ALL(16), - .childGap = 8, .sizing = { - .width = CLAY_SIZING_FIXED(250), + .width = CLAY_SIZING_PERCENT(.3), .height = CLAY_SIZING_GROW(0) - } - } + }, + .padding = CLAY_PADDING_ALL(16), + .childGap = 8, + .layoutDirection = CLAY_TOP_TO_BOTTOM, + }, + .backgroundColor = contentBackgroundColor, }) { for (int i = 0; i < documents.length; i++) { Document document = documents.documents[i]; @@ -210,9 +210,9 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { .cornerRadius = CLAY_CORNER_RADIUS(8) }) { CLAY_TEXT(document.title, CLAY_TEXT_CONFIG({ + .textColor = { 255, 255, 255, 255 }, .fontId = FONT_ID_BODY_16, .fontSize = 20, - .textColor = { 255, 255, 255, 255 } })); } } else { @@ -222,9 +222,10 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { CLAY_AUTO_ID({ .layout = sidebarButtonLayout, .backgroundColor = (Clay_Color) { 120, 120, 120, (float)(Clay_Hovered() ? 120 : 0) }, .cornerRadius = CLAY_CORNER_RADIUS(8) }) { Clay_OnHover(HandleSidebarInteraction, clickData); CLAY_TEXT(document.title, CLAY_TEXT_CONFIG({ + .textColor = { 255, 255, 255, 255 }, .fontId = FONT_ID_BODY_16, .fontSize = 20, - .textColor = { 255, 255, 255, 255 } + .wrapMode = CLAY_TEXT_WRAP_NONE, })); } } @@ -232,25 +233,25 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { } CLAY(CLAY_ID("MainContent"), { - .backgroundColor = contentBackgroundColor, - .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() }, .layout = { - .layoutDirection = CLAY_TOP_TO_BOTTOM, - .childGap = 16, + .sizing = layoutExpand, .padding = CLAY_PADDING_ALL(16), - .sizing = layoutExpand - } + .childGap = 16, + .layoutDirection = CLAY_TOP_TO_BOTTOM, + }, + .backgroundColor = contentBackgroundColor, + .clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() }, }) { Document selectedDocument = documents.documents[data->selectedDocumentIndex]; CLAY_TEXT(selectedDocument.title, CLAY_TEXT_CONFIG({ + .textColor = COLOR_WHITE, .fontId = FONT_ID_BODY_16, .fontSize = 24, - .textColor = COLOR_WHITE })); CLAY_TEXT(selectedDocument.contents, CLAY_TEXT_CONFIG({ + .textColor = { 255, 255, 255, 255 }, .fontId = FONT_ID_BODY_16, .fontSize = 24, - .textColor = COLOR_WHITE })); } } @@ -261,4 +262,4 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) { Clay_RenderCommandArray_Get(&renderCommands, i)->boundingBox.y += data->yOffset; } return renderCommands; -} \ No newline at end of file +} diff --git a/renderers/QRhi/README.md b/renderers/QRhi/README.md new file mode 100644 index 00000000..660fa897 --- /dev/null +++ b/renderers/QRhi/README.md @@ -0,0 +1,11 @@ +# Qt QML QRhi Renderer + +We heard you like GUIs, so we put Clay UI in your QML UI so you can use GUI while you using a GUI. + +Renders into QSGNode - a way for making custom Items in QML user interface. + +Based on the GLES3 renderer. + +## ClayFastQSGRenderNode + +It's called "Fast" because it doesn't implement clipping and renders all the text as one QSGTextNode. diff --git a/renderers/QRhi/clay_renderer_qrhi.h b/renderers/QRhi/clay_renderer_qrhi.h new file mode 100644 index 00000000..0047ed2b --- /dev/null +++ b/renderers/QRhi/clay_renderer_qrhi.h @@ -0,0 +1,607 @@ +#ifndef CLAY_RENDERER_QRHI_H +#define CLAY_RENDERER_QRHI_H + +#ifdef __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#endif + +#include + +#define CLAY_RENDERER_QRHI_TEX_COUNT 16 +#define CLAY_RENDERER_MAX_ELEMENTS 16384 + +/* + * Instanced rendering for Rects/Images/Borders + * will use this data + * Note, it needs to be padded to 4 floats + * Draws: + * - One rectangular with possibly rounded corner + * - And possibly with a hole inside (with rounded edges too, if corners are rounded) + * - It could also draw a picture with also a rounded corner + */ +typedef struct RectInstance { + float x, y, w, h; // 4 Draw where on screen + float u0, v0, u1, v1; // 4 Atlas region + float r, g, b, a; // 4 Color + float radiusTL, radiusTR; // 2 Corner rounding + float radiusBL, radiusBR; // 2 + float borderL, borderR; // 2 Border widths + float borderT, borderB; // 2 + float texToUse; // 1 Texture atlas to take an image from (1-CLAY_RENDERER_QRHI_TEX_COUNT) + float pad[3]; // 3 +} RectInstance; + +typedef struct QuadInstanceArray { + RectInstance *instData; // packed per-instance floats + int capacity; // how many instances it can hold + int count; // how many instances does it actually hold +} QuadInstanceArray; + +typedef struct ImageConfig { + int textureToUse; + float u0, v0; + float u1, v1; +} ImageConfig; + +#ifdef __cplusplus +class ClayFastQSGRenderNode : public QSGRenderNode { + public: + ClayFastQSGRenderNode(QQuickWindow *window); + + void setCmds(Clay_RenderCommandArray cmds, const QList &fonts); + void setTextures(QRhiTexture *textures[CLAY_RENDERER_QRHI_TEX_COUNT]); + + void prepare() override; + void render(const RenderState *state) override; + void releaseResources() override; + RenderingFlags flags() const override; + QSGRenderNode::StateFlags changedStates() const override; + + private: + std::unique_ptr configurePipeline(); + + QQuickWindow *m_window; + + std::unique_ptr m_vertexBuffer; + std::unique_ptr m_quadInstanceBuffer; + std::unique_ptr m_uniformBuffer; + std::vector> m_samplers; + std::unique_ptr m_pipelineResourceBindings; + std::unique_ptr m_resourceBindings; + std::unique_ptr m_pipeline; + QList m_shaders; + + QRhiTexture **m_textures; + bool m_texturesStale; + + QSGTextNode *m_textNode; + + Clay_RenderCommandArray m_cmds; + + QuadInstanceArray m_quads; // Each instance is one quad + + int m_quadInstancesCount = 0; +}; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +Clay_Dimensions clayRendererQRhiMeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData); + +Clay_Color clayRendererQRhiPaletteBaseColor(); +Clay_Color clayRendererQRhiPaletteWindowColor(); +Clay_Color clayRendererQRhiPaletteHighlightColor(); +Clay_Color clayRendererQRhiPaletteTextColor(); +Clay_Color clayRendererQRhiPaletteHiglightedTextColor(); + +#ifdef __cplusplus +} +#endif + +#ifdef CLAY_RENDERER_QRHI_IMPLEMENTATION + +#ifdef __cplusplus +extern "C" { +#endif + +Clay_Dimensions clayRendererQRhiMeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) { + QList *fonts = *(QList **)userData; + QTextLayout textLayout(QString::fromUtf8(text.chars, text.length)); + textLayout.setFont((*fonts)[config->fontId]); + textLayout.beginLayout(); + textLayout.createLine(); + textLayout.endLayout(); + + QRectF boundingRect = textLayout.boundingRect(); + + return (Clay_Dimensions){ + .width = (float)boundingRect.width(), + .height = (float)boundingRect.height(), + }; +} + +Clay_Color clayRendererQRhiPaletteBaseColor() { + QColor color = QGuiApplication::palette().base().color(); + return Clay_Color{ + (float)color.red(), + (float)color.green(), + (float)color.blue(), + (float)color.alpha(), + }; +} + +Clay_Color clayRendererQRhiPaletteWindowColor() { + QColor color = QGuiApplication::palette().window().color(); + return Clay_Color{ + (float)color.red(), + (float)color.green(), + (float)color.blue(), + (float)color.alpha(), + }; +} + +Clay_Color clayRendererQRhiPaletteHighlightColor() { + QColor color = QGuiApplication::palette().highlight().color(); + return Clay_Color{ + (float)color.red(), + (float)color.green(), + (float)color.blue(), + (float)color.alpha(), + }; +} + +Clay_Color clayRendererQRhiPaletteTextColor() { + QColor color = QGuiApplication::palette().text().color(); + return Clay_Color{ + (float)color.red(), + (float)color.green(), + (float)color.blue(), + (float)color.alpha(), + }; +} + +Clay_Color clayRendererQRhiPaletteHiglightedTextColor() { + QColor color = QGuiApplication::palette().highlightedText().color(); + return Clay_Color{ + (float)color.red(), + (float)color.green(), + (float)color.blue(), + (float)color.alpha(), + }; +} + +#ifdef __cplusplus +} +#endif + +ClayFastQSGRenderNode::ClayFastQSGRenderNode(QQuickWindow *window) : m_window(window), m_textures(), m_texturesStale() { + QFile file; + file.setFileName(":/qt/qml/com/clay/renderers/QRhi/clayquad.vert.qsb"); + if (!file.open(QFile::ReadOnly)) { + qFatal("Failed to load vertex shader"); + } + m_shaders.append(QRhiShaderStage(QRhiShaderStage::Vertex, QShader::fromSerialized(file.readAll()))); + + file.close(); + file.setFileName(":/qt/qml/com/clay/renderers/QRhi/clayquad.frag.qsb"); + if (!file.open(QFile::ReadOnly)) { + qFatal("Failed to load fragment shader"); + } + m_shaders.append(QRhiShaderStage(QRhiShaderStage::Fragment, QShader::fromSerialized(file.readAll()))); + + m_textNode = window->createTextNode(); + appendChildNode(m_textNode); + + int maxInstances = CLAY_RENDERER_MAX_ELEMENTS; + + // create instance buffer big enough + m_quads.capacity = maxInstances; + m_quads.instData = (RectInstance *)malloc(sizeof(RectInstance) * m_quads.capacity); + m_quads.count = 0; +} + +void ClayFastQSGRenderNode::setCmds(Clay_RenderCommandArray cmds, const QList &fonts) { + m_cmds = cmds; + + m_textNode->clear(); + for (int i = 0; i < m_cmds.length; i++) { + Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&m_cmds, i); + if (cmd->commandType == CLAY_RENDER_COMMAND_TYPE_TEXT) { + Clay_StringSlice contents = cmd->renderData.text.stringContents; + QTextLayout textLayout(QString::fromUtf8(contents.chars, contents.length)); + textLayout.setFont(fonts[cmd->renderData.text.fontId]); + textLayout.beginLayout(); + textLayout.createLine(); + textLayout.endLayout(); + + Clay_Color textColor = cmd->renderData.text.textColor; + m_textNode->setColor(QColor(textColor.r, textColor.g, textColor.b, textColor.a)); + + m_textNode->addTextLayout(QPointF(cmd->boundingBox.x, cmd->boundingBox.y), &textLayout); + } + } + + markDirty(QSGNode::DirtyGeometry); +} + +void ClayFastQSGRenderNode::setTextures(QRhiTexture *textures[CLAY_RENDERER_QRHI_TEX_COUNT]) { + m_textures = textures; + m_texturesStale = true; +} + +void ClayFastQSGRenderNode::releaseResources() { + m_vertexBuffer.reset(); + m_quadInstanceBuffer.reset(); + m_uniformBuffer.reset(); + m_samplers.clear(); + m_pipeline.reset(); + m_pipelineResourceBindings.reset(); + m_resourceBindings.reset(); +} + +QSGRenderNode::RenderingFlags ClayFastQSGRenderNode::flags() const { + // We are rendering 2D content directly into the scene graph using QRhi, no + // direct usage of a 3D API. Hence NoExternalRendering. This is a minor + // optimization. + + // Additionally, the node takes the item transform into account by relying + // on projectionMatrix() and matrix() (see prepare()) and never rendering at + // other Z coordinates. Hence DepthAwareRendering. This is a potentially + // bigger optimization. + + return QSGRenderNode::NoExternalRendering | QSGRenderNode::DepthAwareRendering; +} + +QSGRenderNode::StateFlags ClayFastQSGRenderNode::changedStates() const { + // In Qt 6 only ViewportState and ScissorState matter, the rest is ignored. + return QSGRenderNode::StateFlag::ViewportState | QSGRenderNode::StateFlag::CullState; +} + +std::unique_ptr ClayFastQSGRenderNode::configurePipeline() { + QRhi *rhi = m_window->rhi(); + + auto pipeline = std::unique_ptr(rhi->newGraphicsPipeline()); + + // If layer.enabled == true on our QQuickItem and layer.textureMirroring is the + // default MirrorVertically, the winding order needs to be inverted when + // isYUpInFrameBuffer == true (OpenGL). This does not happen with other backends, + // unless textureMirroring is changed so that the situation is inverted, meaning + // GL needs no adjustments while others need the opposite winding order. Here we + // choose the replicate, to a degree, what the scenegraph renderer does, but note that + // this is only correct as long as textureMirroring is not changed from the default. + pipeline->setFrontFace( + renderTarget()->resourceType() == QRhiResource::TextureRenderTarget && rhi->isYUpInFramebuffer() + ? QRhiGraphicsPipeline::CW + : QRhiGraphicsPipeline::CCW); + pipeline->setCullMode(QRhiGraphicsPipeline::Back); + pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip); + QRhiGraphicsPipeline::TargetBlend blend; + blend.enable = true; + pipeline->setTargetBlends({blend}); + pipeline->setShaderResourceBindings(m_pipelineResourceBindings.get()); + pipeline->setShaderStages(m_shaders.cbegin(), m_shaders.cend()); + pipeline->setDepthTest(true); + + QRhiVertexInputLayout inputLayout; + + inputLayout.setBindings( + {{2 * sizeof(float)}, // unit quad + {sizeof(RectInstance), QRhiVertexInputBinding::PerInstance}}); + + inputLayout.setAttributes({ + // unit quad + {0, 0, QRhiVertexInputAttribute::Float2, 0}, + + // instance data + {1, 1, QRhiVertexInputAttribute::Float4, offsetof(RectInstance, x)}, + {1, 2, QRhiVertexInputAttribute::Float4, offsetof(RectInstance, u0)}, + {1, 3, QRhiVertexInputAttribute::Float4, offsetof(RectInstance, r)}, + {1, 4, QRhiVertexInputAttribute::Float4, offsetof(RectInstance, radiusTL)}, + {1, 5, QRhiVertexInputAttribute::Float4, offsetof(RectInstance, borderL)}, + {1, 6, QRhiVertexInputAttribute::Float, offsetof(RectInstance, texToUse)}, + }); + + pipeline->setVertexInputLayout(inputLayout); + pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor()); + + return pipeline; +} + +void ClayFastQSGRenderNode::prepare() { + QRhi *rhi = m_window->rhi(); + QSize renderTargetSize = renderTarget()->pixelSize(); + QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch(); + + // Turning Clay cmds into buffers to upload to the GPU through the QRhi API. + + for (int i = 0; i < m_cmds.length; i++) { + Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&m_cmds, i); + Clay_BoundingBox boundingBox = (Clay_BoundingBox){ + .x = roundf(cmd->boundingBox.x), + .y = roundf(cmd->boundingBox.y), + .width = roundf(cmd->boundingBox.width), + .height = roundf(cmd->boundingBox.height), + }; + + switch (cmd->commandType) { + case CLAY_RENDER_COMMAND_TYPE_TEXT: { + // Did in the setCmds(). + break; + } + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: + case CLAY_RENDER_COMMAND_TYPE_IMAGE: { + Clay_RectangleRenderData *config = &cmd->renderData.rectangle; + Clay_Color c = config->backgroundColor; + + // Convert to float 0..1 + float rf = c.r / 255.0f; + float gf = c.g / 255.0f; + float bf = c.b / 255.0f; + float af = c.a / 255.0f; + + bool isImage = cmd->commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE; + + // Ensure we don't overflow the capacity + if (m_quads.count >= m_quads.capacity) { + printf("Clay renderer: instance overflow!\n"); + break; + } + + int idx = m_quads.count; + RectInstance *dst = &m_quads.instData[idx]; + dst->x = boundingBox.x; + dst->y = boundingBox.y; + dst->w = boundingBox.width; + dst->h = boundingBox.height; + + if (isImage) { + ImageConfig *imgConf = (ImageConfig *)cmd->renderData.image.imageData; + dst->u0 = imgConf->u0; + dst->v0 = imgConf->v0; + dst->u1 = imgConf->u1; + dst->v1 = imgConf->v1; + dst->texToUse = (float)imgConf->textureToUse; + } else { + dst->u0 = dst->v0 = 0.0f; + dst->u1 = dst->v1 = 1.0f; + dst->texToUse = -1.0f; // This means no image, use albedo color + } + + // colour + dst->r = rf * af; + dst->g = gf * af; + dst->b = bf * af; + dst->a = af; + + // corner radii + Clay_CornerRadius r = config->cornerRadius; + dst->radiusTL = r.topLeft; + dst->radiusTR = r.topRight; + dst->radiusBL = r.bottomLeft; + dst->radiusBR = r.bottomRight; + + dst->borderT = 0.0f; + dst->borderR = 0.0f; + dst->borderB = 0.0f; + dst->borderL = 0.0f; + + m_quads.count++; + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { + // Ignore. + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: { + // Ignore. + break; + } + case CLAY_RENDER_COMMAND_TYPE_BORDER: { + Clay_BorderRenderData *br = &cmd->renderData.border; + + float rf = br->color.r / 255.0f; + float gf = br->color.g / 255.0f; + float bf = br->color.b / 255.0f; + float af = br->color.a / 255.0f; + + float x = boundingBox.x; + float y = boundingBox.y; + float w = boundingBox.width; + float h = boundingBox.height; + + float top = br->width.top; + float bottom = br->width.bottom; + float left = br->width.left; + float right = br->width.right; + + int idx = m_quads.count; + RectInstance *dst = &m_quads.instData[idx]; + + dst->x = x - left; + dst->y = y - top; + dst->w = w + right; + dst->h = h + bottom; + + dst->borderB = bottom; + dst->borderL = left; + dst->borderT = top; + dst->borderR = right; + + // Clay borders are inset, but adding support to outset borders + // Is as easy as this + some minor changes in shader too + bool CLAY_BORDERS_ARE_INSET = true; + if (CLAY_BORDERS_ARE_INSET) { + // Normal behaviour + dst->x = x; + dst->y = y; + dst->w = w; + dst->h = h; + } else { + // Hypotethical behaviour, if the borders were outside + dst->x = x - left; + dst->y = y - top; + dst->w = w + left + right; + dst->h = h + top + bottom; + } + + dst->u0 = 0.0f; + dst->v0 = 0.0f; + dst->u1 = 1.0f; + dst->v1 = 1.0f; + + dst->r = rf; + dst->g = gf; + dst->b = bf; + dst->a = af; + + dst->radiusTL = br->cornerRadius.topLeft; + dst->radiusTR = br->cornerRadius.topRight; + dst->radiusBR = br->cornerRadius.bottomRight; + dst->radiusBL = br->cornerRadius.bottomLeft; + + dst->texToUse = -1.0f; + + m_quads.count++; + break; + } + + case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { + // printf("Unhandled clay cmd: custom\n"); + break; + } + default: { + printf("Error: unhandled render command\n"); + exit(1); + } + } + } + + if (!m_quadInstanceBuffer || m_quadInstanceBuffer->size() < sizeof(RectInstance) * m_quads.capacity) { + m_quadInstanceBuffer.reset( + rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, sizeof(RectInstance) * m_quads.capacity)); + m_quadInstanceBuffer->create(); + } + + if (m_quads.count > 0) { + resourceUpdates->updateDynamicBuffer( + m_quadInstanceBuffer.get(), 0, m_quads.count * sizeof(RectInstance), m_quads.instData); + } + + m_quadInstancesCount = m_quads.count; + + // Clear instance arrays, as they were flushed to their render buffers. + m_quads.count = 0; + + if (!m_vertexBuffer) { + // create unit quad VBO (0..1) + const float quadVerts[8] = {0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f}; + m_vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(quadVerts))); + m_vertexBuffer->create(); + resourceUpdates->uploadStaticBuffer(m_vertexBuffer.get(), quadVerts); + } + + if (!m_uniformBuffer) { + m_uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68)); + m_uniformBuffer->create(); + } + + if (m_samplers.empty()) { + for (size_t i = 0; i < CLAY_RENDERER_QRHI_TEX_COUNT; ++i) { + m_samplers.emplace_back(rhi->newSampler( + QRhiSampler::Linear, + QRhiSampler::Linear, + QRhiSampler::None, + QRhiSampler::Repeat, + QRhiSampler::Repeat, + QRhiSampler::Repeat)); + if (!m_samplers[i]->create()) { + qFatal("Failed to create a sampler"); + } + } + } + + if (!m_resourceBindings || m_texturesStale) { + m_texturesStale = false; + QList bindings; + bindings.push_back( + QRhiShaderResourceBinding::uniformBuffer( + 0, + QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, + m_uniformBuffer.get())); + + if (m_textures) { + for (size_t i = 0; i < CLAY_RENDERER_QRHI_TEX_COUNT; ++i) { + if (m_textures[i]) { + bindings.push_back( + QRhiShaderResourceBinding::sampledTexture( + 1 + i, QRhiShaderResourceBinding::FragmentStage, m_textures[i], m_samplers[i].get())); + } + } + } + + m_resourceBindings.reset(rhi->newShaderResourceBindings()); + m_resourceBindings->setBindings(bindings.begin(), bindings.end()); + m_resourceBindings->create(); + } + + if (!m_pipelineResourceBindings) { + m_pipelineResourceBindings.reset(rhi->newShaderResourceBindings()); + m_pipelineResourceBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer( + 0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, nullptr), + QRhiShaderResourceBinding::sampledTexture( + 1, QRhiShaderResourceBinding::FragmentStage, nullptr, m_samplers[0].get()), + }); + m_pipelineResourceBindings->create(); + } + + if (!m_pipeline) { + m_pipeline = configurePipeline(); + m_pipeline->create(); + } + + const QMatrix4x4 mvp = *projectionMatrix() * *matrix(); + const float opacity = inheritedOpacity(); + + resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 0, 64, mvp.constData()); + resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 64, 4, &opacity); + commandBuffer()->resourceUpdate(resourceUpdates); +} + +void ClayFastQSGRenderNode::render(const RenderState *) { + QSize renderTargetSize = renderTarget()->pixelSize(); + + QRhiCommandBuffer *cb = commandBuffer(); + + cb->setViewport(QRhiViewport(0, 0, renderTargetSize.width(), renderTargetSize.height())); + + cb->setGraphicsPipeline(m_pipeline.get()); + cb->setShaderResources(m_resourceBindings.get()); + + QRhiCommandBuffer::VertexInput bindings[] = { + {m_vertexBuffer.get(), 0}, // unit quad (0..1) + {m_quadInstanceBuffer.get(), 0} // instances + }; + + cb->setVertexInput(0, 2, bindings); + cb->draw(4, m_quadInstancesCount); + + m_textNode->setViewport(QRect(0, 0, renderTargetSize.width(), renderTargetSize.height())); +} + +#endif + +#endif diff --git a/renderers/QRhi/clayquad.frag b/renderers/QRhi/clayquad.frag new file mode 100644 index 00000000..555e8754 --- /dev/null +++ b/renderers/QRhi/clayquad.frag @@ -0,0 +1,132 @@ +#version 440 +precision mediump float; +layout(location = 0) in vec2 vPos; +layout(location = 1) in vec4 vRect; +layout(location = 2) in vec2 vUV; +layout(location = 3) in vec4 vColor; +layout(location = 4) in vec4 vCornerRadii; +layout(location = 5) in vec4 vBorderWidths; +layout(location = 6) in float vTexSlot; +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; +}; +layout(binding = 1) uniform sampler2D uTex0; +layout(binding = 2) uniform sampler2D uTex1; +layout(binding = 3) uniform sampler2D uTex2; +layout(binding = 4) uniform sampler2D uTex3; +layout(binding = 5) uniform sampler2D uTex4; +layout(binding = 6) uniform sampler2D uTex5; +layout(binding = 7) uniform sampler2D uTex6; +layout(binding = 8) uniform sampler2D uTex7; +layout(binding = 9) uniform sampler2D uTex8; +layout(binding = 10) uniform sampler2D uTex9; +layout(binding = 11) uniform sampler2D uTex10; +layout(binding = 12) uniform sampler2D uTex11; +layout(binding = 13) uniform sampler2D uTex12; +layout(binding = 14) uniform sampler2D uTex13; +layout(binding = 15) uniform sampler2D uTex14; +layout(binding = 16) uniform sampler2D uTex15; +layout(location = 0) out vec4 frag; +void main() { + // Pixel coordinates in pixel space + vec2 pix = vRect.xy + vPos * vRect.zw; + float x0 = vRect.x; + float y0 = vRect.y; + float w = vRect.z; + float h = vRect.w; + // Local position inside the rectangle (0..w, 0..h) + vec2 local = pix - vec2(x0, y0); + // Original corner radii + float tl = vCornerRadii.x; + float tr = vCornerRadii.y; + float bl = vCornerRadii.z; + float br = vCornerRadii.w; + // Border thicknesses + float L = vBorderWidths.x; + float R = vBorderWidths.y; + float T = vBorderWidths.z; + float B = vBorderWidths.w; + bool CLAY_BORDERS_ARE_INSET = true; // it is true + bool isBorder = (L > 0.0 || R > 0.0 || T > 0.0 || B > 0.0); + float outerAlpha = 1.0; + // If it is not a border but rect or image, then it only has outer border what is provided + // Otherwise it increases the outter border, but the provided borde is the ineer border + // I think is better not increase rounding radius when that radius is smaller than border thickness + float outter_tl; + float outter_tr; + float outter_bl; + float outter_br; + if (CLAY_BORDERS_ARE_INSET) { + // Actural behaviour + outter_tl = tl; + outter_tr = tr; + outter_bl = bl; + outter_br = br; + tl = (tl > min(T, L)) ? tl - min(T, L) : tl; + tr = (tr > min(T, R)) ? tr - min(T, R) : tr; + bl = (bl > min(B, L)) ? bl - min(B, L) : bl; + br = (br > min(B, R)) ? br - min(B, R) : br; + } else { + // Hypothetical behaviour + outter_tl = (tl > min(T, L)) ? tl + min(T, L) : tl; + outter_tr = (tr > min(T, R)) ? tr + min(T, R) : tr; + outter_bl = (bl > min(B, L)) ? bl + min(B, L) : bl; + outter_br = (br > min(B, R)) ? br + min(B, R) : br; + } + if (outter_tl > 0.0 && local.x < outter_tl && local.y < outter_tl) + outerAlpha = step(length(local - vec2(outter_tl, outter_tl)), outter_tl); + if (outter_tr > 0.0 && local.x > w - outter_tr && local.y < outter_tr) + outerAlpha *= step(length(local - vec2(w - outter_tr, outter_tr)), outter_tr); + if (outter_bl > 0.0 && local.x < outter_bl && local.y > h - outter_bl) + outerAlpha *= step(length(local - vec2(outter_bl, h - outter_bl)), outter_bl); + if (outter_br > 0.0 && local.x > w - outter_br && local.y > h - outter_br) + outerAlpha *= step(length(local - vec2(w - outter_br, h - outter_br)), outter_br); + if (outerAlpha < 0.5) + discard; + // -------- Border logic -------- + if (isBorder) { + float iw = w - L - R; + float ih = h - T - B; + vec2 innerLocal = local - vec2(L, T); + // Check if pixel is inside inner rounded rect + bool insideInner = true; + if (tl > 0.0 && innerLocal.x < tl && innerLocal.y < tl) + insideInner = (length(innerLocal - vec2(tl, tl)) <= tl); + if (tr > 0.0 && innerLocal.x > iw - tr && innerLocal.y < tr) + insideInner = insideInner && (length(innerLocal - vec2(iw - tr, tr)) <= tr); + // Bottom-left + if (bl> 0.0 && innerLocal.x < bl && innerLocal.y > ih - bl) + insideInner = insideInner && (length(innerLocal - vec2(bl, ih - bl)) <= bl); + // Bottom-right + if (br > 0.0 && innerLocal.x > iw - br && innerLocal.y > ih - br) + insideInner = insideInner && (length(innerLocal - vec2(iw - br, ih - br)) <= br); + // Discard pixels inside inner rounded rect + if (insideInner && innerLocal.x >= 0.0 && innerLocal.x <= iw && innerLocal.y >= 0.0 && innerLocal.y < ih) + discard; + frag = vColor * qt_Opacity; + return; + } + // -------- Non-border rectangle or image -------- + if (vTexSlot < 0.0) { + frag = vColor * qt_Opacity; + } else { + int slot = int(vTexSlot + 0.5); + if (slot == 0) frag = texture(uTex0, vUV) * qt_Opacity; + if (slot == 1) frag = texture(uTex1, vUV) * qt_Opacity; + if (slot == 2) frag = texture(uTex2, vUV) * qt_Opacity; + if (slot == 3) frag = texture(uTex3, vUV) * qt_Opacity; + if (slot == 4) frag = texture(uTex4, vUV) * qt_Opacity; + if (slot == 5) frag = texture(uTex5, vUV) * qt_Opacity; + if (slot == 6) frag = texture(uTex6, vUV) * qt_Opacity; + if (slot == 7) frag = texture(uTex7, vUV) * qt_Opacity; + if (slot == 8) frag = texture(uTex8, vUV) * qt_Opacity; + if (slot == 9) frag = texture(uTex9, vUV) * qt_Opacity; + if (slot == 10) frag = texture(uTex10, vUV) * qt_Opacity; + if (slot == 11) frag = texture(uTex11, vUV) * qt_Opacity; + if (slot == 12) frag = texture(uTex12, vUV) * qt_Opacity; + if (slot == 13) frag = texture(uTex13, vUV) * qt_Opacity; + if (slot == 14) frag = texture(uTex14, vUV) * qt_Opacity; + if (slot == 15) frag = texture(uTex15, vUV) * qt_Opacity; + } +} diff --git a/renderers/QRhi/clayquad.vert b/renderers/QRhi/clayquad.vert new file mode 100644 index 00000000..b13f5484 --- /dev/null +++ b/renderers/QRhi/clayquad.vert @@ -0,0 +1,31 @@ +#version 440 +precision mediump float; +layout(location = 0) in vec2 aPos; // unit quad (0..1) +layout(location = 1) in vec4 aRect; // x,y,w,h (pixels) +layout(location = 2) in vec4 aUV; // u0,v0,u1,v1 +layout(location = 3) in vec4 aColor; // rgba +layout(location = 4) in vec4 aCornerRadii; +layout(location = 5) in vec4 aBorderWidths; +layout(location = 6) in float aTexSlot; +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; +}; +layout(location = 0) out vec2 vPos; +layout(location = 1) out vec4 vRect; +layout(location = 2) out vec2 vUV; +layout(location = 3) out vec4 vColor; +layout(location = 4) out vec4 vCornerRadii; +layout(location = 5) out vec4 vBorderWidths; +layout(location = 6) out float vTexSlot; +void main() { + vec2 pos = vec2(aPos.x * aRect.z + aRect.x, aPos.y * aRect.w + aRect.y); + gl_Position = qt_Matrix * vec4(pos, 0.0, 1.0); + vPos = aPos; + vRect = aRect; + vColor = aColor; + vUV = mix(aUV.xy, aUV.zw, aPos); + vCornerRadii = aCornerRadii; + vBorderWidths = aBorderWidths; + vTexSlot = aTexSlot; +}