MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Encoder.cpp
Go to the documentation of this file.
1#include "Encoder.hpp"
2
4
7
10
12
14
15namespace MayaFlux::Nexus {
16
17namespace {
18
19 // -------------------------------------------------------------------------
20 // State::Range helpers
21 // -------------------------------------------------------------------------
22
23 void expand_range(State::Range& r, float value, bool& initialized)
24 {
25 if (!initialized) {
26 r.min = r.max = value;
27 initialized = true;
28 } else {
29 r.min = std::min(r.min, value);
30 r.max = std::max(r.max, value);
31 }
32 }
33
34 float normalize(float value, const State::Range& r)
35 {
36 if (r.max <= r.min) {
37 return 0.0F;
38 }
39 return (value - r.min) / (r.max - r.min);
40 }
41
42 // -------------------------------------------------------------------------
43 // Wiring builder
44 // -------------------------------------------------------------------------
45
46 State::WiringRecord build_wiring(const Fabric& fabric, uint32_t id)
47 {
48 const Wiring* w = fabric.wiring_for(id);
49 if (!w)
50 return { .kind = State::WiringKind::Unsupported };
51
52 if (!w->move_steps().empty()) {
53 std::vector<State::WiringStep> steps;
54 steps.reserve(w->move_steps().size());
55 for (const auto& s : w->move_steps())
56 steps.push_back({ .position = s.position, .delay_seconds = s.delay_seconds });
57 State::WiringRecord rec { .kind = State::WiringKind::MoveTo, .steps = std::move(steps) };
58 if (w->times_count() > 1)
59 rec.times = w->times_count();
60
61 return rec;
62 }
63
64 if (w->interval().has_value()) {
65 State::WiringRecord rec { .kind = State::WiringKind::Every, .interval = w->interval() };
66 rec.duration = w->duration();
67 if (w->times_count() > 1)
68 rec.times = w->times_count();
69
70 return rec;
71 }
72
73 if (w->is_scroll())
74 return { .kind = State::WiringKind::Scroll };
75
76 return { .kind = State::WiringKind::CommitDriven };
77 }
78
79 void fill_wiring_pixels(const Fabric& fabric, uint32_t id, float& trigger_out, float& time_out)
80 {
81 const Wiring* w = fabric.wiring_for(id);
82 trigger_out = 0.0F;
83 time_out = 0.0F;
84 if (!w)
85 return;
86 if (!w->move_steps().empty()) {
87 time_out = 1.0F;
88 } else if (w->interval().has_value()) {
89 trigger_out = 0.2F;
90 if (w->duration().has_value())
91 time_out = 0.5F;
92 }
93 }
94
95} // namespace
96
97bool StateEncoder::encode(const Fabric& fabric, const std::string& base_path)
98{
99 m_last_error.clear();
100
101 // -------------------------------------------------------------------------
102 // Collect encodable entities.
103 // -------------------------------------------------------------------------
104 struct InternalRecord {
105 uint32_t id;
106 Fabric::Kind kind;
107 glm::vec3 position {};
108 float intensity { 0.0F };
109 float radius { 0.0F };
110 float query_radius { 0.0F };
111 std::optional<glm::vec3> color;
112 std::optional<float> size;
113 std::string influence_fn_name;
114 std::string perception_fn_name;
115 // Layer 0
116 float entity_type_norm { 0.0F };
117 float trigger_kind { 0.0F };
118 float time_kind { 0.0F };
119 // Layer 2
120 uint32_t sink_type { 0 };
121 uint32_t first_audio_channel { 0 };
122 };
123
124 std::vector<InternalRecord> records;
125
126 for (uint32_t id : fabric.all_ids()) {
127 const auto k = fabric.kind(id);
128 switch (k) {
130 auto e = fabric.get_emitter(id);
131 if (!e || !e->position()) {
132 continue;
133 }
134 if (e->fn_name().empty()) {
136 "StateEncoder: Emitter {} has no fn_name", id);
137 }
138 auto& rec = records.emplace_back(InternalRecord {
139 .id = id,
140 .kind = k,
141 .position = *e->position(),
142 .intensity = e->intensity(),
143 .radius = e->radius(),
144 .color = e->color(),
145 .size = e->size(),
146 .influence_fn_name = e->fn_name(),
147 .entity_type_norm = 0.0F,
148 });
149 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
150 rec.sink_type = (e->audio_sinks().empty() ? 0U : 1U)
151 | (e->render_sinks().empty() ? 0U : 2U);
152 if (!e->audio_sinks().empty())
153 rec.first_audio_channel = e->audio_sinks().front().channel;
154 break;
155 }
157 auto s = fabric.get_sensor(id);
158 if (!s || !s->position()) {
159 continue;
160 }
161 if (s->fn_name().empty()) {
163 "StateEncoder: Sensor {} has no fn_name", id);
164 }
165 auto& rec = records.emplace_back(InternalRecord {
166 .id = id,
167 .kind = k,
168 .position = *s->position(),
169 .query_radius = s->query_radius(),
170 .perception_fn_name = s->fn_name(),
171 .entity_type_norm = 0.333F,
172 });
173 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
174 break;
175 }
176 case Fabric::Kind::Agent: {
177 auto a = fabric.get_agent(id);
178 if (!a || !a->position()) {
179 continue;
180 }
181 if (a->perception_fn_name().empty()) {
183 "StateEncoder: Agent {} has no perception_fn_name", id);
184 }
185 if (a->influence_fn_name().empty()) {
187 "StateEncoder: Agent {} has no influence_fn_name", id);
188 }
189 auto& rec = records.emplace_back(InternalRecord {
190 .id = id,
191 .kind = k,
192 .position = *a->position(),
193 .intensity = a->intensity(),
194 .radius = a->radius(),
195 .query_radius = a->query_radius(),
196 .color = a->color(),
197 .size = a->size(),
198 .influence_fn_name = a->influence_fn_name(),
199 .perception_fn_name = a->perception_fn_name(),
200 .entity_type_norm = 0.667F,
201 });
202 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
203 rec.sink_type = (a->audio_sinks().empty() ? 0U : 1U)
204 | (a->render_sinks().empty() ? 0U : 2U);
205 if (!a->audio_sinks().empty())
206 rec.first_audio_channel = a->audio_sinks().front().channel;
207 break;
208 }
209 }
210 }
211
212 if (records.empty()) {
213 m_last_error = "No entities with positions to encode";
215 return false;
216 }
217
218 // -------------------------------------------------------------------------
219 // Compute per-field ranges.
220 // -------------------------------------------------------------------------
222 bool init_pos_x = false, init_pos_y = false, init_pos_z = false;
223 bool init_intensity = false, init_radius = false, init_query_radius = false;
224 bool init_color_r = false, init_color_g = false, init_color_b = false;
225 bool init_size = false;
226
227 for (const auto& rec : records) {
228 expand_range(rs.pos_x, rec.position.x, init_pos_x);
229 expand_range(rs.pos_y, rec.position.y, init_pos_y);
230 expand_range(rs.pos_z, rec.position.z, init_pos_z);
231
232 if (rec.kind == Fabric::Kind::Emitter || rec.kind == Fabric::Kind::Agent) {
233 expand_range(rs.intensity, rec.intensity, init_intensity);
234 expand_range(rs.radius, rec.radius, init_radius);
235 if (rec.color) {
236 expand_range(rs.color_r, rec.color->r, init_color_r);
237 expand_range(rs.color_g, rec.color->g, init_color_g);
238 expand_range(rs.color_b, rec.color->b, init_color_b);
239 }
240 if (rec.size) {
241 expand_range(rs.size, *rec.size, init_size);
242 }
243 }
244 if (rec.kind == Fabric::Kind::Sensor || rec.kind == Fabric::Kind::Agent) {
245 expand_range(rs.query_radius, rec.query_radius, init_query_radius);
246 }
247 }
248
249 // -------------------------------------------------------------------------
250 // Build RGBA32F pixel buffer.
251 // -------------------------------------------------------------------------
252 const auto width = static_cast<uint32_t>(records.size());
253
255 image.width = width;
256 image.height = State::k_exr_rows;
257 image.channels = State::k_channels;
259
260 std::vector<float> pixels(static_cast<size_t>(width) * State::k_exr_rows * State::k_channels, 0.0F);
261
262 for (size_t i = 0; i < records.size(); ++i) {
263 const auto& rec = records[i];
264
265 const size_t row0 = (static_cast<size_t>(0) * width + i) * State::k_channels;
266 pixels[row0 + 0] = normalize(rec.position.x, rs.pos_x);
267 pixels[row0 + 1] = normalize(rec.position.y, rs.pos_y);
268 pixels[row0 + 2] = normalize(rec.position.z, rs.pos_z);
269 pixels[row0 + 3] = normalize(rec.intensity, rs.intensity);
270
271 const size_t row1 = (static_cast<size_t>(1) * width + i) * State::k_channels;
272 if (rec.color) {
273 pixels[row1 + 0] = normalize(rec.color->r, rs.color_r);
274 pixels[row1 + 1] = normalize(rec.color->g, rs.color_g);
275 pixels[row1 + 2] = normalize(rec.color->b, rs.color_b);
276 }
277 if (rec.size) {
278 pixels[row1 + 3] = normalize(*rec.size, rs.size);
279 }
280
281 const size_t row2 = (static_cast<size_t>(2) * width + i) * State::k_channels;
282 pixels[row2 + 0] = normalize(rec.radius, rs.radius);
283 pixels[row2 + 1] = normalize(rec.query_radius, rs.query_radius);
284
285 const size_t row3 = (static_cast<size_t>(3) * width + i) * State::k_channels;
286 pixels[row3 + 0] = static_cast<float>(rec.id);
287 pixels[row3 + 1] = rec.entity_type_norm;
288 pixels[row3 + 2] = rec.trigger_kind;
289 pixels[row3 + 3] = rec.time_kind;
290
291 const size_t row4 = (static_cast<size_t>(4) * width + i) * State::k_channels;
292 pixels[row4 + 0] = static_cast<float>(rec.sink_type);
293 pixels[row4 + 1] = static_cast<float>(rec.first_audio_channel);
294 }
295
296 image.pixels = std::move(pixels);
297
298 // -------------------------------------------------------------------------
299 // Write EXR.
300 // -------------------------------------------------------------------------
301 const std::string exr_path = base_path + ".exr";
302 auto writer = IO::ImageWriterRegistry::instance().create_writer(exr_path);
303 if (!writer) {
304 m_last_error = "No ImageWriter registered for .exr";
306 return false;
307 }
308
309 IO::ImageWriteOptions options;
310 options.channel_names = { "R", "G", "B", "A" };
311
312 if (!writer->write(exr_path, image, options)) {
313 m_last_error = "EXR write failed: " + writer->get_last_error();
315 return false;
316 }
317
318 // -------------------------------------------------------------------------
319 // Build and write schema.
320 // -------------------------------------------------------------------------
321 State::FabricSchema schema;
323 schema.fabric_name = fabric.name();
324 schema.ranges = rs;
325 schema.entities.reserve(records.size());
326
327 for (const auto& rec : records) {
329 ent.id = rec.id;
330 ent.kind = State::kind_to_string(rec.kind);
331 ent.position = rec.position;
332 ent.intensity = rec.intensity;
333 ent.radius = rec.radius;
334 ent.query_radius = rec.query_radius;
335 ent.color = rec.color;
336 ent.size = rec.size;
337 ent.influence_fn_name = rec.influence_fn_name;
338 ent.perception_fn_name = rec.perception_fn_name;
339 ent.wiring = build_wiring(fabric, rec.id);
340
341 if (rec.kind == Fabric::Kind::Emitter) {
342 auto e = fabric.get_emitter(rec.id);
343 for (const auto& s : e->audio_sinks())
344 ent.audio_sinks.push_back({ .channel = s.channel, .fn_name = s.fn_name });
345 for (const auto& s : e->render_sinks())
346 ent.render_sinks.push_back({ .fn_name = s.fn_name });
347 } else if (rec.kind == Fabric::Kind::Agent) {
348 auto a = fabric.get_agent(rec.id);
349 for (const auto& s : a->audio_sinks())
350 ent.audio_sinks.push_back({ .channel = s.channel, .fn_name = s.fn_name });
351
352 for (const auto& s : a->render_sinks())
353 ent.render_sinks.push_back({ .fn_name = s.fn_name });
354
355 if (auto locus = std::dynamic_pointer_cast<Locus>(a)) {
356 ent.subkind = "locus";
357 const auto& nav = locus->nav();
358 ent.locus_nav = State::LocusNavRecord {
359 .eye = nav.eye,
360 .target = nav.eye + glm::vec3 { std::cos(nav.pitch) * std::sin(nav.yaw), std::sin(nav.pitch), std::cos(nav.pitch) * std::cos(nav.yaw) },
361 .up = { 0.0F, 1.0F, 0.0F },
362 .fov = nav.fov_radians,
363 .near_plane = nav.near_plane,
364 .far_plane = nav.far_plane,
365 .speed = nav.move_speed,
366 };
367 } else if (auto presence = std::dynamic_pointer_cast<Presence>(a)) {
368 ent.subkind = "presence";
369 ent.radiate_fn_name = presence->radiate_fn_name();
370 ent.falloff_radius = presence->falloff_radius() != presence->query_radius()
371 ? std::optional<float>(presence->falloff_radius())
372 : std::nullopt;
373 if (auto fc = presence->falloff_curve())
374 ent.falloff_curve_name = Reflect::enum_to_lowercase_string(*fc);
375 }
376 }
377
378 schema.entities.push_back(std::move(ent));
379 }
380
381 for (uint32_t xid : fabric.all_expanse_ids()) {
382 const auto x = fabric.get_expanse(xid);
383 if (!x)
384 continue;
385 if (x->fn_name().empty()) {
387 "StateEncoder: Expanse {} has no fn_name, skipping", xid);
388 continue;
389 }
390 schema.expanses.push_back(State::ExpanseRecord {
391 .id = xid,
392 .fn_name = x->fn_name(),
393 .on_enter_fn_name = x->on_enter_fn_name(),
394 .on_exit_fn_name = x->on_exit_fn_name(),
395 });
396 }
397
398 IO::JSONSerializer ser;
399 const std::string json_path = base_path + ".json";
400 if (!ser.write(json_path, schema)) {
401 m_last_error = "Failed to write schema: " + ser.last_error();
403 return false;
404 }
405
407 "StateEncoder: wrote {} entities to {} + {}",
408 records.size(), exr_path, json_path);
409
410 return true;
411}
412
413bool StateEncoder::encode(const Tapestry& tapestry, const std::string& base_dir, nlohmann::json user_state)
414{
415 m_last_error.clear();
416
418
419 for (const auto& fabric : tapestry.all_fabrics()) {
420 const std::string fabric_id = fabric->name().empty()
421 ? std::to_string(fabric->id())
422 : fabric->name();
423
424 const std::string base_path = base_dir + "/" + fabric_id;
425
426 if (!encode(*fabric, base_path)) {
427 return false;
428 }
429
430 schema.fabrics.push_back(State::FabricRef {
431 .name = fabric_id,
432 .base_path = base_path,
433 });
434 }
435
436 for (const auto& [xname, xptr] : tapestry.all_expanses()) {
438 .name = xname,
439 .fn_name = xptr->fn_name(),
440 .on_enter_fn_name = xptr->on_enter_fn_name(),
441 .on_exit_fn_name = xptr->on_exit_fn_name(),
442 };
443 for (const auto& fabric : tapestry.all_fabrics()) {
444 for (uint32_t xid : fabric->all_expanse_ids()) {
445 if (fabric->get_expanse(xid) == xptr) {
446 const std::string fname = fabric->name().empty()
447 ? std::to_string(fabric->id())
448 : fabric->name();
449 xrec.fabric_names.push_back(fname);
450 break;
451 }
452 }
453 }
454 schema.expanses.push_back(std::move(xrec));
455 }
456
457 schema.user_state = std::move(user_state);
458
460 const std::string tapestry_path = base_dir + "/tapestry.json";
461 if (!ser.write(tapestry_path, schema)) {
462 m_last_error = "Failed to write tapestry schema: " + ser.last_error();
464 return false;
465 }
466
468 "StateEncoder: wrote {} fabrics to {}", schema.fabrics.size(), tapestry_path);
469 return true;
470}
471
472} // namespace MayaFlux::Nexus
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
IO::ImageData image
Definition Decoder.cpp:64
uint32_t width
Definition Decoder.cpp:66
const std::vector< float > * pixels
Definition Decoder.cpp:65
size_t a
float value
uint32_t radius
float k
std::unique_ptr< ImageWriter > create_writer(const std::string &filepath) const
static ImageWriterRegistry & instance()
const std::string & last_error() const
Last error message, empty if no error.
bool write(const std::string &path, const T &value, int indent=2)
Encode value and write to path (created or truncated).
Converts arbitrary C++ types to/from JSON strings and disk files.
std::shared_ptr< Sensor > get_sensor(uint32_t id) const
Get the Sensor registered under id.
Definition Fabric.cpp:150
std::vector< uint32_t > all_ids() const
List all registered entity ids in insertion order.
Definition Fabric.cpp:113
std::shared_ptr< Agent > get_agent(uint32_t id) const
Get the Agent registered under id.
Definition Fabric.cpp:161
const std::string & name() const
Assigned name, empty if the Fabric was constructed outside a Tapestry.
Definition Fabric.hpp:67
Kind kind(uint32_t id) const
Return the kind of entity registered under id.
Definition Fabric.cpp:123
uint32_t id() const
Stable id for this Fabric, assigned by Tapestry at construction.
Definition Fabric.hpp:77
std::shared_ptr< Emitter > get_emitter(uint32_t id) const
Get the Emitter registered under id.
Definition Fabric.cpp:139
Orchestrates spatial indexing and scheduling for Nexus objects.
Definition Fabric.hpp:38
bool encode(const Fabric &fabric, const std::string &base_path)
Encode the given Fabric to {base_path}.exr and {base_path}.json.
Definition Encoder.cpp:97
const std::vector< std::shared_ptr< Fabric > > & all_fabrics() const
All woven Fabrics, named and unnamed.
Definition Tapestry.cpp:79
const std::unordered_map< std::string, std::shared_ptr< Expanse > > & all_expanses() const
Read-only view of all Expanses owned by this Tapestry.
Definition Tapestry.hpp:83
Owner of one or more Fabrics and the shared state they rely on.
Definition Tapestry.hpp:25
@ FileIO
Filesystem I/O operations.
@ Nexus
Spatial indexing and scheduling for user-defined behaviour.
constexpr uint32_t k_schema_version
Current schema version written by StateEncoder and accepted by StateDecoder.
Definition Schema.hpp:20
constexpr uint32_t k_exr_rows
RGBA32F EXR layout constants shared between encoder and decoder.
Definition Schema.hpp:31
constexpr uint32_t k_channels
Definition Schema.hpp:32
std::string kind_to_string(Fabric::Kind k)
Map Fabric::Kind to its lowercase JSON string token via magic_enum.
Definition Schema.hpp:357
@ RGBA32F
Four channel 32-bit float.
std::string enum_to_lowercase_string(EnumType value) noexcept
Universal enum to lowercase string converter using magic_enum.
void normalize(std::vector< double > &data, double target_peak)
Normalize single-channel data to specified peak level (in-place)
Definition Yantra.cpp:565
Raw image data loaded from file.
std::vector< std::string > channel_names
Configuration for image writing.
std::optional< float > size
Definition Schema.hpp:203
std::optional< glm::vec3 > color
Definition Schema.hpp:202
std::vector< AudioSinkRecord > audio_sinks
Definition Schema.hpp:208
std::vector< RenderSinkRecord > render_sinks
Definition Schema.hpp:209
Per-entity JSON record.
Definition Schema.hpp:194
Entry in the Tapestry envelope pointing to one Fabric's EXR+JSON pair.
Definition Schema.hpp:295
std::vector< EntityRecord > entities
Definition Schema.hpp:273
Tapestry-level named Expanse record.
Definition Schema.hpp:316
std::vector< FabricRef > fabrics
Definition Schema.hpp:336
std::vector< TapestryExpanseRecord > expanses
Definition Schema.hpp:337