CaptureMoment 0.1.0
Free and open-source non-destructive photo editor
Loading...
Searching...
No Matches
rhi_image_item_renderer.cpp
Go to the documentation of this file.
1
10#include "common/image_region.h"
11
12#include <spdlog/spdlog.h>
13#include <QMutexLocker>
14#include <QFile>
15#include <QMatrix4x4>
16#include <algorithm>
17
19
20// =============================================================================
21// Constructor & Destructor
22// =============================================================================
23
25 : m_item(item)
26{
27 spdlog::debug("[RHIImageItemRenderer::RHIImageItemRenderer]: Created");
28}
29
31
32// =============================================================================
33// QQuickRhiItemRenderer Interface
34// =============================================================================
35
36void RHIImageItemRenderer::initialize(QRhiCommandBuffer* cb)
37{
38 if (m_initialized) {
39 return;
40 }
41
42 if (!rhi()) {
43 spdlog::error("[RHIImageItemRenderer::initialize]: No RHI context available");
44 return;
45 }
46
47 spdlog::debug("[RHIImageItemRenderer::initialize]: Starting initialization...");
48
49 // Create all GPU resources
50 createGeometry(cb);
51 createPipeline();
52
53 // Verify all resources were created successfully
54 if (m_vertex_buffer && m_index_buffer && m_uniform_buffer && m_pipeline && m_sampler && m_srb) {
55 m_initialized = true;
56 spdlog::info("[RHIImageItemRenderer::initialize]: Initialized successfully");
57 } else {
58 spdlog::error("[RHIImageItemRenderer::initialize]: Initialization failed - missing resources");
59 }
60}
61
62void RHIImageItemRenderer::synchronize(QQuickRhiItem* item)
63{
64 spdlog::debug("[RHIImageItemRenderer::synchronize]: Synchronizing...");
65
66 auto* rhi_item { static_cast<RHIImageItem*>(item) };
67 if (!rhi_item) {
68 spdlog::warn("[RHIImageItemRenderer::synchronize]: Null item");
69 return;
70 }
71
72 // Lock mutex and copy state from GUI thread
73 QMutexLocker lock(rhi_item->getImageMutex());
74
75 // Copy display parameters
76 m_zoom = rhi_item->zoom();
77 m_pan = rhi_item->pan();
78
79 m_item_width = static_cast<float>(rhi_item->width());
80 m_item_height = static_cast<float>(rhi_item->height());
81
82 spdlog::debug("[RHIImageItemRenderer::synchronize]: zoom={}, pan=({}, {})",
83 m_zoom, m_pan.x(), m_pan.y());
84
85 // Get the current image
86 const auto* image { rhi_item->getFullImage() };
87
88 // Check if texture needs update:
89 // 1. Flag was set by setImage() or updateTile()
90 // 2. Or we have an image but texture dimensions don't match
91 const bool needs_update { rhi_item->m_texture_needs_update };
92 const bool size_mismatch { image && image->isValid() &&
93 (!m_texture || m_texture->pixelSize() != QSize(image->width(), image->height())) };
94
95 if (needs_update || size_mismatch)
96 {
97 if (image && image->isValid())
98 {
99 m_image_width = static_cast<int>(image->width());
100 m_image_height = static_cast<int>(image->height());
101
102 spdlog::debug("[RHIImageItemRenderer::synchronize]: Converting image {}x{}",
103 m_image_width, m_image_height);
104
105 updateTextureFromImage(*image);
106 m_texture_needs_update = true;
107 }
108 // Clear the item's flag
109 rhi_item->m_texture_needs_update = false;
110 }
111}
112
113void RHIImageItemRenderer::render(QRhiCommandBuffer* cb)
114{
115 spdlog::error("[RHIImageItemRenderer::render]: RENDER CALLED");
116
117 if (!m_initialized || !m_pipeline || !cb) {
118 spdlog::warn("[RHIImageItemRenderer::render]: Not ready (initialized={}, pipeline={})",
119 m_initialized, static_cast<bool>(m_pipeline));
120 return;
121 }
122
123 if (!renderTarget()) {
124 spdlog::warn("[RHIImageItemRenderer::render]: No render target");
125 return;
126 }
127
128 // Upload texture data if needed
129 if (m_texture_needs_update && m_texture && !m_pixel_data.empty())
130 {
131 QRhiResourceUpdateBatch* resourceUpdates = rhi()->nextResourceUpdateBatch();
132 if (resourceUpdates)
133 {
134 QRhiTextureSubresourceUploadDescription subresDesc(
135 m_pixel_data.data(),
136 static_cast<quint32>(m_pixel_data.size())
137 );
138
139 QRhiTextureUploadDescription desc(QRhiTextureUploadEntry(0, 0, subresDesc));
140 resourceUpdates->uploadTexture(m_texture.get(), desc);
141 cb->resourceUpdate(resourceUpdates);
142 m_texture_needs_update = false;
143
144 spdlog::debug("[RHIImageItemRenderer::render]: Texture uploaded {}x{}",
145 m_pixel_data_size.width(), m_pixel_data_size.height());
146 }
147 }
148
149 // Build transformation matrix
150 // The quad vertices are in [0,1] range, so we scale to image dimensions
151 // then apply zoom and pan transformations
152 QMatrix4x4 matrix;
153
154 const float logical_w { m_item_width };
155 const float logical_h { m_item_height };
156
157 matrix.ortho(0.0f, logical_w, logical_h, 0.0f, -1.0f, 1.0f);
158
159 const float display_w { static_cast<float>(m_image_width) * m_zoom };
160 const float display_h { static_cast<float>(m_image_height) * m_zoom };
161
162 const float x_pos { (logical_w - display_w) / 2.0f + static_cast<float>(m_pan.x()) };
163 const float y_pos { (logical_h - display_h) / 2.0f + static_cast<float>(m_pan.y()) };
164
165 matrix.translate(x_pos, y_pos);
166 matrix.scale(display_w, display_h);
167
168 spdlog::debug("[RHIImageItemRenderer::render]: image={}x{}, zoom={}, pan=({}, {}), logical={}x{}",
169 m_image_width, m_image_height, m_zoom, m_pan.x(), m_pan.y(),
170 logical_w, logical_h);
171
172 // Update uniform buffer
173 if (m_uniform_buffer)
174 {
175 QRhiResourceUpdateBatch* batch = rhi()->nextResourceUpdateBatch();
176 if (batch) {
177 batch->updateDynamicBuffer(m_uniform_buffer.get(), 0, 64, matrix.constData());
178 cb->resourceUpdate(batch);
179 }
180 }
181
182 // Begin render pass
183 QSize rt_size { renderTarget()->pixelSize() };
184 cb->beginPass(renderTarget(), QColor(30, 30, 30, 255), {1.0f, 0});
185
186 // Set graphics pipeline
187 cb->setGraphicsPipeline(m_pipeline.get());
188
189 // Le viewport RHI reste en pixels PHYSIQUES (c'est normal)
190 cb->setViewport(QRhiViewport(0, 0, rt_size.width(), rt_size.height()));
191
192 // Bind shader resources
193 cb->setShaderResources(m_srb.get());
194
195 // Bind vertex and index buffers
196 QRhiCommandBuffer::VertexInput vertex_input{m_vertex_buffer.get(), 0};
197 cb->setVertexInput(0, 1, &vertex_input, m_index_buffer.get(), 0, QRhiCommandBuffer::IndexUInt16);
198
199 // Draw quad
200 cb->drawIndexed(6);
201
202 cb->endPass();
203}
204
205void RHIImageItemRenderer::createGeometry(QRhiCommandBuffer* cb)
206{
207 // Vertex structure: position (x, y) + texcoord (u, v)
208 struct Vertex {
209 float x, y; // Position in [0,1] range
210 float u, v; // Texture coordinates
211 };
212
213 // Quad vertices in [0,1] position range
214 // Texture coordinates: V=0 at top, V=1 at bottom (matches Qt Quick Y-down coordinate system)
215 //
216 // (0,0)----(1,0)
217 // | / |
218 // | / |
219 // | / |
220 // (0,1)----(1,1)
221 //
222 // Vertex layout for two triangles:
223 // Triangle 1: v0, v1, v2 (top-left, top-right, bottom-right)
224 // Triangle 2: v0, v2, v3 (top-left, bottom-right, bottom-left)
225 Vertex vertices[] = {
226 {0.0f, 0.0f, 0.0f, 0.0f}, // v0: top-left
227 {1.0f, 0.0f, 1.0f, 0.0f}, // v1: top-right
228 {1.0f, 1.0f, 1.0f, 1.0f}, // v2: bottom-right
229 {0.0f, 1.0f, 0.0f, 1.0f} // v3: bottom-left
230 };
231
232 // Two triangles with explicit indices (more reliable than triangle strip)
233 // Triangle 1: v0 -> v1 -> v2
234 // Triangle 2: v0 -> v2 -> v3
235 uint16_t indices[] = {
236 0, 1, 2, // First triangle (top-left, top-right, bottom-right)
237 0, 2, 3 // Second triangle (top-left, bottom-right, bottom-left)
238 };
239
240 // Create buffers
241 m_vertex_buffer.reset(rhi()->newBuffer(
242 QRhiBuffer::Immutable,
243 QRhiBuffer::VertexBuffer,
244 sizeof(vertices)
245 ));
246
247 m_index_buffer.reset(rhi()->newBuffer(
248 QRhiBuffer::Immutable,
249 QRhiBuffer::IndexBuffer,
250 sizeof(indices)
251 ));
252
253 // Uniform buffer: 256 bytes (aligned for GPU requirements)
254 m_uniform_buffer.reset(rhi()->newBuffer(
255 QRhiBuffer::Dynamic,
256 QRhiBuffer::UniformBuffer,
257 256
258 ));
259
260 if (!m_vertex_buffer->create()) {
261 spdlog::error("[RHIImageItemRenderer::createGeometry]: Failed to create vertex buffer");
262 return;
263 }
264
265 if (!m_index_buffer->create()) {
266 spdlog::error("[RHIImageItemRenderer::createGeometry]: Failed to create index buffer");
267 return;
268 }
269
270 if (!m_uniform_buffer->create()) {
271 spdlog::error("[RHIImageItemRenderer::createGeometry]: Failed to create uniform buffer");
272 return;
273 }
274
275 // CRITICAL: Upload vertex/index data via command buffer
276 if (cb) {
277 QRhiResourceUpdateBatch* batch { rhi()->nextResourceUpdateBatch() };
278 if (batch) {
279 batch->uploadStaticBuffer(m_vertex_buffer.get(), vertices);
280 batch->uploadStaticBuffer(m_index_buffer.get(), indices);
281 cb->resourceUpdate(batch);
282 spdlog::debug("[RHIImageItemRenderer::createGeometry]: Geometry buffers uploaded");
283 } else {
284 spdlog::error("[RHIImageItemRenderer::createGeometry]: Failed to get resource update batch");
285 }
286 } else {
287 spdlog::error("[RHIImageItemRenderer::createGeometry]: No command buffer provided");
288 }
289}
290
291void RHIImageItemRenderer::createPipeline()
292{
293 // Load compiled shaders (.qsb files)
294 QFile vs_file(":/shaders/glsl/image_display.vert.qsb");
295 QFile fs_file(":/shaders/glsl/image_display.frag.qsb");
296
297 if (!vs_file.open(QIODevice::ReadOnly)) {
298 spdlog::error("[RHIImageItemRenderer::createPipeline]: Failed to open vertex shader: {}",
299 vs_file.fileName().toStdString());
300 return;
301 }
302
303 if (!fs_file.open(QIODevice::ReadOnly)) {
304 spdlog::error("[RHIImageItemRenderer::createPipeline]: Failed to open fragment shader: {}",
305 fs_file.fileName().toStdString());
306 return;
307 }
308
309 QShader vs { QShader::fromSerialized(vs_file.readAll()) };
310 QShader fs { QShader::fromSerialized(fs_file.readAll()) };
311
312 if (!vs.isValid()) {
313 spdlog::error("[RHIImageItemRenderer::createPipeline]: Invalid vertex shader");
314 return;
315 }
316
317 if (!fs.isValid()) {
318 spdlog::error("[RHIImageItemRenderer::createPipeline]: Invalid fragment shader");
319 return;
320 }
321
322 spdlog::debug("[RHIImageItemRenderer::createPipeline]: Shaders loaded successfully");
323
324 // Create texture sampler
325 m_sampler.reset(rhi()->newSampler(
326 QRhiSampler::Linear, // mag filter
327 QRhiSampler::Linear, // min filter
328 QRhiSampler::None, // mipmap mode
329 QRhiSampler::ClampToEdge, // address U
330 QRhiSampler::ClampToEdge // address V
331 ));
332
333 if (!m_sampler->create()) {
334 spdlog::error("[RHIImageItemRenderer::createPipeline]: Failed to create sampler");
335 return;
336 }
337
338 // Create placeholder texture (1x1, will be replaced when image is loaded)
339 m_texture.reset(rhi()->newTexture(QRhiTexture::RGBA8, QSize(1, 1)));
340
341 if (!m_texture->create()) {
342 spdlog::error("[RHIImageItemRenderer::createPipeline]: Failed to create placeholder texture");
343 return;
344 }
345
346 // Create shader resource bindings
347 m_srb.reset(rhi()->newShaderResourceBindings());
348
349 QRhiShaderResourceBinding bindings[] = {
350 // Binding 0: Uniform buffer (vertex + fragment stages)
351 QRhiShaderResourceBinding::uniformBuffer(
352 0,
353 QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
354 m_uniform_buffer.get()
355 ),
356 // Binding 1: Sampled texture (fragment stage only)
357 QRhiShaderResourceBinding::sampledTexture(
358 1,
359 QRhiShaderResourceBinding::FragmentStage,
360 m_texture.get(),
361 m_sampler.get()
362 )
363 };
364
365 m_srb->setBindings(bindings, bindings + 2);
366
367 if (!m_srb->create()) {
368 spdlog::error("[RHIImageItemRenderer::createPipeline]: Failed to create shader resource bindings");
369 return;
370 }
371
372 // Create graphics pipeline
373 m_pipeline.reset(rhi()->newGraphicsPipeline());
374
375 // Set shader stages
376 m_pipeline->setShaderStages({
377 QRhiShaderStage{QRhiShaderStage::Vertex, vs},
378 QRhiShaderStage{QRhiShaderStage::Fragment, fs}
379 });
380
381 // Vertex input layout
382 QRhiVertexInputLayout input_layout;
383 input_layout.setBindings({
384 QRhiVertexInputBinding(4 * sizeof(float)) // 4 floats per vertex: x, y, u, v
385 });
386 input_layout.setAttributes({
387 QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0), // position
388 QRhiVertexInputAttribute(0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float)) // texcoord
389 });
390
391 m_pipeline->setVertexInputLayout(input_layout);
392 m_pipeline->setShaderResourceBindings(m_srb.get());
393
394 // Set render pass descriptor
395 if (!renderTarget()) {
396 spdlog::error("[RHIImageItemRenderer::createPipeline]: No render target available");
397 return;
398 }
399
400 m_pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
401
402 // Enable alpha blending for proper image display
403 QRhiGraphicsPipeline::TargetBlend blend;
404 blend.enable = true;
405 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
406 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
407 blend.srcAlpha = QRhiGraphicsPipeline::One;
408 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
409 m_pipeline->setTargetBlends({blend});
410
411 // Use Triangles topology (explicit two triangles, more reliable than triangle strip)
412 m_pipeline->setTopology(QRhiGraphicsPipeline::Triangles);
413
414 // Create the pipeline
415 if (!m_pipeline->create()) {
416 spdlog::error("[RHIImageItemRenderer::createPipeline]: Failed to create graphics pipeline");
417 return;
418 }
419
420 spdlog::debug("[RHIImageItemRenderer::createPipeline]: Pipeline created successfully");
421}
422
423void RHIImageItemRenderer::updateTextureFromImage(const Core::Common::ImageRegion& image)
424{
425 const int w = static_cast<int>(image.width());
426 const int h = static_cast<int>(image.height());
427
428 spdlog::debug("[RHIImageItemRenderer::updateTextureFromImage]: Converting {}x{} (channels: {})",
429 w, h, image.channels());
430
431 // Resize pixel data buffer (RGBA8 = 4 bytes per pixel)
432 m_pixel_data.resize(static_cast<size_t>(w) * static_cast<size_t>(h) * 4);
433 m_pixel_data_size = QSize(w, h);
434
435 // Convert from RGBA_F32 (float) to RGBA8 (uint8)
436 const float* src { image.getBuffer().data() };
437 uint8_t* dst { m_pixel_data.data() };
438 const size_t total_pixels { static_cast<size_t>(w) * static_cast<size_t>(h) };
439
440 for (size_t i = 0; i < total_pixels; ++i)
441 {
442 const size_t idx = i * 4;
443
444 // Clamp to [0,1] range and convert to [0,255]
445 dst[idx + 0] = static_cast<uint8_t>(std::clamp(src[idx + 0], 0.0f, 1.0f) * 255.0f); // R
446 dst[idx + 1] = static_cast<uint8_t>(std::clamp(src[idx + 1], 0.0f, 1.0f) * 255.0f); // G
447 dst[idx + 2] = static_cast<uint8_t>(std::clamp(src[idx + 2], 0.0f, 1.0f) * 255.0f); // B
448 dst[idx + 3] = static_cast<uint8_t>(std::clamp(src[idx + 3], 0.0f, 1.0f) * 255.0f); // A
449 }
450
451 // Recreate texture if size changed
452 if (!m_texture || m_texture->pixelSize() != m_pixel_data_size)
453 {
454 m_texture.reset(rhi()->newTexture(QRhiTexture::RGBA8, m_pixel_data_size));
455
456 if (!m_texture->create()) {
457 spdlog::error("[RHIImageItemRenderer::updateTextureFromImage]: Failed to create texture {}x{}",
458 w, h);
459 return;
460 }
461
462 spdlog::debug("[RHIImageItemRenderer::updateTextureFromImage]: Texture created {}x{}", w, h);
463
464 // Recreate shader resource bindings with new texture
465 m_srb.reset(rhi()->newShaderResourceBindings());
466
467 QRhiShaderResourceBinding bindings[] = {
468 QRhiShaderResourceBinding::uniformBuffer(
469 0,
470 QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
471 m_uniform_buffer.get()
472 ),
473 QRhiShaderResourceBinding::sampledTexture(
474 1,
475 QRhiShaderResourceBinding::FragmentStage,
476 m_texture.get(),
477 m_sampler.get()
478 )
479 };
480
481 m_srb->setBindings(bindings, bindings + 2);
482
483 if (!m_srb->create()) {
484 spdlog::error("[RHIImageItemRenderer::updateTextureFromImage]: Failed to recreate SRB");
485 }
486 }
487}
488
489} // namespace CaptureMoment::UI::Rendering
Modern QQuickItem that renders an image via RHI (Vulkan/Metal/DX12/OpenGL) using QQuickRhiItem.
void render(QRhiCommandBuffer *cb) override
Performs the actual GPU rendering.
void synchronize(QQuickRhiItem *item) override
Synchronizes data from the GUI thread to the render thread.
void initialize(QRhiCommandBuffer *cb) override
Initializes all GPU resources when the RHI context is ready.
RHIImageItemRenderer(RHIImageItem *item)
Constructs the renderer with a reference to its parent item.
~RHIImageItemRenderer() override
Destructor releasing all GPU resources.
Defines the ImageRegion structure for representing pixel buffers in the CaptureMoment Core library.
@ blend
Blend clipped areas with surrounding unclipped data. Natural-looking highlights.
Namespace containing Qt-specific UI components for CaptureMoment.
Modern RHI-based image display (Qt6 - Vulkan/Metal/DX12 compatible) using QQuickRhiItem.
RHI renderer implementation for hardware-accelerated image display.
CaptureMoment — footer