// SPDX-License-Identifier: MPL-2.0 // scene/delta en FlatBuffers (contrat contract/, ADR 0013, lot B) : aller-retour exact, // valeurs refusées par les contrôles générés. #include "sequa/3d/wire_contract.hpp" #include #include #include #include #include #include using namespace sequa::three_d; using namespace sequa::three_d::wire; namespace cr = sequa::core_render; namespace common = sequa::common; namespace { std::span bytes_of(const std::string& frame) { return {reinterpret_cast(frame.data()), frame.size()}; } std::string frame_bytes(const flatbuffers::FlatBufferBuilder& builder) { return {reinterpret_cast(builder.GetBufferPointer()), builder.GetSize()}; } SceneDeltaMessage every_command() { Transform transform; transform.position = {1.5f, -2.25f, 1e-7f}; transform.rotation = {0, 0.70710677f, 0, 0.70710677f}; transform.scale = {2, 1, 0.5f}; Visual visual; visual.primitive = Primitive::capsule; visual.size = {0.5f, 1.8f, 0.5f}; visual.color = {0.1f, 0.2f, 0.3f, 0.4f}; visual.semantic_class = 0xFFFFFFFFU; Camera camera; camera.transform = transform; camera.vertical_fov_deg = 55; camera.near_plane = 0.1f; camera.far_plane = 500; SceneDeltaMessage message; message.delta.world_version = (1ULL << 53) + 7; // au-delà de ce qu'un double JS garde exactement message.tick = 0xFFFFFFFFFFFFFFFFULL; message.full = true; message.sent_at_us = 1'000'000'123ULL; message.delta.commands = {CreateObject{0x10, transform, visual}, SetTransform{0x10, transform}, SetVisual{0x10, visual}, SetCamera{camera}, DestroyObject{0xFFFFFFFFFFFFFFFFULL}}; return message; } // Trame scene/delta d'une seule commande SetTransform, écrite à la main. std::string set_transform_frame(std::uint64_t id, float x) { flatbuffers::FlatBufferBuilder builder; const common::Transform transform(common::Vec3(x, 0, 0), common::Quat(0, 0, 0, 1), common::Vec3(1, 1, 1)); const std::vector types{cr::SceneCommand::SetTransform}; const std::vector> tables{cr::CreateSetTransform(builder, id, &transform).Union()}; const auto types_vector = builder.CreateVector(types); const auto tables_vector = builder.CreateVector(tables); cr::contract::finish_scene_delta(builder, 0, cr::CreateSceneDelta(builder, 1, 1, false, 1, types_vector, tables_vector)); return frame_bytes(builder); } } // namespace TEST_CASE("scene/delta FlatBuffers : aller-retour exact de toutes les commandes") { const SceneDeltaMessage message = every_command(); const std::string frame = encode_scene_delta_frame(message); std::string error; const auto back = decode_scene_delta_frame(bytes_of(frame), &error); REQUIRE_MESSAGE(back.has_value(), error); CHECK(back->delta == message.delta); CHECK(back->tick == message.tick); CHECK(back->full == message.full); CHECK(back->sent_at_us == message.sent_at_us); // Identifiant de la liaison core-render dans les octets 4 à 7. REQUIRE(frame.size() > 8); CHECK(frame.substr(4, 4) == "SQCR"); } TEST_CASE("scene/delta FlatBuffers : delta vide, full absent vaut faux") { SceneDeltaMessage message; message.delta.world_version = 3; message.tick = 2; message.sent_at_us = 1; const auto back = decode_scene_delta_frame(bytes_of(encode_scene_delta_frame(message))); REQUIRE(back.has_value()); CHECK(back->delta.commands.empty()); CHECK_FALSE(back->full); CHECK(back->delta.world_version == 3); } TEST_CASE("scene/delta FlatBuffers : valeurs refusees par les controles generes") { std::string error; CHECK(decode_scene_delta_frame(bytes_of(set_transform_frame(5, 1.0f))).has_value()); SUBCASE("id 0 réservé") { CHECK_FALSE(decode_scene_delta_frame(bytes_of(set_transform_frame(0, 1.0f)), &error).has_value()); CHECK(error.find("id") != std::string::npos); } SUBCASE("nombre non fini") { CHECK_FALSE(decode_scene_delta_frame(bytes_of(set_transform_frame(5, std::numeric_limits::infinity())), &error) .has_value()); CHECK_FALSE(decode_scene_delta_frame(bytes_of(set_transform_frame(5, std::numeric_limits::quiet_NaN()))) .has_value()); } SUBCASE("primitive inconnue") { flatbuffers::FlatBufferBuilder builder; const common::Vec3 size(1, 1, 1); const common::Color color(1, 1, 1, 1); const auto visual = cr::CreateVisual(builder, static_cast(9), &size, &color, 0U); const std::vector types{cr::SceneCommand::SetVisual}; const std::vector> tables{cr::CreateSetVisual(builder, 4, visual).Union()}; const auto types_vector = builder.CreateVector(types); const auto tables_vector = builder.CreateVector(tables); cr::contract::finish_scene_delta(builder, 0, cr::CreateSceneDelta(builder, 1, 1, false, 1, types_vector, tables_vector)); CHECK_FALSE(decode_scene_delta_frame(bytes_of(frame_bytes(builder)), &error).has_value()); CHECK(error.find("primitive") != std::string::npos); } SUBCASE("champ obligatoire absent (tick)") { flatbuffers::FlatBufferBuilder builder; const auto types_vector = builder.CreateVector(std::vector{}); const auto tables_vector = builder.CreateVector(std::vector>{}); cr::contract::finish_scene_delta( builder, 0, cr::CreateSceneDelta(builder, 1, flatbuffers::nullopt, false, 1, types_vector, tables_vector)); CHECK_FALSE(decode_scene_delta_frame(bytes_of(frame_bytes(builder)), &error).has_value()); CHECK(error.find("tick") != std::string::npos); } SUBCASE("trame tronquée") { const std::string frame = encode_scene_delta_frame(every_command()); for (std::size_t size = 0; size < frame.size(); size += 7) { CHECK_FALSE(decode_scene_delta_frame(bytes_of(frame.substr(0, size))).has_value()); } } }