1/*
2 * SPDX-FileCopyrightText: 2025 Arjen Hiemstra <ahiemstra@heimr.nl>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7#include "shadernode.h"
8
9#include "shadermaterial.h"
10#include "texturecache.h"
11
12struct VertexLayout {
13 using RectPropertyFunction = qreal (QRectF::*)() const;
14
15 RectPropertyFunction x;
16 RectPropertyFunction y;
17};
18
19static const std::array<VertexLayout, 4> Vertices = {
20 VertexLayout{.x = &QRectF::left, .y = &QRectF::top},
21 VertexLayout{.x = &QRectF::left, .y = &QRectF::bottom},
22 VertexLayout{.x = &QRectF::right, .y = &QRectF::top},
23 VertexLayout{.x = &QRectF::right, .y = &QRectF::bottom},
24};
25
26ShaderNode::ShaderNode()
27 : m_rect(QRectF{0.0, 0.0, 1.0, 1.0})
28 , m_uvs(16, QRectF{0.0, 0.0, 1.0, 1.0})
29{
30 setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial | QSGNode::UsePreprocess);
31}
32
33ShaderNode::~ShaderNode() noexcept
34{
35 for (auto texture : std::as_const(t&: m_textures)) {
36 if (texture.provider) {
37 texture.provider->disconnect(texture.providerConnection);
38 }
39 }
40
41 delete[] m_attributeSet->attributes;
42 delete m_attributeSet;
43}
44
45void ShaderNode::preprocess()
46{
47 for (const auto &info : std::as_const(t&: m_textures)) {
48 if (info.provider) {
49 preprocessTexture(texture: info);
50 }
51 }
52}
53
54QRectF ShaderNode::rect() const
55{
56 return m_rect;
57}
58
59void ShaderNode::setRect(const QRectF &newRect)
60{
61 if (newRect == m_rect) {
62 return;
63 }
64
65 m_rect = newRect;
66 m_geometryUpdateNeeded = true;
67}
68
69QRectF ShaderNode::uvs(TextureChannel channel) const
70{
71 Q_ASSERT(channel < m_textureChannels);
72 return m_uvs[channel];
73}
74
75void ShaderNode::setUVs(TextureChannel channel, const QRectF &newUvs)
76{
77 Q_ASSERT(channel < m_textureChannels);
78
79 if (newUvs == m_uvs[channel]) {
80 return;
81 }
82
83 m_uvs[channel] = newUvs;
84 m_geometryUpdateNeeded = true;
85}
86
87QSGMaterialType *ShaderNode::materialVariant() const
88{
89 return m_materialVariant;
90}
91
92void ShaderNode::setMaterialVariant(QSGMaterialType *variant)
93{
94 if (variant == m_materialVariant) {
95 return;
96 }
97
98 m_materialVariant = variant;
99
100 auto newMaterial = createMaterialVariant(variant: m_materialVariant);
101 if (newMaterial) {
102 m_shaderMaterial = dynamic_cast<ShaderMaterial *>(newMaterial);
103 setMaterial(newMaterial);
104 markDirty(bits: QSGNode::DirtyMaterial);
105 }
106}
107
108void ShaderNode::setShader(const QString &shader)
109{
110 setMaterialVariant(ShaderMaterial::typeForName(name: shader));
111}
112
113void ShaderNode::setUniformBufferSize(qsizetype size)
114{
115 if (!m_shaderMaterial) {
116 return;
117 }
118
119 m_shaderMaterial->setUniformBufferSize(size);
120}
121
122std::span<char> ShaderNode::uniformData()
123{
124 if (!m_shaderMaterial) {
125 return std::span<char>{};
126 }
127
128 return m_shaderMaterial->uniformData();
129}
130
131void ShaderNode::setTextureChannels(unsigned char count)
132{
133 if (count == m_textureChannels) {
134 return;
135 }
136
137 m_textureChannels = std::clamp(val: count, lo: uint8_t(1), hi: uint8_t(16));
138
139 if (geometry()) {
140 setGeometry(nullptr);
141 delete[] m_attributeSet->attributes;
142 delete m_attributeSet;
143 }
144
145 while (m_textures.size() > count) {
146 m_textures.removeLast();
147 }
148
149 m_geometryUpdateNeeded = true;
150}
151
152void ShaderNode::setTexture(TextureChannel channel, const QImage &image, QQuickWindow *window, QQuickWindow::CreateTextureOptions options)
153{
154 if (!m_shaderMaterial) {
155 return;
156 }
157
158 auto texture = TextureCache::loadTexture(window, image, options);
159 if (!texture) {
160 return;
161 }
162
163 auto info = TextureInfo{
164 .channel = channel,
165 .options = options,
166 .texture = texture,
167 .provider = nullptr,
168 .providerConnection = {},
169 };
170
171 auto itr = std::find_if(first: m_textures.begin(), last: m_textures.end(), pred: [channel](auto info) {
172 return info.channel == channel;
173 });
174 if (itr != m_textures.end()) {
175 *itr = info;
176 } else {
177 m_textures.append(t: info);
178 }
179
180 setUVs(channel, newUvs: texture->normalizedTextureSubRect());
181
182 texture->setFiltering(QSGTexture::Filtering::Linear);
183
184 m_shaderMaterial->setTexture(binding: channel + 1, texture: texture.get());
185 markDirty(bits: QSGNode::DirtyMaterial);
186}
187
188void ShaderNode::setTexture(TextureChannel channel, QSGTextureProvider *provider, QQuickWindow::CreateTextureOptions options)
189{
190 if (!m_shaderMaterial) {
191 return;
192 }
193
194 auto connection = QObject::connect(sender: provider, signal: &QSGTextureProvider::textureChanged, slot: [this]() {
195 markDirty(bits: QSGNode::DirtyMaterial);
196 });
197
198 auto info = TextureInfo{
199 .channel = channel,
200 .options = options,
201 .texture = nullptr,
202 .provider = provider,
203 .providerConnection = connection,
204 };
205
206 auto itr = std::find_if(first: m_textures.begin(), last: m_textures.end(), pred: [channel](auto info) {
207 return info.channel == channel;
208 });
209 if (itr != m_textures.end()) {
210 if (itr->provider) {
211 itr->provider->disconnect(itr->providerConnection);
212 }
213 *itr = info;
214 } else {
215 m_textures.append(t: info);
216 }
217}
218
219void ShaderNode::setTextureFiltering(TextureChannel channel, QSGTexture::Filtering filtering)
220{
221 auto itr = std::find_if(first: m_textures.begin(), last: m_textures.end(), pred: [channel](auto info) {
222 return info.channel == channel;
223 });
224 if (itr != m_textures.end()) {
225 itr->filtering = filtering;
226 if (itr->texture) {
227 itr->texture->setFiltering(filtering);
228 }
229 }
230}
231
232void ShaderNode::update()
233{
234 if (m_geometryUpdateNeeded) {
235 const auto attributeCount = 1 + m_textureChannels;
236
237 if (!geometry()) {
238 QSGGeometry::Attribute *attributes = new QSGGeometry::Attribute[attributeCount];
239 attributes[0] = QSGGeometry::Attribute::createWithAttributeType(pos: 0, tupleSize: 2, primitiveType: QSGGeometry::FloatType, attributeType: QSGGeometry::PositionAttribute);
240
241 for (int i = 0; i < m_textureChannels; ++i) {
242 attributes[i + 1] = QSGGeometry::Attribute::createWithAttributeType(pos: i + 1, tupleSize: 2, primitiveType: QSGGeometry::FloatType, attributeType: QSGGeometry::TexCoordAttribute);
243 }
244
245 m_attributeSet =
246 new QSGGeometry::AttributeSet{.count = attributeCount, .stride = int(sizeof(float)) * 2 * attributeCount, .attributes = attributes};
247
248 setGeometry(new QSGGeometry{*m_attributeSet, Vertices.size()});
249 }
250
251 auto vertices = static_cast<float *>(geometry()->vertexData());
252
253 auto index = 0;
254 for (auto layout : Vertices) {
255 vertices[index++] = (m_rect.*layout.x)();
256 vertices[index++] = (m_rect.*layout.y)();
257
258 for (int channel = 0; channel < m_textureChannels; ++channel) {
259 auto uv = uvs(channel);
260 vertices[index++] = (uv.*layout.x)();
261 vertices[index++] = (uv.*layout.y)();
262 }
263 }
264
265 markDirty(bits: QSGNode::DirtyGeometry);
266 m_geometryUpdateNeeded = false;
267 }
268}
269
270QSGMaterial *ShaderNode::createMaterialVariant(QSGMaterialType *variant)
271{
272 return new ShaderMaterial(variant);
273}
274
275void ShaderNode::preprocessTexture(const TextureInfo &info)
276{
277 auto provider = info.provider;
278 if (!provider || !provider->texture() || !m_shaderMaterial) {
279 return;
280 }
281
282 if (provider->texture()->isAtlasTexture() && !info.options.testFlag(flag: QQuickWindow::TextureCanUseAtlas)) {
283 m_shaderMaterial->setTexture(binding: info.channel + 1, texture: provider->texture()->removedFromAtlas());
284 } else {
285 m_shaderMaterial->setTexture(binding: info.channel + 1, texture: provider->texture());
286 }
287 if (QSGDynamicTexture *dynamic_texture = qobject_cast<QSGDynamicTexture *>(object: provider->texture())) {
288 dynamic_texture->updateTexture();
289 }
290 m_shaderMaterial->setTextureFiltering(binding: info.channel + 1, filtering: info.filtering);
291}
292

source code of kirigami/src/primitives/scenegraph/shadernode.cpp