MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SpatialExport.cpp
Go to the documentation of this file.
1#include "SpatialExport.hpp"
2
10
12
13namespace MayaFlux::IO {
14
15namespace {
16
17 /**
18 * @brief Read every vertex, whole, into SpatialSample-ready columns.
19 *
20 * A GraphicsOperator's raw vertex buffer is Kakshya::Vertex records:
21 * PointVertex/LineVertex/MeshVertex all share its exact 60-byte layout,
22 * and Vertex's own doc calls it "pipeline-ready" for exactly this.
23 * position becomes SpatialSample::positions; color, scalar, uv, normal,
24 * and tangent each become their own named attribute (scalar carries
25 * size/thickness/weight depending on the concrete vertex type, per
26 * Vertex's own doc).
27 *
28 * @return False if @p layout is not this 60-byte record (no known
29 * GraphicsOperator today produces anything else); positions and
30 * attributes are left untouched in that case.
31 */
32 bool extract_vertex_columns(
33 const Kakshya::VertexLayout& layout,
34 std::span<const uint8_t> raw,
35 size_t vertex_count,
36 std::vector<glm::vec3>& positions,
37 std::vector<SpatialAttribute>& attributes)
38 {
39 if (layout.stride_bytes != sizeof(Kakshya::Vertex)) {
40 return false;
41 }
42
43 const auto* verts = reinterpret_cast<const Kakshya::Vertex*>(raw.data());
44
45 positions.resize(vertex_count);
46 std::vector<glm::vec3> colors(vertex_count);
47 std::vector<float> scalars(vertex_count);
48 std::vector<glm::vec2> uvs(vertex_count);
49 std::vector<glm::vec3> normals(vertex_count);
50 std::vector<glm::vec3> tangents(vertex_count);
51
52 for (size_t i = 0; i < vertex_count; ++i) {
53 positions[i] = verts[i].position;
54 colors[i] = verts[i].color;
55 scalars[i] = verts[i].scalar;
56 uvs[i] = verts[i].uv;
57 normals[i] = verts[i].normal;
58 tangents[i] = verts[i].tangent;
59 }
60
61 attributes.push_back(SpatialAttribute { .name = "color", .scope = SpatialScope::Varying,
62 .values = Kakshya::DataVariant { std::move(colors) } });
63 attributes.push_back(SpatialAttribute { .name = "scalar", .scope = SpatialScope::Varying,
64 .values = Kakshya::DataVariant { std::move(scalars) } });
65 attributes.push_back(SpatialAttribute { .name = "uv", .scope = SpatialScope::Varying,
66 .values = Kakshya::DataVariant { std::move(uvs) } });
67 attributes.push_back(SpatialAttribute { .name = "normal", .scope = SpatialScope::Varying,
68 .values = Kakshya::DataVariant { std::move(normals) } });
69 attributes.push_back(SpatialAttribute { .name = "tangent", .scope = SpatialScope::Varying,
70 .values = Kakshya::DataVariant { std::move(tangents) } });
71
72 return true;
73 }
74
75 /**
76 * @brief Chunk a curve topology's flat vertex array into per-curve
77 * vertex counts.
78 * @param topology LINE_STRIP or LINE_LIST. Any other value is a
79 * caller error (not asserted here; the caller
80 * only reaches this for those two).
81 * @param cluster_ids build_cluster_ids()-shaped: one entry per vertex,
82 * contiguous runs of equal value in
83 * get_vertex_data() order.
84 * @return For LINE_STRIP, the run-length encoding of @p cluster_ids:
85 * one curve per contiguous cluster run, matching how
86 * PathOperator lays out each path as one such run (a single
87 * continuous interpolated strip). For LINE_LIST, fixed pairs
88 * (every count is 2), matching how TopologyOperator lays out
89 * each graph as independent expanded edges rather than one
90 * connected strip through the whole graph; cluster_ids is
91 * unused in this case since edge boundaries do not follow
92 * cluster boundaries. nullopt if @p vertex_count is odd for
93 * LINE_LIST, which no known producer of that topology should
94 * ever report.
95 */
96 std::optional<std::vector<int32_t>> curve_vertex_counts(
98 const std::vector<uint32_t>& cluster_ids,
99 size_t vertex_count)
100 {
102 std::vector<int32_t> counts;
103 size_t i = 0;
104 while (i < cluster_ids.size()) {
105 size_t j = i + 1;
106 while (j < cluster_ids.size() && cluster_ids[j] == cluster_ids[i]) {
107 ++j;
108 }
109 counts.push_back(static_cast<int32_t>(j - i));
110 i = j;
111 }
112 return counts;
113 }
114
115 if (vertex_count % 2 != 0) {
116 return std::nullopt;
117 }
118 return std::vector<int32_t>(vertex_count / 2, 2);
119 }
120
121} // namespace
122
124 const std::shared_ptr<Buffers::RelaxationGridBuffer>& grid,
125 float extent,
126 std::vector<glm::vec3>& positions,
127 std::vector<uint64_t>& ids)
128{
129 if (!grid) {
131 "relaxation_grid_positions: null grid");
132 return false;
133 }
134
135 const uint32_t width = grid->get_grid_width();
136 const uint32_t height = grid->get_grid_height();
137 const uint32_t cell_count = grid->get_cell_count();
138
139 positions.resize(cell_count);
140 ids.resize(cell_count);
141
142 for (uint32_t i = 0; i < cell_count; ++i) {
143 const uint32_t ix = i % width;
144 const uint32_t iy = i / width;
145
146 const float fx = (static_cast<float>(ix) + 0.5F) / static_cast<float>(width);
147 const float fy = (static_cast<float>(iy) + 0.5F) / static_cast<float>(height);
148
149 positions[i] = glm::vec3((fx * 2.0F - 1.0F) * extent, (fy * 2.0F - 1.0F) * extent, 0.0F);
150 ids[i] = i;
151 }
152
153 return true;
154}
155
157 SpatialCache& cache,
158 const std::string& stream_name,
160{
161 if (!op) {
163 "write_operator_sample: null operator");
164 return false;
165 }
166
167 const size_t vertex_count = op->get_vertex_count();
168 if (vertex_count == 0) {
170 "write_operator_sample: operator reports zero vertices");
171 return false;
172 }
173
174 const Kakshya::VertexLayout layout = op->get_vertex_layout();
175 const std::span<const uint8_t> raw = op->get_vertex_data();
176 if (layout.stride_bytes == 0 || raw.size() < static_cast<size_t>(layout.stride_bytes) * vertex_count) {
178 "write_operator_sample: vertex buffer smaller than layout implies");
179 return false;
180 }
181
182 std::vector<glm::vec3> positions;
183 std::vector<SpatialAttribute> attributes;
184 if (!extract_vertex_columns(layout, raw, vertex_count, positions, attributes)) {
186 "write_operator_sample: vertex layout is not a Kakshya::Vertex record");
187 return false;
188 }
189
190 std::vector<uint32_t> cluster_ids = op->build_cluster_ids();
191 if (cluster_ids.size() != vertex_count) {
193 "write_operator_sample: build_cluster_ids() count mismatch");
194 return false;
195 }
196
197 const auto topology = op->declared_topology().value_or(Portal::Graphics::PrimitiveTopology::POINT_LIST);
198 std::optional<std::vector<int32_t>> vertex_counts_per_curve;
201 vertex_counts_per_curve = curve_vertex_counts(topology, cluster_ids, vertex_count);
202 if (!vertex_counts_per_curve) {
204 "write_operator_sample: {} vertices cannot form LINE_LIST pairs", vertex_count);
205 return false;
206 }
207 }
208
209 attributes.push_back(SpatialAttribute {
210 .name = "cluster",
211 .scope = SpatialScope::Varying,
212 .values = Kakshya::DataVariant { std::move(cluster_ids) } });
213
214 for (auto& [name, values] : op->extract_vertex_attributes()) {
215 const size_t count = std::visit([](const auto& v) { return v.size(); }, values);
216 if (count != vertex_count) {
218 "write_operator_sample: extract_vertex_attributes() '{}' count mismatch", name);
219 return false;
220 }
221 attributes.push_back(SpatialAttribute {
222 .name = name,
223 .scope = SpatialScope::Varying,
224 .values = std::move(values) });
225 }
226
227 if (vertex_counts_per_curve) {
228 return cache.write(stream_name,
230 .topology = topology,
231 .positions = positions,
232 .vertex_counts_per_curve = *vertex_counts_per_curve,
233 .attributes = attributes });
234 }
235
236 std::vector<uint64_t> ids(vertex_count);
237 std::ranges::iota(ids, uint64_t { 0 });
238
239 std::vector<glm::vec3> velocities = op->extract_vertex_velocities();
240 if (!velocities.empty() && velocities.size() != vertex_count) {
242 "write_operator_sample: extract_vertex_velocities() count mismatch");
243 return false;
244 }
245
246 return cache.write(stream_name,
249 .positions = positions,
250 .ids = ids,
251 .velocities = velocities,
252 .attributes = attributes });
253}
254
255namespace {
256
257 /**
258 * @brief GPU-authoritative path: download vertex bytes straight from the
259 * buffer itself, since NetworkGeometryBuffer is a VKBuffer.
260 *
261 * Population size, record schema, and declared_topology() come from
262 * the still-CPU-tracked primary GraphicsOperator (these describe the
263 * buffer's contract, not its live GPU bytes, so they stay valid even
264 * once a GpuFieldOperator owns those bytes). A curve topology chunks
265 * on build_cluster_ids(), same as write_operator_sample(), since graph/
266 * path membership is structural and unaffected by what a GpuFieldOperator
267 * does to positions. Attaches a "cluster" attribute from the declared
268 * hash_cluster_id state field when present; velocities and any other
269 * per-rule state are left out, since no name or shape for them is known
270 * generically here.
271 */
272 bool write_network_geometry_buffer_gpu_sample(
273 SpatialCache& cache,
274 const std::string& stream_name,
275 const std::shared_ptr<Buffers::NetworkGeometryBuffer>& buffer,
277 {
278 const size_t vertex_count = graphics_op->get_vertex_count();
279 if (vertex_count == 0) {
281 "write_network_geometry_buffer_sample: operator reports zero vertices");
282 return false;
283 }
284
285 const Kakshya::VertexLayout layout = graphics_op->get_vertex_layout();
286 if (layout.stride_bytes != sizeof(Kakshya::Vertex)) {
288 "write_network_geometry_buffer_sample: vertex layout is not a Kakshya::Vertex record");
289 return false;
290 }
291
292 std::vector<uint8_t> raw(vertex_count * layout.stride_bytes);
293 std::shared_ptr<Buffers::VKBuffer> staging;
294 Buffers::download_from_gpu_async(buffer, raw.data(), raw.size(), staging);
295
296 std::vector<glm::vec3> positions;
297 std::vector<SpatialAttribute> attributes;
298 if (!extract_vertex_columns(layout, raw, vertex_count, positions, attributes)) {
300 "write_network_geometry_buffer_sample: vertex layout is not a Kakshya::Vertex record");
301 return false;
302 }
303
304 const auto topology = graphics_op->declared_topology().value_or(Portal::Graphics::PrimitiveTopology::POINT_LIST);
305 std::optional<std::vector<int32_t>> vertex_counts_per_curve;
308 std::vector<uint32_t> structural_cluster_ids = graphics_op->build_cluster_ids();
309 if (structural_cluster_ids.size() != vertex_count) {
311 "write_network_geometry_buffer_sample: build_cluster_ids() count mismatch");
312 return false;
313 }
314 vertex_counts_per_curve = curve_vertex_counts(topology, structural_cluster_ids, vertex_count);
315 if (!vertex_counts_per_curve) {
317 "write_network_geometry_buffer_sample: {} vertices cannot form LINE_LIST pairs", vertex_count);
318 return false;
319 }
320 }
321
322 if (buffer->has_state("hash_cluster_id")
323 && buffer->get_state_bytes("hash_cluster_id") == vertex_count * sizeof(uint32_t)) {
324 std::vector<uint32_t> cluster_ids(vertex_count);
326 buffer->read_state_slot("hash_cluster_id"), cluster_ids.data(),
327 cluster_ids.size() * sizeof(uint32_t), staging);
328 attributes.push_back(SpatialAttribute {
329 .name = "cluster",
330 .scope = SpatialScope::Varying,
331 .values = Kakshya::DataVariant { std::move(cluster_ids) } });
332 }
333
334 if (vertex_counts_per_curve) {
335 return cache.write(stream_name,
336 SpatialSample {
337 .topology = topology,
338 .positions = positions,
339 .vertex_counts_per_curve = *vertex_counts_per_curve,
340 .attributes = attributes });
341 }
342
343 std::vector<uint64_t> ids(vertex_count);
344 std::ranges::iota(ids, uint64_t { 0 });
345
346 return cache.write(stream_name,
347 SpatialSample {
349 .positions = positions,
350 .ids = ids,
351 .attributes = attributes });
352 }
353
354} // namespace
355
357 SpatialCache& cache,
358 const std::string& stream_name,
359 const std::shared_ptr<Buffers::NetworkGeometryBuffer>& buffer)
360{
361 if (!buffer) {
363 "write_network_geometry_buffer_sample: null buffer");
364 return false;
365 }
366
367 auto network = buffer->get_network();
368 if (!network) {
370 "write_network_geometry_buffer_sample: buffer has no network");
371 return false;
372 }
373
374 auto chain = network->get_operator_chain();
375
377 auto* graphics_op = dynamic_cast<Nodes::Network::GraphicsOperator*>(network->get_operator());
378 if (!graphics_op) {
380 "write_network_geometry_buffer_sample: network's primary operator is not a GraphicsOperator");
381 return false;
382 }
383 return write_network_geometry_buffer_gpu_sample(cache, stream_name, buffer, graphics_op);
384 }
385
386 std::vector<std::pair<std::string, Nodes::Network::GraphicsOperator*>> graphics_ops;
387 if (auto* primary = dynamic_cast<Nodes::Network::GraphicsOperator*>(network->get_operator())) {
388 graphics_ops.emplace_back(std::string(primary->get_type_name()), primary);
389 }
390 if (chain) {
391 for (const auto& op : chain->operators()) {
392 if (auto* secondary = dynamic_cast<Nodes::Network::GraphicsOperator*>(op.get())) {
393 graphics_ops.emplace_back(std::string(secondary->get_type_name()), secondary);
394 }
395 }
396 }
397
398 if (graphics_ops.empty()) {
400 "write_network_geometry_buffer_sample: network has no GraphicsOperator");
401 return false;
402 }
403
404 if (graphics_ops.size() == 1) {
405 return write_operator_sample(cache, stream_name, graphics_ops.front().second);
406 }
407
408 std::unordered_map<std::string, int> seen;
409 for (const auto& [type_name, op] : graphics_ops) {
410 int& occurrence = seen[type_name];
411 std::string set_name = stream_name;
412 set_name.append("_").append(type_name);
413 if (occurrence != 0) {
414 set_name.append(std::to_string(occurrence));
415 }
416 ++occurrence;
417
418 if (!write_operator_sample(cache, set_name, op)) {
419 return false;
420 }
421 }
422
423 return true;
424}
425
426} // namespace MayaFlux::IO
#define MF_ERROR(comp, ctx,...)
Core::GlobalNetworkConfig network
Definition Config.cpp:39
std::shared_ptr< NetworkGeometryBuffer > buffer
BufferProcessingChain * chain
std::vector< std::byte > values
Definition VDBWriter.cpp:26
std::string name
Definition VKDevice.cpp:143
size_t count
uint32_t width
uint32_t height
bool write(const std::string &stream_name, const SpatialSample &sample)
Append one sample to a named stream.
Alembic-backed writer for time-sampled spatial entity state: particle systems, point clouds,...
Chain operator that declares Tendency field deformation as a compute shader rather than evaluating it...
virtual std::vector< std::pair< std::string, Kakshya::DataVariant > > extract_vertex_attributes() const
Extra named, per-vertex attributes beyond position/color/size, for an external cache/export consumer.
virtual size_t get_vertex_count() const =0
Get number of vertices (may differ from point count for topology/path)
virtual std::vector< uint32_t > build_cluster_ids() const
Per-vertex collection index, global index order.
virtual Kakshya::VertexLayout get_vertex_layout() const =0
Get vertex layout describing vertex structure.
virtual std::optional< Portal::Graphics::PrimitiveTopology > declared_topology() const
Topology this operator's vertex data is authored for, if it holds a node to ask.
virtual std::vector< glm::vec3 > extract_vertex_velocities() const
Per-vertex velocity, for a consumer that wants to write it as a native channel (e....
virtual std::span< const uint8_t > get_vertex_data() const =0
Get vertex data for GPU upload.
Operator that produces GPU-renderable geometry.
void download_from_gpu_async(const std::shared_ptr< VKBuffer > &source, void *data, size_t size, std::shared_ptr< VKBuffer > &staging)
Download from a device-local GPU buffer without stalling the graphics queue.
void download_back_buffer(const VKBufferResources::GenerationSlot &slot, void *data, size_t size, std::shared_ptr< VKBuffer > &staging)
Download a raw back_buffers slot to host memory.
bool relaxation_grid_positions(const std::shared_ptr< Buffers::RelaxationGridBuffer > &grid, float extent, std::vector< glm::vec3 > &positions, std::vector< uint64_t > &ids)
Generate the fixed grid positions and cell ids a RelaxationGridBuffer's cells occupy,...
bool write_operator_sample(SpatialCache &cache, const std::string &stream_name, const Nodes::Network::GraphicsOperator *op)
Pack one GraphicsOperator's current vertex state into a SpatialCache stream, as a Points or Curves sa...
bool write_network_geometry_buffer_sample(SpatialCache &cache, const std::string &stream_name, const std::shared_ptr< Buffers::NetworkGeometryBuffer > &buffer)
Pack a NetworkGeometryBuffer's driving network into one or more SpatialCache streams,...
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
std::variant< std::vector< double >, std::vector< float >, std::vector< uint8_t >, std::vector< uint16_t >, std::vector< uint32_t >, std::vector< std::complex< float > >, std::vector< std::complex< double > >, std::vector< glm::vec2 >, std::vector< glm::vec3 >, std::vector< glm::vec4 >, std::vector< glm::mat4 > > DataVariant
Multi-type data storage for different precision needs.
Definition NDData.hpp:102
PrimitiveTopology
Vertex assembly primitive topology.
One named, scoped channel of already-typed values.
Portal::Graphics::PrimitiveTopology topology
One sample's worth of data for one named stream.
uint32_t stride_bytes
Total bytes per vertex (stride in Vulkan terms) e.g., 3 floats (position) + 3 floats (normal) = 24 by...
Complete description of vertex data layout in a buffer.