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