MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Decoder.cpp
Go to the documentation of this file.
1#include "Decoder.hpp"
2
4
7
9
11
12namespace MayaFlux::Nexus {
13
14namespace {
15
16 // -------------------------------------------------------------------------
17 // Versions and pixels
18 // -------------------------------------------------------------------------
19
20 bool version_supported(uint32_t version)
21 {
22 return version >= State::k_schema_min_version && version <= State::k_schema_version;
23 }
24
25 std::string version_error(uint32_t version)
26 {
27 return "Unsupported schema version: " + std::to_string(version)
28 + " (supported " + std::to_string(State::k_schema_min_version)
29 + " to " + std::to_string(State::k_schema_version) + ")";
30 }
31
32 void note_older_version(uint32_t version, std::vector<std::string>& warnings)
33 {
34 if (version < State::k_schema_version) {
35 warnings.push_back("Schema version " + std::to_string(version)
36 + " predates " + std::to_string(State::k_schema_version)
37 + ": fields added since are absent and take their defaults");
38 }
39 }
40
41 float denormalize(float norm, const State::Range& r)
42 {
43 return r.min + norm * (r.max - r.min);
44 }
45
46 struct PixelView {
48 const std::vector<float>* pixels { nullptr };
49 uint32_t width { 0 };
50 };
51
52 std::optional<PixelView> load_exr(
53 const std::string& exr_path,
54 uint32_t expected_entity_count,
55 uint32_t expected_rows,
56 std::string& error_out)
57 {
58 auto image_opt = IO::ImageReader::load(exr_path, 0);
59 if (!image_opt) {
60 error_out = "Failed to load EXR: " + exr_path;
61 return std::nullopt;
62 }
63
64 const auto* pixels = image_opt->as_float();
65 if (!pixels || pixels->empty()) {
66 error_out = "EXR has no float pixel data: " + exr_path;
67 return std::nullopt;
68 }
69 if (image_opt->channels != State::k_channels) {
70 error_out = "EXR channel count mismatch: expected "
71 + std::to_string(State::k_channels) + " got " + std::to_string(image_opt->channels);
72 return std::nullopt;
73 }
74 if (image_opt->height != expected_rows) {
75 error_out = "EXR row count mismatch: expected "
76 + std::to_string(expected_rows) + " got " + std::to_string(image_opt->height);
77 return std::nullopt;
78 }
79 if (image_opt->width != expected_entity_count) {
80 error_out = "EXR width (" + std::to_string(image_opt->width)
81 + ") does not match entity count (" + std::to_string(expected_entity_count) + ")";
82 return std::nullopt;
83 }
84
85 const uint32_t w = image_opt->width;
86 return PixelView { .image = std::move(*image_opt), .pixels = nullptr, .width = w };
87 }
88
89 /**
90 * @brief The numeric fields of one entity, read from its EXR column.
91 */
92 struct Fields {
93 std::optional<glm::vec3> position;
94 float intensity {};
95 float radius {};
96 float query_radius {};
97 glm::vec3 color {};
98 float size {};
99 };
100
101 Fields read_fields(
102 const State::EntityRecord& entry,
103 const std::vector<float>& pixels,
104 uint32_t width,
105 size_t column,
106 const State::RangeSet& r)
107 {
108 const auto px = [&](size_t row, size_t channel) {
109 return pixels[((row * width + column) * State::k_channels) + channel];
110 };
111
112 Fields f;
113 if (entry.position) {
114 f.position = glm::vec3 {
115 denormalize(px(0, 0), r.pos_x),
116 denormalize(px(0, 1), r.pos_y),
117 denormalize(px(0, 2), r.pos_z),
118 };
119 }
120 f.intensity = denormalize(px(0, 3), r.intensity);
121 f.color = {
122 denormalize(px(1, 0), r.color_r),
123 denormalize(px(1, 1), r.color_g),
124 denormalize(px(1, 2), r.color_b),
125 };
126 f.size = denormalize(px(1, 3), r.size);
127 f.radius = denormalize(px(2, 0), r.radius);
128 f.query_radius = denormalize(px(2, 1), r.query_radius);
129 return f;
130 }
131
132 // -------------------------------------------------------------------------
133 // Callables and wiring
134 // -------------------------------------------------------------------------
135
136 template <typename Fn>
137 Fn resolve_fn(
138 const std::shared_ptr<Fn>& resolved,
139 const std::string& name,
140 std::string_view label,
141 std::vector<std::string>& warnings)
142 {
143 if (name.empty()) {
144 return {};
145 }
146 if (!resolved || !*resolved) {
147 warnings.push_back(std::string(label) + ": unknown '" + name + "', left empty");
148 return {};
149 }
150 return *resolved;
151 }
152
153 void apply_wiring(
154 Wiring wiring,
155 const State::WiringRecord& rec,
156 std::string_view who,
157 std::vector<std::string>& warnings)
158 {
159 const auto fallback = [&](const std::string& why) {
160 warnings.push_back(std::string(who) + " wiring: " + why + ", commit_driven instead");
161 };
162
163 switch (rec.kind) {
165 if (!rec.interval) {
166 fallback("every has no interval");
167 break;
168 }
169 wiring.every(*rec.interval,
170 rec.interval_token.value_or(Vruta::ProcessingToken::SAMPLE_ACCURATE));
171 if (rec.duration) {
172 wiring.for_duration(*rec.duration,
173 rec.duration_token.value_or(Vruta::ProcessingToken::SAMPLE_ACCURATE));
174 }
175 if (rec.times && *rec.times > 1) {
176 wiring.times(*rec.times);
177 }
178 if (!rec.position_fn_name.empty()) {
179 warnings.push_back(std::string(who) + " wiring: position function '"
180 + rec.position_fn_name + "' cannot be restored");
181 }
182 break;
183
185 if (rec.steps) {
186 for (const auto& s : *rec.steps) {
187 wiring.move_to(s.position, s.delay_seconds);
188 }
189 }
190 if (rec.times && *rec.times > 1) {
191 wiring.times(*rec.times);
192 }
193 break;
194
196 fallback("scroll needs a live window");
197 break;
199 fallback("a key trigger needs a live window");
200 break;
202 fallback("a mouse trigger needs a live window");
203 break;
205 fallback("a network trigger needs a live source");
206 break;
208 fallback("a window event trigger needs a live source");
209 break;
211 fallback("factory '" + rec.factory_name + "' cannot be restored");
212 break;
214 fallback("bind callables cannot be restored");
215 break;
217 fallback("the saved wiring kind is unsupported");
218 break;
220 break;
221 }
222 wiring.finalise();
223 }
224
225 // -------------------------------------------------------------------------
226 // Applying fields to live entities
227 // -------------------------------------------------------------------------
228
229 void apply_locus_nav(Locus& locus, const State::LocusNavRecord& nav)
230 {
231 auto& state = locus.nav();
232 state.eye = nav.eye;
233 state.fov_radians = nav.fov;
234 state.near_plane = nav.near_plane;
235 state.far_plane = nav.far_plane;
236 state.move_speed = nav.speed;
237 state.mouse_sensitivity = nav.mouse_sensitivity;
238 state.scroll_speed = nav.scroll_speed;
239
240 const glm::vec3 look = nav.target - nav.eye;
241 if (glm::dot(look, look) > 0.0F) {
242 const glm::vec3 dir = glm::normalize(look);
243 state.yaw = std::atan2(dir.x, dir.z);
244 state.pitch = std::asin(glm::clamp(dir.y, -1.0F, 1.0F));
245 }
246 locus.set_aspect(nav.aspect);
247 }
248
249 void apply_presence(Presence& presence, const State::EntityRecord& entry)
250 {
251 if (!entry.falloff_curve_name.empty()) {
252 if (auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name)) {
253 presence.set_falloff_curve(*fc);
254 }
255 }
256 if (entry.falloff_radius) {
257 presence.set_falloff_radius(*entry.falloff_radius);
258 }
259 }
260
261 void apply_emitter(Emitter& emitter, const State::EntityRecord& entry, const Fields& f)
262 {
263 if (f.position) {
264 emitter.set_position(*f.position);
265 } else {
266 emitter.clear_position();
267 }
268 emitter.set_intensity(f.intensity);
269 emitter.set_radius(f.radius);
270 if (entry.color) {
271 emitter.set_color(f.color);
272 }
273 if (entry.size) {
274 emitter.set_size(f.size);
275 }
276 }
277
278 void apply_sensor(Sensor& sensor, const Fields& f)
279 {
280 if (f.position) {
281 sensor.set_position(*f.position);
282 } else {
283 sensor.clear_position();
284 }
285 sensor.set_query_radius(f.query_radius);
286 }
287
288 void apply_agent(
289 Agent& agent,
290 const State::EntityRecord& entry,
291 const Fields& f,
292 std::vector<std::string>& warnings)
293 {
294 if (f.position) {
295 agent.set_position(*f.position);
296 } else {
297 agent.clear_position();
298 }
299 if (entry.orientation) {
300 agent.set_orientation(glm::quat(
301 entry.orientation->w, entry.orientation->x, entry.orientation->y, entry.orientation->z));
302 }
303 agent.set_intensity(f.intensity);
304 agent.set_radius(f.radius);
305 agent.set_query_radius(f.query_radius);
306 if (entry.color) {
307 agent.set_color(f.color);
308 }
309 if (entry.size) {
310 agent.set_size(f.size);
311 }
312
313 if (entry.locus_nav) {
314 if (auto* locus = dynamic_cast<Locus*>(&agent)) {
315 apply_locus_nav(*locus, *entry.locus_nav);
316 } else {
317 warnings.push_back("Agent " + std::to_string(entry.id)
318 + " has locus_nav in schema but is not a Locus at runtime");
319 }
320 }
321 if (auto* presence = dynamic_cast<Presence*>(&agent)) {
322 apply_presence(*presence, entry);
323 }
324 }
325
326 // -------------------------------------------------------------------------
327 // Entities: patch an existing one, or construct a missing one
328 // -------------------------------------------------------------------------
329
330 std::string who_is(const State::EntityRecord& entry)
331 {
332 return entry.kind + " " + std::to_string(entry.id);
333 }
334
335 void note_name_mismatch(
336 const State::EntityRecord& entry,
337 std::string_view what,
338 const std::string& saved,
339 const std::string& live,
340 std::vector<std::string>& warnings)
341 {
342 if (!saved.empty() && saved != live) {
343 warnings.push_back(who_is(entry) + " " + std::string(what)
344 + " mismatch: schema='" + saved + "' live='" + live + "'");
345 }
346 }
347
348 bool patch_entity(
349 Fabric& fabric,
350 const State::EntityRecord& entry,
351 const Fields& f,
352 std::vector<std::string>& warnings)
353 {
354 switch (State::parse_kind(entry.kind)) {
356 auto e = fabric.get_emitter(entry.id);
357 if (!e) {
358 return false;
359 }
360 note_name_mismatch(entry, "fn_name", entry.influence_fn_name, e->fn_name(), warnings);
361 apply_emitter(*e, entry, f);
362 return true;
363 }
365 auto s = fabric.get_sensor(entry.id);
366 if (!s) {
367 return false;
368 }
369 note_name_mismatch(entry, "fn_name", entry.perception_fn_name, s->fn_name(), warnings);
370 apply_sensor(*s, f);
371 return true;
372 }
373 case Fabric::Kind::Agent: {
374 auto a = fabric.get_agent(entry.id);
375 if (!a) {
376 return false;
377 }
378 note_name_mismatch(entry, "perception_fn_name", entry.perception_fn_name, a->perception_fn_name(), warnings);
379 note_name_mismatch(entry, "influence_fn_name", entry.influence_fn_name, a->influence_fn_name(), warnings);
380 apply_agent(*a, entry, f, warnings);
381 return true;
382 }
383 }
384 return false;
385 }
386
387 void note_runtime_id(
388 const State::EntityRecord& entry,
389 uint32_t runtime_id,
390 std::vector<std::string>& warnings)
391 {
392 if (runtime_id != entry.id) {
393 warnings.push_back(who_is(entry) + " reconstructed as runtime_id=" + std::to_string(runtime_id));
394 }
395 }
396
397 std::shared_ptr<Agent> construct_agent(
398 Fabric& fabric,
399 const State::EntityRecord& entry,
400 const Fields& f,
401 std::vector<std::string>& warnings)
402 {
403 const std::string who = who_is(entry);
404
405 auto pfn = resolve_fn(fabric.resolve_perception_fn(entry.perception_fn_name),
406 entry.perception_fn_name, who + " perception_fn", warnings);
407 auto ifn = resolve_fn(fabric.resolve_influence_fn(entry.influence_fn_name),
408 entry.influence_fn_name, who + " influence_fn", warnings);
409
410 if (entry.subkind == "locus" && entry.locus_nav) {
411 const auto& nav = *entry.locus_nav;
412 const Kinesis::NavigationConfig config {
413 .initial_eye = nav.eye,
414 .initial_target = nav.target,
415 .fov_radians = nav.fov,
416 .near_plane = nav.near_plane,
417 .far_plane = nav.far_plane,
418 .move_speed = nav.speed,
419 .mouse_sensitivity = nav.mouse_sensitivity,
420 .scroll_speed = nav.scroll_speed,
421 };
422 warnings.push_back(who + ": view_targets must be reconnected by caller");
423 return std::make_shared<Locus>(config, f.query_radius,
424 entry.perception_fn_name, std::move(pfn),
425 entry.influence_fn_name, std::move(ifn));
426 }
427
428 if (entry.subkind == "presence") {
429 auto rfn = resolve_fn(fabric.resolve_radiate_fn(entry.radiate_fn_name),
430 entry.radiate_fn_name, who + " radiate_fn", warnings);
431 return std::make_shared<Presence>(f.query_radius,
432 entry.perception_fn_name, std::move(pfn),
433 entry.influence_fn_name, std::move(ifn),
434 entry.radiate_fn_name, std::move(rfn));
435 }
436
437 if (entry.subkind == "locus") {
438 warnings.push_back(who + ": no locus_nav in schema, reconstructed as plain Agent");
439 } else if (!entry.subkind.empty()) {
440 warnings.push_back(who + ": unknown subkind '" + entry.subkind
441 + "', reconstructed as plain Agent");
442 }
443
444 if (entry.perception_fn_name.empty() && entry.influence_fn_name.empty()) {
445 return std::make_shared<Agent>(f.query_radius);
446 }
447 return std::make_shared<Agent>(f.query_radius,
448 entry.perception_fn_name, std::move(pfn),
449 entry.influence_fn_name, std::move(ifn));
450 }
451
452 void construct_entity(
453 Fabric& fabric,
454 const State::EntityRecord& entry,
455 const Fields& f,
456 std::vector<std::string>& warnings)
457 {
458 const std::string who = who_is(entry);
459
460 if (!entry.audio_sinks.empty() || !entry.render_sinks.empty()) {
461 warnings.push_back(who + ": sinks are not restored, they need a BufferManager and a window");
462 }
463
464 switch (State::parse_kind(entry.kind)) {
466 std::shared_ptr<Emitter> emitter;
467 if (entry.influence_fn_name.empty()) {
468 emitter = std::make_shared<Emitter>();
469 } else {
470 emitter = std::make_shared<Emitter>(entry.influence_fn_name,
471 resolve_fn(fabric.resolve_influence_fn(entry.influence_fn_name),
472 entry.influence_fn_name, who + " influence_fn", warnings));
473 }
474 apply_emitter(*emitter, entry, f);
475 auto wiring = fabric.wire(emitter);
476 note_runtime_id(entry, emitter->id(), warnings);
477 apply_wiring(std::move(wiring), entry.wiring, who, warnings);
478 break;
479 }
481 std::shared_ptr<Sensor> sensor;
482 if (entry.perception_fn_name.empty()) {
483 sensor = std::make_shared<Sensor>(f.query_radius);
484 } else {
485 sensor = std::make_shared<Sensor>(f.query_radius, entry.perception_fn_name,
486 resolve_fn(fabric.resolve_perception_fn(entry.perception_fn_name),
487 entry.perception_fn_name, who + " perception_fn", warnings));
488 }
489 apply_sensor(*sensor, f);
490 auto wiring = fabric.wire(sensor);
491 note_runtime_id(entry, sensor->id(), warnings);
492 apply_wiring(std::move(wiring), entry.wiring, who, warnings);
493 break;
494 }
495 case Fabric::Kind::Agent: {
496 auto agent = construct_agent(fabric, entry, f, warnings);
497 apply_agent(*agent, entry, f, warnings);
498 auto wiring = fabric.wire(agent);
499 note_runtime_id(entry, agent->id(), warnings);
500 apply_wiring(std::move(wiring), entry.wiring, who, warnings);
501 break;
502 }
503 }
504 }
505
506 // -------------------------------------------------------------------------
507 // Expanses
508 // -------------------------------------------------------------------------
509
510 enum class Restored : uint8_t {
511 Constructed,
512 Patched,
513 Skipped,
514 };
515
516 struct ExpanseParts {
520 };
521
522 template <typename Record>
523 void apply_bounds(Expanse& expanse, const Record& rec)
524 {
525 if (rec.bounds_min && rec.bounds_max) {
526 expanse.set_bounds({ .min = *rec.bounds_min, .max = *rec.bounds_max });
527 }
528 }
529
530 /**
531 * @brief Whether a record can become an Expanse at all.
532 *
533 * A Hull wraps a live buffer and cannot be rebuilt from data. A Mantle is
534 * restored as its plain box. Anything else needs a predicate name or a box.
535 */
536 template <typename Record>
537 bool expanse_restorable(const Record& rec, const std::string& label, std::vector<std::string>& warnings)
538 {
539 if (rec.subkind == "hull") {
540 warnings.push_back(label + ": a Hull wraps a live buffer that is not encoded, skipping");
541 return false;
542 }
543 if (rec.subkind == "mantle") {
544 warnings.push_back(label + ": a Mantle is restored as a plain boxed Expanse, its look must be made again");
545 } else if (!rec.subkind.empty()) {
546 warnings.push_back(label + ": unknown subkind '" + rec.subkind + "', restored as a plain Expanse");
547 }
548 if (rec.fn_name.empty() && !(rec.bounds_min && rec.bounds_max)) {
549 warnings.push_back(label + ": neither fn_name nor bounds, skipping");
550 return false;
551 }
552 return true;
553 }
554
555 template <typename Record>
556 bool resolve_expanse_parts(
557 const Record& rec,
558 const std::vector<Fabric*>& fabrics,
559 const std::string& label,
560 ExpanseParts& parts,
561 std::vector<std::string>& warnings)
562 {
563 if (!rec.fn_name.empty()) {
564 for (const Fabric* fabric : fabrics) {
565 if (auto ptr = fabric->resolve_expanse_fn(rec.fn_name); ptr && *ptr) {
566 parts.contains = *ptr;
567 break;
568 }
569 }
570 if (!parts.contains) {
571 warnings.push_back(label + ": fn '" + rec.fn_name + "' not in registry, skipping");
572 return false;
573 }
574 }
575
576 const auto resolve_crossing = [&](const std::string& name, Expanse::CrossingFn& out) {
577 if (name.empty()) {
578 return;
579 }
580 for (const Fabric* fabric : fabrics) {
581 if (auto ptr = fabric->resolve_crossing_fn(name); ptr && *ptr) {
582 out = *ptr;
583 return;
584 }
585 }
586 warnings.push_back(label + ": crossing '" + name + "' not in registry, left empty");
587 };
588 resolve_crossing(rec.on_enter_fn_name, parts.on_enter);
589 resolve_crossing(rec.on_exit_fn_name, parts.on_exit);
590 return true;
591 }
592
593 /**
594 * @brief A plain Expanse already on the Fabric that this record describes.
595 *
596 * Matched by predicate and crossing names. A box-only Expanse has no name to
597 * match on, so its box must be equal.
598 */
599 std::shared_ptr<Expanse> find_equivalent(const Fabric& fabric, const State::ExpanseRecord& rec)
600 {
601 for (uint32_t id : fabric.all_expanse_ids()) {
602 auto x = fabric.get_expanse(id);
603 if (!x) {
604 continue;
605 }
606 const Expanse& live = *x;
607 if (typeid(live) != typeid(Expanse)) {
608 continue;
609 }
610 if (x->fn_name() != rec.fn_name
611 || x->on_enter_fn_name() != rec.on_enter_fn_name
612 || x->on_exit_fn_name() != rec.on_exit_fn_name) {
613 continue;
614 }
615 if (rec.fn_name.empty()
616 && (!x->bounds() || x->bounds()->min != *rec.bounds_min || x->bounds()->max != *rec.bounds_max)) {
617 continue;
618 }
619 return x;
620 }
621 return nullptr;
622 }
623
624 Restored restore_expanse(
625 Fabric& fabric,
626 const State::ExpanseRecord& rec,
627 std::vector<std::string>& warnings)
628 {
629 const std::string label = "Expanse " + std::to_string(rec.id);
630 if (!expanse_restorable(rec, label, warnings)) {
631 return Restored::Skipped;
632 }
633
634 if (auto existing = find_equivalent(fabric, rec)) {
635 apply_bounds(*existing, rec);
636 return Restored::Patched;
637 }
638
639 ExpanseParts parts;
640 if (!resolve_expanse_parts(rec, { &fabric }, label, parts, warnings)) {
641 return Restored::Skipped;
642 }
643
644 auto expanse = std::make_shared<Expanse>(
645 rec.fn_name,
646 rec.on_enter_fn_name,
647 rec.on_exit_fn_name,
648 std::move(parts.contains),
649 std::move(parts.on_enter),
650 std::move(parts.on_exit));
651 apply_bounds(*expanse, rec);
652 fabric.add_expanse(std::move(expanse));
653 return Restored::Constructed;
654 }
655
656 std::optional<float> read_cell_size(const std::string& json_path)
657 {
658 IO::JSONSerializer ser;
659 auto schema = ser.read<State::FabricSchema>(json_path);
660 return schema ? schema->cell_size : std::nullopt;
661 }
662
663 void log_warnings(const std::vector<std::string>& warnings)
664 {
665 for (const auto& w : warnings) {
667 }
668 }
669
670} // namespace
671
672// -------------------------------------------------------------------------
673// decode()
674// -------------------------------------------------------------------------
675
676bool StateDecoder::decode(Fabric& fabric, const std::string& base_path)
677{
678 m_last_error.clear();
679 m_patched_count = 0;
680 m_missing_count = 0;
681
682 const std::string json_path = base_path + ".json";
683 const std::string exr_path = base_path + ".exr";
684
686 auto schema_opt = ser.read<State::FabricSchema>(json_path);
687 if (!schema_opt) {
688 m_last_error = "Failed to load schema: " + ser.last_error();
690 return false;
691 }
692 const auto& schema = *schema_opt;
693
694 if (!version_supported(schema.version)) {
695 m_last_error = version_error(schema.version);
697 return false;
698 }
699
700 if (schema.entities.empty()) {
701 m_last_error = "Schema contains no entities: " + json_path;
703 return false;
704 }
705
706 auto pv_opt = load_exr(exr_path, static_cast<uint32_t>(schema.entities.size()), State::k_exr_rows, m_last_error);
707 if (!pv_opt) {
709 return false;
710 }
711 const auto* pixels = pv_opt->image.as_float();
712 const uint32_t width = pv_opt->width;
713
714 std::vector<std::string> warnings;
715 note_older_version(schema.version, warnings);
716
717 for (size_t i = 0; i < schema.entities.size(); ++i) {
718 const auto& entry = schema.entities[i];
719
720 if (!State::kind_known(entry.kind)) {
721 warnings.push_back("unknown kind '" + entry.kind + "' for id "
722 + std::to_string(entry.id) + ", skipping");
724 continue;
725 }
726
727 const Fields fields = read_fields(entry, *pixels, width, i, schema.ranges);
728 if (patch_entity(fabric, entry, fields, warnings)) {
730 } else {
731 warnings.push_back("id " + std::to_string(entry.id) + " not found as "
732 + entry.kind + ", skipping");
734 }
735 }
736
737 for (const auto& xrec : schema.expanses) {
738 restore_expanse(fabric, xrec, warnings);
739 }
740
741 log_warnings(warnings);
742
744 "StateDecoder: patched {} entities ({} missing) from {} + {}",
745 m_patched_count, m_missing_count, exr_path, json_path);
746
747 return true;
748}
749
750// -------------------------------------------------------------------------
751// reconstruct()
752// -------------------------------------------------------------------------
753
755{
757 m_last_error.clear();
758
759 const std::string json_path = base_path + ".json";
760 const std::string exr_path = base_path + ".exr";
761
763 auto schema_opt = ser.read<State::FabricSchema>(json_path);
764 if (!schema_opt) {
765 m_last_error = "Failed to load schema: " + ser.last_error();
767 return result;
768 }
769 const auto& schema = *schema_opt;
770
771 if (!version_supported(schema.version)) {
772 m_last_error = version_error(schema.version);
774 return result;
775 }
776
777 if (schema.entities.empty()) {
778 m_last_error = "Schema contains no entities: " + json_path;
780 return result;
781 }
782
783 auto pv_opt = load_exr(exr_path, static_cast<uint32_t>(schema.entities.size()), State::k_exr_rows, m_last_error);
784 if (!pv_opt) {
786 return result;
787 }
788 const auto* pixels = pv_opt->image.as_float();
789 const uint32_t width = pv_opt->width;
790
791 note_older_version(schema.version, result.warnings);
792
793 const auto existing_ids = fabric.all_ids();
794 const std::unordered_set<uint32_t> existing(existing_ids.begin(), existing_ids.end());
795
796 for (size_t i = 0; i < schema.entities.size(); ++i) {
797 const auto& entry = schema.entities[i];
798
799 if (!State::kind_known(entry.kind)) {
800 result.warnings.push_back("Unknown kind '" + entry.kind
801 + "' for id " + std::to_string(entry.id) + ", skipping");
802 ++result.skipped;
803 continue;
804 }
805
806 const Fields fields = read_fields(entry, *pixels, width, i, schema.ranges);
807
808 if (existing.contains(entry.id)) {
809 if (patch_entity(fabric, entry, fields, result.warnings)) {
810 ++result.patched;
811 } else {
812 ++result.skipped;
813 }
814 } else {
815 construct_entity(fabric, entry, fields, result.warnings);
816 ++result.constructed;
817 }
818 }
819
820 for (const auto& xrec : schema.expanses) {
821 switch (restore_expanse(fabric, xrec, result.warnings)) {
822 case Restored::Constructed:
823 ++result.constructed;
824 break;
825 case Restored::Patched:
826 ++result.patched;
827 break;
828 case Restored::Skipped:
829 ++result.skipped;
830 break;
831 }
832 }
833
835 "StateDecoder::reconstruct: constructed={} patched={} skipped={} warnings={}",
836 result.constructed, result.patched, result.skipped, result.warnings.size());
837
838 return result;
839}
840
842 Tapestry& tapestry, const std::string& base_dir)
843{
845 m_last_error.clear();
846
847 const std::string tapestry_path = base_dir + "/tapestry.json";
849 auto schema_opt = ser.read<State::TapestrySchema>(tapestry_path);
850 if (!schema_opt) {
851 m_last_error = "Failed to load tapestry schema: " + ser.last_error();
853 return total;
854 }
855 const auto& schema = *schema_opt;
856
857 if (!version_supported(schema.version)) {
858 m_last_error = version_error(schema.version);
860 return total;
861 }
862
863 for (const auto& ref : schema.fabrics) {
864 auto fabric = tapestry.get_fabric(ref.name);
865 if (!fabric) {
866 fabric = tapestry.create_fabric(ref.name,
867 read_cell_size(ref.base_path + ".json").value_or(1.0F));
868 }
869 auto result = reconstruct(*fabric, ref.base_path);
870 total.constructed += result.constructed;
871 total.patched += result.patched;
872 total.skipped += result.skipped;
873 for (auto& w : result.warnings) {
874 total.warnings.push_back(ref.name + ": " + std::move(w));
875 }
876 }
877
878 for (const auto& xrec : schema.expanses) {
879 const std::string label = "TapestryExpanse '" + xrec.name + "'";
880 if (!expanse_restorable(xrec, label, total.warnings)) {
881 ++total.skipped;
882 continue;
883 }
884
885 std::vector<Fabric*> fabrics;
886 for (const auto& fname : xrec.fabric_names) {
887 if (auto fabric = tapestry.get_fabric(fname)) {
888 fabrics.push_back(fabric.get());
889 } else {
890 total.warnings.push_back(label + ": fabric '" + fname + "' not found");
891 }
892 }
893
894 std::shared_ptr<Expanse> expanse = tapestry.get_expanse(xrec.name);
895 const bool existed = expanse != nullptr;
896
897 if (existed) {
898 apply_bounds(*expanse, xrec);
899 } else {
900 ExpanseParts parts;
901 if (!resolve_expanse_parts(xrec, fabrics, label, parts, total.warnings)) {
902 ++total.skipped;
903 continue;
904 }
905
906 if (xrec.fn_name.empty()) {
907 expanse = tapestry.create_expanse<Expanse>(
908 xrec.name,
909 Kinesis::AABB3D { .min = *xrec.bounds_min, .max = *xrec.bounds_max },
910 std::move(parts.on_enter),
911 std::move(parts.on_exit));
912 } else {
913 expanse = tapestry.create_expanse(
914 xrec.name,
915 std::move(parts.contains),
916 std::move(parts.on_enter),
917 std::move(parts.on_exit));
918 apply_bounds(*expanse, xrec);
919 }
920
921 if (!expanse) {
922 ++total.skipped;
923 continue;
924 }
925 expanse->set_fn_name(xrec.fn_name);
926 expanse->set_on_enter_fn_name(xrec.on_enter_fn_name);
927 expanse->set_on_exit_fn_name(xrec.on_exit_fn_name);
928 }
929
930 for (Fabric* fabric : fabrics) {
931 fabric->add_expanse(expanse);
932 }
933 if (existed) {
934 ++total.patched;
935 } else {
936 ++total.constructed;
937 }
938 }
939
940 total.user_state = schema.user_state;
941
943 "StateDecoder::reconstruct(Tapestry): constructed={} patched={} skipped={} warnings={}",
944 total.constructed, total.patched, total.skipped, total.warnings.size());
945 return total;
946}
947
948} // namespace MayaFlux::Nexus
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
uint32_t label
Definition Contours.cpp:16
glm::vec3 color
Definition Decoder.cpp:97
Expanse::ContainsFn contains
Definition Decoder.cpp:517
IO::ImageData image
Definition Decoder.cpp:47
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
float intensity
Definition Decoder.cpp:94
float query_radius
Definition Decoder.cpp:96
Expanse::CrossingFn on_enter
Definition Decoder.cpp:518
Expanse::CrossingFn on_exit
Definition Decoder.cpp:519
const std::vector< float > * pixels
Definition Decoder.cpp:48
bool size
float radius
uint32_t total
size_t a
std::vector< float > * out
std::string name
Definition VKDevice.cpp:123
const uint8_t * ptr
uint32_t px
uint32_t width
Cycle Behavior: The for_cycles(N) configuration controls how many times the capture operation execute...
static std::optional< ImageData > load(const std::string &path, int desired_channels=4, std::optional< Kakshya::Region > region=std::nullopt)
Load image from file (static utility)
std::optional< T > read(const std::string &path)
Read path and deserialize into T.
const std::string & last_error() const
Last error message, empty if no error.
Converts arbitrary C++ types to/from JSON strings and disk files.
std::function< void(uint32_t)> CrossingFn
Definition Expanse.hpp:58
std::function< bool(const glm::vec3 &)> ContainsFn
Definition Expanse.hpp:57
A defined region of space with a containment test and crossing actions.
Definition Expanse.hpp:55
std::vector< uint32_t > all_ids() const
List all registered entity ids in insertion order.
Definition Fabric.cpp:119
Orchestrates spatial indexing and scheduling for Nexus objects.
Definition Fabric.hpp:38
ReconstructionResult reconstruct(Fabric &fabric, const std::string &base_path)
Patch existing entities and construct missing ones from schema.
Definition Decoder.cpp:754
bool decode(Fabric &fabric, const std::string &base_path)
Decode and apply to fabric.
Definition Decoder.cpp:676
std::shared_ptr< Fabric > get_fabric(std::string_view name) const
Look up a named Fabric.
Definition Tapestry.cpp:73
std::shared_ptr< Expanse > create_expanse(std::string name, Expanse::ContainsFn contains, Expanse::CrossingFn on_enter, Expanse::CrossingFn on_exit)
Create and register a named Expanse.
Definition Tapestry.cpp:84
std::shared_ptr< Expanse > get_expanse(std::string_view name) const
Look up a named Expanse.
Definition Tapestry.cpp:122
std::shared_ptr< Fabric > create_fabric(float cell_size=1.0F)
create_fabric a new unnamed Fabric into the Tapestry.
Definition Tapestry.cpp:19
Owner of one or more Fabrics and the shared state they rely on.
Definition Tapestry.hpp:25
Kakshya::ImageData ImageData
@ FileIO
Filesystem I/O operations.
@ Runtime
General runtime operations (default fallback)
@ Nexus
Spatial indexing and scheduling for user-defined behaviour.
constexpr T denormalize(T t, T lo, T hi) noexcept
Denormalize t from [0, 1] to [lo, hi].
Definition Scalar.hpp:61
constexpr uint32_t k_schema_version
Current schema version written by StateEncoder and accepted by StateDecoder.
Definition Schema.hpp:26
bool kind_known(std::string_view s)
Return true if s maps to a known Fabric::Kind token.
Definition Schema.hpp:469
Fabric::Kind parse_kind(std::string_view s)
Parse a JSON kind token to Fabric::Kind (case-insensitive).
Definition Schema.hpp:480
constexpr uint32_t k_schema_min_version
Oldest schema version StateDecoder still reads.
Definition Schema.hpp:32
constexpr uint32_t k_exr_rows
RGBA32F EXR layout constants shared between encoder and decoder.
Definition Schema.hpp:47
constexpr uint32_t k_channels
Definition Schema.hpp:48
@ SAMPLE_ACCURATE
Coroutine is sample-accurate.
Axis-aligned bounding box in 3D world space.
Definition Bounds.hpp:188