From 19abe9bfba5f8eacc887ba22a896e8f59cc3a328 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Thu, 22 Oct 2026 11:15:26 -0500 Subject: [PATCH 1/2] feat(physics): body_state / pose replication codec (H1 slice 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first H1 (single-holon) slice and the dependency H0 flagged: a holon replicates a participant's avatar pose as a node's data(), but no cvc::physics type had a registered data-type name or a codec, so none could cross the cluster. Add cvc::physics::physics_codec (inc/cvc/physics/physics_codec.h, src/cvc/physics/physics_codec.cpp), mirroring the cvc::ariadne::stream::format_desc codec: a little-endian, fixed-width, FIELD-BY-FIELD wire (never a struct memcpy, so trailing-bool padding and ABI never leak) with the double serializer stream_codec lacked — put_f64 writes the IEEE-754 binary64 bit pattern as 8 LE bytes (memcpy, C++17- clean, matching the built-in "cvc.f64.v1" double codec). body_state is 13 doubles (position, rotation, linear_velocity, angular_velocity) + an active byte; pose is 7 doubles. The decoder bounds-checks via reader{ok} and throws on truncation. register_physics_data_types(app&) and register_physics_codecs(state_codec_registry&) key both the registerDataType name and the registry codec on the identical strings "cvc::physics::body_state" / "cvc::physics::pose" (codec_ids "cvc.physics.body_state.v1" / ".pose.v1"), so dataTypeName() always matches a registered codec — the #767 S1 mismatch class. Mass is intentionally NOT on the wire (body_state has none; it lives in body_settings and stays server-owned). No floating-point math, so the TU is not in the -ffp-contract=off exact-sources set. physics_codec_test (8 cases): bit-for-bit round-trip of body_state and pose including NaN / Inf / -0.0 / denormals; registry has() both; registry key == dataTypeName(); golden fixed-width LE wire (105 bytes / 56 bytes); truncated payloads throw. All green. --- inc/cvc/physics/physics_codec.h | 71 ++++++++++++ src/cvc/CMakeLists.txt | 2 + src/cvc/physics/physics_codec.cpp | 167 +++++++++++++++++++++++++++ src/cvc/tests/CMakeLists.txt | 11 ++ src/cvc/tests/physics_codec_test.cpp | 161 ++++++++++++++++++++++++++ 5 files changed, 412 insertions(+) create mode 100644 inc/cvc/physics/physics_codec.h create mode 100644 src/cvc/physics/physics_codec.cpp create mode 100644 src/cvc/tests/physics_codec_test.cpp diff --git a/inc/cvc/physics/physics_codec.h b/inc/cvc/physics/physics_codec.h new file mode 100644 index 000000000..101cf8fcc --- /dev/null +++ b/inc/cvc/physics/physics_codec.h @@ -0,0 +1,71 @@ +/* + Copyright 2026 The University of Texas at Austin + + This file is part of libcvc. + + libcvc is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + libcvc is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. +*/ + +#ifndef __CVC_PHYSICS_PHYSICS_CODEC_H__ +#define __CVC_PHYSICS_PHYSICS_CODEC_H__ + +#include + +namespace cvc { + +class app; +class state_codec_registry; + +namespace physics { + +// ---------------- +// cvc::physics — pose / body_state replication codec +// ---------------- +// A holon replicates a participant's avatar as a cvc::physics::body_state (or a +// bare cvc::physics::pose) carried in a state node's data(). For that payload to +// cross the cluster, the distributed-state pipeline needs the type to have both a +// registered data-type name and a (de)serialization codec in the shard's codec +// registry -- the same contract cvc::ariadne::stream::format_desc uses. +// +// The wire is little-endian, fixed-width, FIELD-BY-FIELD (never a struct memcpy, +// so trailing-bool padding and ABI differences never leak onto the wire): each +// double is its IEEE-754 binary64 bit pattern as 8 LE bytes (matching the +// built-in "cvc.f64.v1" double codec), and `active` is one byte. So the encoding +// is byte-identical across the (little-endian) fleet, which content-addressed +// blob offload and live/snapshot digest agreement both require. +// +// These helpers bridge cvc::physics types to the cvc::state codec registry; like +// the stream codec they are registered INTO a shard's (mutable) registry rather +// than baked into cvc::state, which sits below physics. + +// The canonical, cross-ABI-stable data-type names, used BOTH as the app's +// registerDataType name and the codec-registry key, so dataTypeName() resolves +// to this exact string rather than the compiler's mangled typeid name (an +// unregistered type would fall back to the mangled name, which differs across +// toolchains and would silently fail to replicate). +extern const char *const kBodyStateTypeName; // "cvc::physics::body_state" +extern const char *const kPoseTypeName; // "cvc::physics::pose" + +// Register body_state + pose as named data types on `ctx`, so a node holding one +// in data() reports the stable name from dataTypeName(). Idempotent. +void register_physics_data_types(app &ctx); + +// Register the body_state + pose (de)serialization codecs into a shard's codec +// registry so a node's physics data() replicates cross-node. Call on every +// participating node's shard registry (alongside the built-in / volume-geometry +// codecs the shard installs itself). The registry key is kBodyStateTypeName / +// kPoseTypeName -- identical to the registerDataType name above, so the key the +// emit path looks up (dataTypeName()) always matches a registered codec. +void register_physics_codecs(state_codec_registry ®istry); + +} // namespace physics +} // namespace cvc + +#endif // __CVC_PHYSICS_PHYSICS_CODEC_H__ diff --git a/src/cvc/CMakeLists.txt b/src/cvc/CMakeLists.txt index bf23c1a10..50ddc12b4 100644 --- a/src/cvc/CMakeLists.txt +++ b/src/cvc/CMakeLists.txt @@ -70,6 +70,7 @@ set(INCLUDE_FILES ../../inc/cvc/physics/world_geometry.h ../../inc/cvc/physics/plant_host.h ../../inc/cvc/physics/pose_tracker.h + ../../inc/cvc/physics/physics_codec.h ../../inc/cvc/physics/nav_plant.h ../../inc/cvc/physics/nav_eval.h ../../inc/cvc/physics/plant_log.h @@ -268,6 +269,7 @@ set(SOURCE_FILES physics/world_geometry.cpp physics/plant_host.cpp physics/pose_tracker.cpp + physics/physics_codec.cpp physics/binding_meta.cpp physics/nav_plant.cpp physics/nav_eval.cpp diff --git a/src/cvc/physics/physics_codec.cpp b/src/cvc/physics/physics_codec.cpp new file mode 100644 index 000000000..f02a0f3a0 --- /dev/null +++ b/src/cvc/physics/physics_codec.cpp @@ -0,0 +1,167 @@ +/* + Copyright 2026 The University of Texas at Austin + + This file is part of libcvc. + + libcvc is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + libcvc is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cvc { +namespace physics { + +const char *const kBodyStateTypeName = "cvc::physics::body_state"; +const char *const kPoseTypeName = "cvc::physics::pose"; + +namespace { + +// Little-endian, fixed-width framing (mirrors cvc::ariadne::stream::stream_codec) +// so the content is byte-identical across the little-endian fleet. +void put_u8(std::vector &o, std::uint8_t v) { o.push_back(v); } +void put_u64(std::vector &o, std::uint64_t v) { + for (int i = 0; i < 8; ++i) + o.push_back(static_cast((v >> (8 * i)) & 0xff)); +} +// A double as its IEEE-754 binary64 bit pattern, 8 LE bytes. memcpy (not a +// reinterpret or std::bit_cast) keeps it well-defined and C++17-clean; encode and +// decode are exact inverses on any single host and byte-identical across LE hosts, +// matching the built-in "cvc.f64.v1" double codec. +void put_f64(std::vector &o, double d) { + std::uint64_t bits = 0; + std::memcpy(&bits, &d, sizeof(bits)); + put_u64(o, bits); +} + +struct reader { + const unsigned char *p; + const unsigned char *e; + bool ok = true; + explicit reader(const std::vector &b) : p(b.data()), e(b.data() + b.size()) {} + std::uint8_t u8() { + if (p + 1 > e) { + ok = false; + return 0; + } + return *p++; + } + std::uint64_t u64() { + if (p + 8 > e) { + ok = false; + return 0; + } + std::uint64_t v = 0; + for (int i = 0; i < 8; ++i) + v |= static_cast(p[i]) << (8 * i); + p += 8; + return v; + } + double f64() { + std::uint64_t bits = u64(); + double d = 0.0; + std::memcpy(&d, &bits, sizeof(d)); + return d; + } +}; + +void put_vec3(std::vector &o, const vec3 &v) { + put_f64(o, v.x); + put_f64(o, v.y); + put_f64(o, v.z); +} +void put_quat(std::vector &o, const quat &q) { + put_f64(o, q.w); + put_f64(o, q.x); + put_f64(o, q.y); + put_f64(o, q.z); +} +vec3 read_vec3(reader &r) { + vec3 v; + v.x = r.f64(); + v.y = r.f64(); + v.z = r.f64(); + return v; +} +quat read_quat(reader &r) { + quat q; + q.w = r.f64(); + q.x = r.f64(); + q.y = r.f64(); + q.z = r.f64(); + return q; +} + +std::vector encode_body_state(const boost::any &v) { + const body_state s = boost::any_cast(v); + std::vector o; + put_vec3(o, s.position); + put_quat(o, s.rotation); + put_vec3(o, s.linear_velocity); + put_vec3(o, s.angular_velocity); + put_u8(o, s.active ? 1u : 0u); + return o; +} + +boost::any decode_body_state(const std::vector &b) { + reader r(b); + body_state s; + s.position = read_vec3(r); + s.rotation = read_quat(r); + s.linear_velocity = read_vec3(r); + s.angular_velocity = read_vec3(r); + s.active = r.u8() != 0; + if (!r.ok) + throw std::runtime_error("cvc::physics::body_state decode: truncated payload"); + return boost::any(s); +} + +std::vector encode_pose(const boost::any &v) { + const pose p = boost::any_cast(v); + std::vector o; + put_vec3(o, p.position); + put_quat(o, p.rotation); + return o; +} + +boost::any decode_pose(const std::vector &b) { + reader r(b); + pose p; + p.position = read_vec3(r); + p.rotation = read_quat(r); + if (!r.ok) + throw std::runtime_error("cvc::physics::pose decode: truncated payload"); + return boost::any(p); +} + +} // namespace + +void register_physics_data_types(app &ctx) { + ctx.registerDataType(kBodyStateTypeName); + ctx.registerDataType(kPoseTypeName); +} + +void register_physics_codecs(state_codec_registry ®istry) { + registry.register_codec(kBodyStateTypeName, encode_body_state, decode_body_state, + "cvc.physics.body_state.v1"); + registry.register_codec(kPoseTypeName, encode_pose, decode_pose, "cvc.physics.pose.v1"); +} + +} // namespace physics +} // namespace cvc diff --git a/src/cvc/tests/CMakeLists.txt b/src/cvc/tests/CMakeLists.txt index 3d563ae9a..ef000d3bc 100644 --- a/src/cvc/tests/CMakeLists.txt +++ b/src/cvc/tests/CMakeLists.txt @@ -107,6 +107,7 @@ add_executable(world_clock_test world_clock_test.cpp) add_executable(clock_driver_test clock_driver_test.cpp) add_executable(world_units_test world_units_test.cpp) add_executable(physics_world_test physics_world_test.cpp) +add_executable(physics_codec_test physics_codec_test.cpp) add_executable(physics_field_test physics_field_test.cpp) add_executable(physics_vehicle_test physics_vehicle_test.cpp) add_executable(physics_energy_test physics_energy_test.cpp) @@ -303,6 +304,7 @@ set(TEST_TARGETS clock_driver_test world_units_test physics_world_test + physics_codec_test physics_field_test physics_vehicle_test physics_energy_test @@ -1640,6 +1642,13 @@ target_link_libraries(physics_world_test GTest::gtest_main ) +target_link_libraries(physics_codec_test + PRIVATE + cvc + GTest::gtest + GTest::gtest_main +) + target_link_libraries(physics_vehicle_test PRIVATE cvc @@ -2038,6 +2047,7 @@ target_compile_features(world_clock_test PRIVATE cxx_std_17) target_compile_features(clock_driver_test PRIVATE cxx_std_17) target_compile_features(world_units_test PRIVATE cxx_std_17) target_compile_features(physics_world_test PRIVATE cxx_std_17) +target_compile_features(physics_codec_test PRIVATE cxx_std_17) target_compile_features(physics_field_test PRIVATE cxx_std_17) target_compile_features(physics_vehicle_test PRIVATE cxx_std_17) target_compile_features(physics_energy_test PRIVATE cxx_std_17) @@ -2493,6 +2503,7 @@ cvc_discover_tests(world_clock_test) cvc_discover_tests(clock_driver_test) cvc_discover_tests(world_units_test) cvc_discover_tests(physics_world_test) +cvc_discover_tests(physics_codec_test) cvc_discover_tests(physics_field_test) cvc_discover_tests(physics_vehicle_test) cvc_discover_tests(physics_energy_test) diff --git a/src/cvc/tests/physics_codec_test.cpp b/src/cvc/tests/physics_codec_test.cpp new file mode 100644 index 000000000..e45930a0b --- /dev/null +++ b/src/cvc/tests/physics_codec_test.cpp @@ -0,0 +1,161 @@ +/* + Copyright 2026 The University of Texas at Austin + + This file is part of libcvc. + + libcvc is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. +*/ + +// Tests for cvc::physics::physics_codec -- the body_state / pose wire codec that +// lets a holon replicate a participant's avatar pose as a node's data(). The +// properties worth pinning: an exact (bit-for-bit, including NaN / -0.0 / +// denormals) round-trip, a truncated payload that throws rather than reading out +// of bounds, a byte-identical little-endian wire, and -- guarding the #767 S1 +// class of bug -- that the codec-registry key equals dataTypeName() so the +// emit path's lookup actually finds the codec. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using cvc::physics::body_state; +using cvc::physics::pose; +using cvc::physics::quat; +using cvc::physics::vec3; + +namespace { +// Bit pattern of a double, so NaN / -0.0 / denormals compare exactly. +std::uint64_t bits(double d) { + std::uint64_t u = 0; + std::memcpy(&u, &d, sizeof(u)); + return u; +} +void expect_vec3_bit_eq(const vec3 &a, const vec3 &b) { + EXPECT_EQ(bits(a.x), bits(b.x)); + EXPECT_EQ(bits(a.y), bits(b.y)); + EXPECT_EQ(bits(a.z), bits(b.z)); +} +void expect_quat_bit_eq(const quat &a, const quat &b) { + EXPECT_EQ(bits(a.w), bits(b.w)); + EXPECT_EQ(bits(a.x), bits(b.x)); + EXPECT_EQ(bits(a.y), bits(b.y)); + EXPECT_EQ(bits(a.z), bits(b.z)); +} + +// A body_state with every field distinct and several nasty doubles. +body_state sample_body_state() { + body_state s; + s.position = {1.5, -2.25, 3.0e300}; + s.rotation = {0.5, -0.5, 0.5, -0.5}; + s.linear_velocity = {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), -0.0}; + s.angular_velocity = {std::numeric_limits::denorm_min(), -1e-9, 42.0}; + s.active = true; + return s; +} +} // namespace + +// ── a shard-registry round-trip (the real usage) ──────────────────────────── + +class PhysicsCodecTest : public ::testing::Test { +protected: + cvc::app ctx; + cvc::state_codec_registry reg; + void SetUp() override { + cvc::physics::register_physics_data_types(ctx); + cvc::physics::register_physics_codecs(reg); + } +}; + +TEST_F(PhysicsCodecTest, BodyStateRoundTripsBitForBit) { + const body_state in = sample_body_state(); + const std::vector wire = + reg.encode(cvc::physics::kBodyStateTypeName, boost::any(in)); + const boost::any back = reg.decode(cvc::physics::kBodyStateTypeName, wire); + ASSERT_FALSE(back.empty()); + const body_state out = boost::any_cast(back); + expect_vec3_bit_eq(in.position, out.position); + expect_quat_bit_eq(in.rotation, out.rotation); + expect_vec3_bit_eq(in.linear_velocity, out.linear_velocity); // NaN / Inf / -0.0 survive + expect_vec3_bit_eq(in.angular_velocity, out.angular_velocity); + EXPECT_EQ(in.active, out.active); +} + +TEST_F(PhysicsCodecTest, PoseRoundTripsBitForBit) { + pose in; + in.position = {-7.0, 0.0, 12345.678}; + in.rotation = {1.0, 0.0, 0.0, 0.0}; + const std::vector wire = reg.encode(cvc::physics::kPoseTypeName, boost::any(in)); + const pose out = boost::any_cast(reg.decode(cvc::physics::kPoseTypeName, wire)); + expect_vec3_bit_eq(in.position, out.position); + expect_quat_bit_eq(in.rotation, out.rotation); +} + +TEST_F(PhysicsCodecTest, RegistryHasBothCodecs) { + EXPECT_TRUE(reg.has(cvc::physics::kBodyStateTypeName)); + EXPECT_TRUE(reg.has(cvc::physics::kPoseTypeName)); +} + +// The #767 S1 class of bug: if the registry key differed from what dataTypeName() +// returns, the emit path's _codecs->encode(dataTypeName(), ...) would miss and the +// object would silently fail to replicate. Pin that they are identical. +TEST_F(PhysicsCodecTest, RegistryKeyEqualsDataTypeName) { + EXPECT_EQ(ctx.dataTypeName(), std::string(cvc::physics::kBodyStateTypeName)); + EXPECT_EQ(ctx.dataTypeName(), std::string(cvc::physics::kPoseTypeName)); +} + +// ── wire shape: fixed-width little-endian, field-by-field ─────────────────── + +TEST_F(PhysicsCodecTest, WireIsFixedWidthLittleEndian) { + body_state s; // all zero, then set position.x = 1.0 and active = true + s.position.x = 1.0; + s.active = true; + const std::vector wire = + reg.encode(cvc::physics::kBodyStateTypeName, boost::any(s)); + // 13 doubles (pos.xyz, rot.wxyz, linvel.xyz, angvel.xyz) * 8 + 1 active byte. + ASSERT_EQ(wire.size(), 13u * 8u + 1u); + // position.x == 1.0 -> IEEE-754 0x3FF0000000000000, little-endian first field. + const unsigned char one_le[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F}; + for (int i = 0; i < 8; ++i) + EXPECT_EQ(wire[i], one_le[i]) << "byte " << i; + // the very next field (position.y == 0.0) is eight zero bytes. + for (int i = 8; i < 16; ++i) + EXPECT_EQ(wire[i], 0x00) << "byte " << i; + EXPECT_EQ(wire.back(), 0x01); // active = true, the trailing byte +} + +TEST_F(PhysicsCodecTest, PoseWireIsSevenDoubles) { + const std::vector wire = + reg.encode(cvc::physics::kPoseTypeName, boost::any(pose{})); + EXPECT_EQ(wire.size(), 7u * 8u); +} + +// ── truncation is rejected, not read out of bounds ────────────────────────── + +TEST_F(PhysicsCodecTest, TruncatedBodyStateThrows) { + const std::vector full = + reg.encode(cvc::physics::kBodyStateTypeName, boost::any(sample_body_state())); + for (std::size_t n : {std::size_t{0}, std::size_t{1}, full.size() - 1, full.size() - 8}) { + const std::vector short_wire(full.begin(), full.begin() + n); + EXPECT_THROW(reg.decode(cvc::physics::kBodyStateTypeName, short_wire), std::exception) + << "n=" << n; + } +} + +TEST_F(PhysicsCodecTest, TruncatedPoseThrows) { + const std::vector full = + reg.encode(cvc::physics::kPoseTypeName, boost::any(pose{vec3{1, 2, 3}, quat{}})); + const std::vector short_wire(full.begin(), full.end() - 1); + EXPECT_THROW(reg.decode(cvc::physics::kPoseTypeName, short_wire), std::exception); +} From f679fa783b83055d9d1af4ba08d005d778af7f5d Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Thu, 22 Oct 2026 11:51:23 -0500 Subject: [PATCH 2/2] refactor(physics): type names as inline accessors, not static-global consts Replace the `extern const char* const kBodyStateTypeName / kPoseTypeName` namespace-scope constants with inline accessors body_state_type_name() / pose_type_name() returning the literal. Two reasons: - No static globals (project convention): the names are now a plain value returned by a function, not a global variable. - It also fixes a Windows-only CI failure. CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS exports functions but NOT data symbols, so cvc.dll did not export the `const char* const` consts; physics_codec_test (a separate exe linking the DLL) failed to link them (LNK2019). An inline accessor is compiled into each consumer, so it links across a shared-library boundary on every platform with no exported symbol at all. register_physics_data_types / register_physics_codecs and the test now call the accessors. No wire or behavior change; 8 physics_codec_test cases still green. --- inc/cvc/physics/physics_codec.h | 13 +++++++---- src/cvc/physics/physics_codec.cpp | 11 ++++----- src/cvc/tests/physics_codec_test.cpp | 34 ++++++++++++++++------------ 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/inc/cvc/physics/physics_codec.h b/inc/cvc/physics/physics_codec.h index 101cf8fcc..6fe2185ed 100644 --- a/inc/cvc/physics/physics_codec.h +++ b/inc/cvc/physics/physics_codec.h @@ -50,8 +50,13 @@ namespace physics { // to this exact string rather than the compiler's mangled typeid name (an // unregistered type would fall back to the mangled name, which differs across // toolchains and would silently fail to replicate). -extern const char *const kBodyStateTypeName; // "cvc::physics::body_state" -extern const char *const kPoseTypeName; // "cvc::physics::pose" +// +// Returned by an inline accessor (a plain string literal) rather than held in a +// namespace-scope constant: no static global to initialize or to export across a +// shared-library boundary (a `const char* const` data symbol is not exported by +// CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS, which covers functions only). +inline const char *body_state_type_name() { return "cvc::physics::body_state"; } +inline const char *pose_type_name() { return "cvc::physics::pose"; } // Register body_state + pose as named data types on `ctx`, so a node holding one // in data() reports the stable name from dataTypeName(). Idempotent. @@ -60,8 +65,8 @@ void register_physics_data_types(app &ctx); // Register the body_state + pose (de)serialization codecs into a shard's codec // registry so a node's physics data() replicates cross-node. Call on every // participating node's shard registry (alongside the built-in / volume-geometry -// codecs the shard installs itself). The registry key is kBodyStateTypeName / -// kPoseTypeName -- identical to the registerDataType name above, so the key the +// codecs the shard installs itself). The registry key is body_state_type_name() / +// pose_type_name() -- identical to the registerDataType name above, so the key the // emit path looks up (dataTypeName()) always matches a registered codec. void register_physics_codecs(state_codec_registry ®istry); diff --git a/src/cvc/physics/physics_codec.cpp b/src/cvc/physics/physics_codec.cpp index f02a0f3a0..495caecc9 100644 --- a/src/cvc/physics/physics_codec.cpp +++ b/src/cvc/physics/physics_codec.cpp @@ -28,9 +28,6 @@ namespace cvc { namespace physics { -const char *const kBodyStateTypeName = "cvc::physics::body_state"; -const char *const kPoseTypeName = "cvc::physics::pose"; - namespace { // Little-endian, fixed-width framing (mirrors cvc::ariadne::stream::stream_codec) @@ -153,14 +150,14 @@ boost::any decode_pose(const std::vector &b) { } // namespace void register_physics_data_types(app &ctx) { - ctx.registerDataType(kBodyStateTypeName); - ctx.registerDataType(kPoseTypeName); + ctx.registerDataType(body_state_type_name()); + ctx.registerDataType(pose_type_name()); } void register_physics_codecs(state_codec_registry ®istry) { - registry.register_codec(kBodyStateTypeName, encode_body_state, decode_body_state, + registry.register_codec(body_state_type_name(), encode_body_state, decode_body_state, "cvc.physics.body_state.v1"); - registry.register_codec(kPoseTypeName, encode_pose, decode_pose, "cvc.physics.pose.v1"); + registry.register_codec(pose_type_name(), encode_pose, decode_pose, "cvc.physics.pose.v1"); } } // namespace physics diff --git a/src/cvc/tests/physics_codec_test.cpp b/src/cvc/tests/physics_codec_test.cpp index e45930a0b..059b1780d 100644 --- a/src/cvc/tests/physics_codec_test.cpp +++ b/src/cvc/tests/physics_codec_test.cpp @@ -35,6 +35,11 @@ using cvc::physics::quat; using cvc::physics::vec3; namespace { +// The type names come from the public inline accessors +// cvc::physics::body_state_type_name() / pose_type_name() -- being inline (not an +// exported data symbol) they link across a shared-library boundary on every +// platform, and there is no static global to carry them. + // Bit pattern of a double, so NaN / -0.0 / denormals compare exactly. std::uint64_t bits(double d) { std::uint64_t u = 0; @@ -81,8 +86,8 @@ class PhysicsCodecTest : public ::testing::Test { TEST_F(PhysicsCodecTest, BodyStateRoundTripsBitForBit) { const body_state in = sample_body_state(); const std::vector wire = - reg.encode(cvc::physics::kBodyStateTypeName, boost::any(in)); - const boost::any back = reg.decode(cvc::physics::kBodyStateTypeName, wire); + reg.encode(cvc::physics::body_state_type_name(), boost::any(in)); + const boost::any back = reg.decode(cvc::physics::body_state_type_name(), wire); ASSERT_FALSE(back.empty()); const body_state out = boost::any_cast(back); expect_vec3_bit_eq(in.position, out.position); @@ -96,23 +101,24 @@ TEST_F(PhysicsCodecTest, PoseRoundTripsBitForBit) { pose in; in.position = {-7.0, 0.0, 12345.678}; in.rotation = {1.0, 0.0, 0.0, 0.0}; - const std::vector wire = reg.encode(cvc::physics::kPoseTypeName, boost::any(in)); - const pose out = boost::any_cast(reg.decode(cvc::physics::kPoseTypeName, wire)); + const std::vector wire = + reg.encode(cvc::physics::pose_type_name(), boost::any(in)); + const pose out = boost::any_cast(reg.decode(cvc::physics::pose_type_name(), wire)); expect_vec3_bit_eq(in.position, out.position); expect_quat_bit_eq(in.rotation, out.rotation); } TEST_F(PhysicsCodecTest, RegistryHasBothCodecs) { - EXPECT_TRUE(reg.has(cvc::physics::kBodyStateTypeName)); - EXPECT_TRUE(reg.has(cvc::physics::kPoseTypeName)); + EXPECT_TRUE(reg.has(cvc::physics::body_state_type_name())); + EXPECT_TRUE(reg.has(cvc::physics::pose_type_name())); } // The #767 S1 class of bug: if the registry key differed from what dataTypeName() // returns, the emit path's _codecs->encode(dataTypeName(), ...) would miss and the // object would silently fail to replicate. Pin that they are identical. TEST_F(PhysicsCodecTest, RegistryKeyEqualsDataTypeName) { - EXPECT_EQ(ctx.dataTypeName(), std::string(cvc::physics::kBodyStateTypeName)); - EXPECT_EQ(ctx.dataTypeName(), std::string(cvc::physics::kPoseTypeName)); + EXPECT_EQ(ctx.dataTypeName(), std::string(cvc::physics::body_state_type_name())); + EXPECT_EQ(ctx.dataTypeName(), std::string(cvc::physics::pose_type_name())); } // ── wire shape: fixed-width little-endian, field-by-field ─────────────────── @@ -122,7 +128,7 @@ TEST_F(PhysicsCodecTest, WireIsFixedWidthLittleEndian) { s.position.x = 1.0; s.active = true; const std::vector wire = - reg.encode(cvc::physics::kBodyStateTypeName, boost::any(s)); + reg.encode(cvc::physics::body_state_type_name(), boost::any(s)); // 13 doubles (pos.xyz, rot.wxyz, linvel.xyz, angvel.xyz) * 8 + 1 active byte. ASSERT_EQ(wire.size(), 13u * 8u + 1u); // position.x == 1.0 -> IEEE-754 0x3FF0000000000000, little-endian first field. @@ -137,7 +143,7 @@ TEST_F(PhysicsCodecTest, WireIsFixedWidthLittleEndian) { TEST_F(PhysicsCodecTest, PoseWireIsSevenDoubles) { const std::vector wire = - reg.encode(cvc::physics::kPoseTypeName, boost::any(pose{})); + reg.encode(cvc::physics::pose_type_name(), boost::any(pose{})); EXPECT_EQ(wire.size(), 7u * 8u); } @@ -145,17 +151,17 @@ TEST_F(PhysicsCodecTest, PoseWireIsSevenDoubles) { TEST_F(PhysicsCodecTest, TruncatedBodyStateThrows) { const std::vector full = - reg.encode(cvc::physics::kBodyStateTypeName, boost::any(sample_body_state())); + reg.encode(cvc::physics::body_state_type_name(), boost::any(sample_body_state())); for (std::size_t n : {std::size_t{0}, std::size_t{1}, full.size() - 1, full.size() - 8}) { const std::vector short_wire(full.begin(), full.begin() + n); - EXPECT_THROW(reg.decode(cvc::physics::kBodyStateTypeName, short_wire), std::exception) + EXPECT_THROW(reg.decode(cvc::physics::body_state_type_name(), short_wire), std::exception) << "n=" << n; } } TEST_F(PhysicsCodecTest, TruncatedPoseThrows) { const std::vector full = - reg.encode(cvc::physics::kPoseTypeName, boost::any(pose{vec3{1, 2, 3}, quat{}})); + reg.encode(cvc::physics::pose_type_name(), boost::any(pose{vec3{1, 2, 3}, quat{}})); const std::vector short_wire(full.begin(), full.end() - 1); - EXPECT_THROW(reg.decode(cvc::physics::kPoseTypeName, short_wire), std::exception); + EXPECT_THROW(reg.decode(cvc::physics::pose_type_name(), short_wire), std::exception); }