Loading...
Searching...
No Matches
CubemapComposer.hpp
1#pragma once
2
3#include <halp/controls.hpp>
4#include <Gfx/Graph/MipGeneration.hpp>
5#include <halp/meta.hpp>
6#include <halp/texture.hpp>
7
8#include <Gfx/Graph/RenderList.hpp>
9
10#include <ossia/dataflow/geometry_port.hpp>
11
12#include <QtGui/private/qrhi_p.h>
13
14#include <cstdint>
15#include <memory>
16
17namespace Threedim
18{
19
21{
22public:
23 halp_meta(name, "Cubemap Composer")
24 halp_meta(category, "Visuals/3D")
25 halp_meta(c_name, "cubemap_composer")
26 halp_meta(description, "Combine six face textures into a cubemap and scene skybox contribution.")
27 halp_meta(
28 manual_url,
29 "https://ossia.io/score-docs/processes/cubemap-composer.html")
30 halp_meta(uuid, "a7c3e8f1-5b2d-4a9e-8f3c-1d6e9b4a2c5f")
31
32 struct ins
33 {
34 halp::texture_input<"+X", halp::custom_variable_texture> pos_x;
35 halp::texture_input<"-X", halp::custom_variable_texture> neg_x;
36 halp::texture_input<"+Y", halp::custom_variable_texture> pos_y;
37 halp::texture_input<"-Y", halp::custom_variable_texture> neg_y;
38 halp::texture_input<"+Z", halp::custom_variable_texture> pos_z;
39 halp::texture_input<"-Z", halp::custom_variable_texture> neg_z;
40 } inputs;
41
42 struct
43 {
44 halp::gpu_cubemap_output<"Cubemap"> cubemap;
45 // Scene-graph route: emits a scene_spec whose environment.skybox_texture
46 // points at our cube handle. See CubemapLoader for the same pattern.
47 struct
48 {
49 halp_meta(name, "Scene");
50 ossia::scene_spec scene;
51 uint8_t dirty{0};
52 } scene_out;
53 } outputs;
54
55 // Per-face shape cache. Drives texture-recreation when face size changes.
56 // Content-change detection uses the producer's `changed` flag instead of
57 // a bytes-pointer compare: pointer identity does not detect in-place
58 // buffer updates (ring-buffer video readback reuses the address).
60 {
61 int width{0};
62 int height{0};
63 };
64
65 QRhiTexture* m_cubemapTex{};
66 int m_faceSize{0};
67 bool m_dirty{true};
68 FaceFingerprint m_lastFaces[6]{};
69 std::shared_ptr<ossia::scene_state> m_sceneState;
70 int64_t m_sceneVersion{0};
71 void* m_lastPublishedHandle{};
72
73 // Dtor safety net — same rationale as CubemapLoader: guarantees the
74 // VkImage is deleteLater'd even if release(RenderList&) was skipped,
75 // so QRhi's destructor drains the pending-delete list before
76 // vkDestroyDevice. Without this, Vulkan validation reports a leaked
77 // VkImage on exit.
78 ~CubemapComposer()
79 {
80 if(m_cubemapTex)
81 {
82 m_cubemapTex->deleteLater();
83 m_cubemapTex = nullptr;
84 }
85 }
86
87 void operator()() { }
88
89 void init(score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res)
90 {
91 m_dirty = true;
92 }
93
94 void update(
95 score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res,
97 {
98 // Face size comes from the largest input; content changes come from the
99 // producer's `changed` flag (set by halp::texture's update() — see
100 // avendish texture_formats.hpp), cleared here on consumption. Size is
101 // tracked separately so a producer that resizes a face still triggers a
102 // texture recreation even when it forgot to toggle `changed`.
103 int maxSize = 0;
104 int faceIdx = 0;
105 auto checkFace = [&](auto& tex) {
106 FaceFingerprint cur{tex.texture.width, tex.texture.height};
107 const bool sizeChanged
108 = (cur.width != m_lastFaces[faceIdx].width
109 || cur.height != m_lastFaces[faceIdx].height);
110 const bool contentChanged = tex.texture.changed;
111 if(sizeChanged || contentChanged)
112 {
113 m_lastFaces[faceIdx] = cur;
114 m_dirty = true;
115 }
116 tex.texture.changed = false; // consumed; producer will set it on next update()
117 ++faceIdx;
118 if(tex.texture.bytes && tex.texture.width > 0 && tex.texture.height > 0)
119 {
120 int s = std::max(tex.texture.width, tex.texture.height);
121 maxSize = std::max(maxSize, s);
122 }
123 };
124 checkFace(inputs.pos_x);
125 checkFace(inputs.neg_x);
126 checkFace(inputs.pos_y);
127 checkFace(inputs.neg_y);
128 checkFace(inputs.pos_z);
129 checkFace(inputs.neg_z);
130
131 if(maxSize <= 0)
132 return;
133
134 // Recreate texture if size changed
135 if(maxSize != m_faceSize)
136 {
137 if(m_cubemapTex)
138 {
139 m_cubemapTex->deleteLater();
140 m_cubemapTex = nullptr;
141 }
142 m_faceSize = maxSize;
143 m_dirty = true;
144 }
145
146 if(!m_cubemapTex && m_faceSize > 0)
147 {
148 auto& rhi = *renderer.state.rhi;
149 m_cubemapTex = rhi.newTexture(
150 QRhiTexture::RGBA8, QSize{m_faceSize, m_faceSize}, 1,
151 QRhiTexture::CubeMap | QRhiTexture::MipMapped
152 | QRhiTexture::UsedWithGenerateMips);
153 m_cubemapTex->create();
154 outputs.cubemap.texture.handle = m_cubemapTex;
155 m_dirty = true;
156 }
157
158 // Publish the cube on the Scene outlet (skybox_texture only — other
159 // environment fields are left for EnvironmentLoader / elsewhere to
160 // populate, merge_scenes overlays field-by-field).
161 if(!m_sceneState)
162 m_sceneState = std::make_shared<ossia::scene_state>();
163 if(m_lastPublishedHandle != m_cubemapTex)
164 {
165 m_sceneState->environment = {};
166 m_sceneState->environment.skybox_texture.native_handle = m_cubemapTex;
167 m_lastPublishedHandle = m_cubemapTex;
168 m_sceneVersion++;
169 m_sceneState->version = m_sceneVersion;
170 outputs.scene_out.scene.state = m_sceneState;
171 outputs.scene_out.dirty = ossia::scene_port::dirty_environment;
172 }
173 }
174
175 void release(score::gfx::RenderList& r)
176 {
177 if(m_cubemapTex)
178 {
179 m_cubemapTex->deleteLater();
180 m_cubemapTex = nullptr;
181 }
182 m_faceSize = 0;
183 outputs.cubemap.texture.handle = nullptr;
184 if(m_sceneState)
185 {
186 m_sceneState->environment = {};
187 m_lastPublishedHandle = nullptr;
188 m_sceneVersion++;
189 m_sceneState->version = m_sceneVersion;
190 outputs.scene_out.dirty = ossia::scene_port::dirty_environment;
191 }
192 }
193
194 void runInitialPasses(
195 score::gfx::RenderList& renderer, QRhiCommandBuffer& commands,
196 QRhiResourceUpdateBatch*& res, score::gfx::Edge& edge)
197 {
198 if(!m_cubemapTex)
199 return;
200
201 if(!m_dirty)
202 return;
203
204 bool anyUploaded = false;
205
206 // Upload each face that has valid data
207 auto uploadFace = [&](const auto& tex, int layer) {
208 if(!tex.texture.bytes || tex.texture.width <= 0 || tex.texture.height <= 0)
209 return;
210
211 // Convert to QImage, scale to face size if needed
212 QImage img(
213 tex.texture.bytes, tex.texture.width, tex.texture.height,
214 QImage::Format_RGBA8888);
215
216 if(img.width() != m_faceSize || img.height() != m_faceSize)
217 img = img.scaled(
218 m_faceSize, m_faceSize, Qt::IgnoreAspectRatio,
219 Qt::SmoothTransformation);
220
221 img = img.convertToFormat(QImage::Format_RGBA8888);
222
223 QRhiTextureSubresourceUploadDescription subresDesc(img);
224 QRhiTextureUploadEntry entry(layer, 0, subresDesc);
225 QRhiTextureUploadDescription desc({entry});
226 res->uploadTexture(m_cubemapTex, desc);
227 anyUploaded = true;
228 };
229
230 uploadFace(inputs.pos_x, 0); // +X
231 uploadFace(inputs.neg_x, 1); // -X
232 uploadFace(inputs.pos_y, 2); // +Y
233 uploadFace(inputs.neg_y, 3); // -Y
234 uploadFace(inputs.pos_z, 4); // +Z
235 uploadFace(inputs.neg_z, 5); // -Z
236
237 if(anyUploaded)
238 {
239 score::gfx::generateMipsIfAny(*res, m_cubemapTex);
240 }
241
242 m_dirty = false;
243 }
244};
245
246}
Definition CubemapComposer.hpp:21
List of nodes to be rendered to an output.
Definition RenderList.hpp:36
void generateMipsIfAny(QRhiResourceUpdateBatch &res, QRhiTexture *tex)
Definition MipGeneration.hpp:12
Definition CubemapComposer.hpp:60
Definition CubemapComposer.hpp:33
Connection between two score::gfx::Port.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:236