diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..60ebea5 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +# Revisions listed here are ignored by `git blame` so that purely +# mechanical commits (reformatting, mass renames) don't overwrite +# the blame history of every line they touch. +# +# GitHub honours this file automatically on the web UI. Locally, run: +# +# git config blame.ignoreRevsFile .git-blame-ignore-revs +# +# to make the local `git blame` respect it as well. +# +# Only add a SHA here when the commit is guaranteed to change no +# semantics -- formatters, whitespace-only edits, renames. + +# Repo-wide clang-format-15 sweep (2026-04-23) +101feb407ac0fdfa1b96de37f625b40b716573b0 diff --git a/benchmarks/bench_accessor.cpp b/benchmarks/bench_accessor.cpp index a219515..a82e9cc 100644 --- a/benchmarks/bench_accessor.cpp +++ b/benchmarks/bench_accessor.cpp @@ -39,50 +39,50 @@ namespace { -// The arena replay lives under samples/content/reader/arena/. Benchmarks -// run from various working dirs, so we resolve the path relative to the -// fixtures dir that CMake exposes via VTX_BENCH_FIXTURES_DIR. -std::string ArenaReplayPath() { - return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() - / "samples" / "content" / "reader" / "arena" / "arena_from_fbs_ds.vtx") - .string(); -} + // The arena replay lives under samples/content/reader/arena/. Benchmarks + // run from various working dirs, so we resolve the path relative to the + // fixtures dir that CMake exposes via VTX_BENCH_FIXTURES_DIR. + std::string ArenaReplayPath() { + return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" / + "reader" / "arena" / "arena_from_fbs_ds.vtx") + .string(); + } -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_accessor_debug_logs_once{}; - -// Small bundle that holds the reader + accessor + resolved keys. Moving -// this construction out of the measured loop is intentional -- a real -// integration resolves keys once at setup, not per frame. -struct AccessorFixture { - VTX::ReaderContext reader_result; - VTX::FrameAccessor accessor; - VTX::PropertyKey key_position; - VTX::PropertyKey key_health; - VTX::PropertyKey key_unique_id; - - static AccessorFixture Load(benchmark::State& state) { - AccessorFixture f; - f.reader_result = VTX::OpenReplayFile(ArenaReplayPath()); - if (!f.reader_result) { - state.SkipWithError("OpenReplayFile failed (run vtx_sample_generate + vtx_sample_advance_write first)"); - return f; - } - f.accessor = f.reader_result.reader->CreateAccessor(); - f.key_position = f.accessor.Get("Player", "Position"); - f.key_health = f.accessor.Get("Player", "Health"); - f.key_unique_id = f.accessor.Get("Player", "UniqueID"); + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_accessor_debug_logs_once {}; + + // Small bundle that holds the reader + accessor + resolved keys. Moving + // this construction out of the measured loop is intentional -- a real + // integration resolves keys once at setup, not per frame. + struct AccessorFixture { + VTX::ReaderContext reader_result; + VTX::FrameAccessor accessor; + VTX::PropertyKey key_position; + VTX::PropertyKey key_health; + VTX::PropertyKey key_unique_id; + + static AccessorFixture Load(benchmark::State& state) { + AccessorFixture f; + f.reader_result = VTX::OpenReplayFile(ArenaReplayPath()); + if (!f.reader_result) { + state.SkipWithError("OpenReplayFile failed (run vtx_sample_generate + vtx_sample_advance_write first)"); + return f; + } + f.accessor = f.reader_result.reader->CreateAccessor(); + f.key_position = f.accessor.Get("Player", "Position"); + f.key_health = f.accessor.Get("Player", "Health"); + f.key_unique_id = f.accessor.Get("Player", "UniqueID"); - if (!f.key_position.IsValid() || !f.key_health.IsValid() || !f.key_unique_id.IsValid()) { - state.SkipWithError("one or more Player keys did not resolve"); + if (!f.key_position.IsValid() || !f.key_health.IsValid() || !f.key_unique_id.IsValid()) { + state.SkipWithError("one or more Player keys did not resolve"); + } + return f; } - return f; - } -}; + }; -} // namespace +} // namespace // Open the replay, create the accessor, resolve three keys, then linearly // enumerate every frame, iterate every entity in the "entity" bucket, and @@ -92,24 +92,27 @@ static void BM_AccessorSequentialScan(benchmark::State& state) { int32_t total_frames = 0; for (auto _ : state) { auto fixture = AccessorFixture::Load(state); - if (!fixture.reader_result) break; + if (!fixture.reader_result) + break; auto& reader = fixture.reader_result.reader; total_frames = reader->GetTotalFrames(); double pos_accum = 0.0; - float hp_accum = 0.0f; - size_t id_count = 0; + float hp_accum = 0.0f; + size_t id_count = 0; for (int32_t i = 0; i < total_frames; ++i) { const auto* frame = reader->GetFrameSync(i); - if (!frame) continue; + if (!frame) + continue; for (const auto& bucket : frame->GetBuckets()) { for (const auto& entity : bucket.entities) { - if (entity.entity_type_id != 0) continue; // 0 = Player + if (entity.entity_type_id != 0) + continue; // 0 = Player VTX::EntityView view(entity); pos_accum += view.Get(fixture.key_position).x; - hp_accum += view.Get(fixture.key_health); - id_count += view.Get(fixture.key_unique_id).size(); + hp_accum += view.Get(fixture.key_health); + id_count += view.Get(fixture.key_unique_id).size(); } } } @@ -128,12 +131,15 @@ BENCHMARK(BM_AccessorSequentialScan)->Unit(benchmark::kMillisecond); // the replay in memory would observe when scrubbing over the properties. static void BM_AccessorHotLoopPreloaded(benchmark::State& state) { auto result = VTX::OpenReplayFile(ArenaReplayPath()); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; - auto accessor = reader->CreateAccessor(); + auto accessor = reader->CreateAccessor(); auto key_position = accessor.Get("Player", "Position"); - auto key_health = accessor.Get("Player", "Health"); + auto key_health = accessor.Get("Player", "Health"); if (!key_position.IsValid() || !key_health.IsValid()) { state.SkipWithError("keys did not resolve"); return; @@ -161,7 +167,8 @@ static void BM_AccessorHotLoopPreloaded(benchmark::State& state) { for (const auto& frame : ram_cache) { for (const auto& bucket : frame.GetBuckets()) { for (const auto& entity : bucket.entities) { - if (entity.entity_type_id == 0) ++entities_per_sweep; + if (entity.entity_type_id == 0) + ++entities_per_sweep; } } } @@ -171,7 +178,8 @@ static void BM_AccessorHotLoopPreloaded(benchmark::State& state) { for (const auto& frame : ram_cache) { for (const auto& bucket : frame.GetBuckets()) { for (const auto& entity : bucket.entities) { - if (entity.entity_type_id != 0) continue; + if (entity.entity_type_id != 0) + continue; VTX::EntityView view(entity); sink += view.Get(key_position).x + view.Get(key_health); } @@ -189,10 +197,13 @@ BENCHMARK(BM_AccessorHotLoopPreloaded)->Unit(benchmark::kMillisecond); // behaviour under non-sequential access. static void BM_AccessorRandomWithinBucket(benchmark::State& state) { auto result = VTX::OpenReplayFile(ArenaReplayPath()); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; - auto accessor = reader->CreateAccessor(); + auto accessor = reader->CreateAccessor(); auto key_position = accessor.Get("Player", "Position"); if (!key_position.IsValid()) { state.SkipWithError("Player::Position did not resolve"); @@ -227,16 +238,20 @@ static void BM_AccessorRandomWithinBucket(benchmark::State& state) { std::vector idxs; idxs.reserve(bucket.entities.size()); for (size_t i = 0; i < bucket.entities.size(); ++i) { - if (bucket.entities[i].entity_type_id == 0) idxs.push_back(i); - if (bucket.entities[i].entity_type_id == 0) idxs.push_back(i); + if (bucket.entities[i].entity_type_id == 0) + idxs.push_back(i); + if (bucket.entities[i].entity_type_id == 0) + idxs.push_back(i); } - if (idxs.size() < 2) continue; + if (idxs.size() < 2) + continue; std::shuffle(idxs.begin(), idxs.end(), rng); prepared.push_back({&bucket.entities, std::move(idxs)}); } } int64_t ops_per_sweep = 0; - for (const auto& pb : prepared) ops_per_sweep += static_cast(pb.shuffled.size()); + for (const auto& pb : prepared) + ops_per_sweep += static_cast(pb.shuffled.size()); double sink = 0.0; for (auto _ : state) { diff --git a/benchmarks/bench_accessor_key_resolution.cpp b/benchmarks/bench_accessor_key_resolution.cpp index c7651ae..fa56987 100644 --- a/benchmarks/bench_accessor_key_resolution.cpp +++ b/benchmarks/bench_accessor_key_resolution.cpp @@ -20,39 +20,45 @@ namespace { -std::string ArenaReplayPath() { - return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() - / "samples" / "content" / "reader" / "arena" / "arena_from_fbs_ds.vtx") - .string(); -} + std::string ArenaReplayPath() { + return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" / + "reader" / "arena" / "arena_from_fbs_ds.vtx") + .string(); + } -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_keyres_debug_logs_once{}; + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_keyres_debug_logs_once {}; -// A representative spread of property lookups a real integration performs. -struct PropSpec { const char* struct_name; const char* prop_name; }; -constexpr std::array kProps = {{ - {"Player", "Position"}, - {"Player", "Health"}, - {"Player", "UniqueID"}, - {"Player", "Velocity"}, - {"Player", "Score"}, - {"Projectile", "Position"}, - {"Projectile", "Damage"}, - {"MatchState", "ScoreTeam1"}, - {"MatchState", "ScoreTeam2"}, -}}; + // A representative spread of property lookups a real integration performs. + struct PropSpec { + const char* struct_name; + const char* prop_name; + }; + constexpr std::array kProps = {{ + {"Player", "Position"}, + {"Player", "Health"}, + {"Player", "UniqueID"}, + {"Player", "Velocity"}, + {"Player", "Score"}, + {"Projectile", "Position"}, + {"Projectile", "Damage"}, + {"MatchState", "ScoreTeam1"}, + {"MatchState", "ScoreTeam2"}, + }}; -} // namespace +} // namespace // Resolve a full set of PropertyKey via the public accessor API. If // the underlying cache is O(1) then time-per-resolution should stay // constant regardless of how many properties the schema defines. static void BM_AccessorKeyResolution(benchmark::State& state) { auto result = VTX::OpenReplayFile(ArenaReplayPath()); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } const auto accessor = result.reader->CreateAccessor(); diff --git a/benchmarks/bench_cs.cpp b/benchmarks/bench_cs.cpp index 2ed734d..46a8644 100644 --- a/benchmarks/bench_cs.cpp +++ b/benchmarks/bench_cs.cpp @@ -37,60 +37,59 @@ namespace { -std::string CsReplayPath(const char* backend_filename) { - return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() - / "samples" / "content" / "reader" / "cs" / backend_filename) - .string(); -} - -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_cs_debug_logs_once{}; - -// Opens + iterates every frame + touches the bucket list. Mirrors the -// BM_ReaderSequentialScan shape but against a realistic fixture. -void SequentialScanImpl(benchmark::State& state, const char* filename) { - const std::string path = CsReplayPath(filename); - if (!std::filesystem::exists(path)) { - state.SkipWithError("CS fixture missing (expected under samples/content/reader/cs/)"); - return; + std::string CsReplayPath(const char* backend_filename) { + return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" / + "reader" / "cs" / backend_filename) + .string(); } - VtxBench::WarmFileCache(path); - int64_t total_frames = 0; - for (auto _ : state) { - auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); break; } - total_frames = result.reader->GetTotalFrames(); - int64_t bucket_count = 0; - for (int32_t i = 0; i < total_frames; ++i) { - if (const auto* frame = result.reader->GetFrameSync(i)) { - bucket_count += static_cast(frame->GetBuckets().size()); + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_cs_debug_logs_once {}; + + // Opens + iterates every frame + touches the bucket list. Mirrors the + // BM_ReaderSequentialScan shape but against a realistic fixture. + void SequentialScanImpl(benchmark::State& state, const char* filename) { + const std::string path = CsReplayPath(filename); + if (!std::filesystem::exists(path)) { + state.SkipWithError("CS fixture missing (expected under samples/content/reader/cs/)"); + return; + } + VtxBench::WarmFileCache(path); + + int64_t total_frames = 0; + for (auto _ : state) { + auto result = VTX::OpenReplayFile(path); + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + break; } + total_frames = result.reader->GetTotalFrames(); + int64_t bucket_count = 0; + for (int32_t i = 0; i < total_frames; ++i) { + if (const auto* frame = result.reader->GetFrameSync(i)) { + bucket_count += static_cast(frame->GetBuckets().size()); + } + } + benchmark::DoNotOptimize(bucket_count); } - benchmark::DoNotOptimize(bucket_count); - } - state.SetItemsProcessed(state.iterations() * total_frames); - VtxBench::SetNsPerFrame(state, total_frames); -} + state.SetItemsProcessed(state.iterations() * total_frames); + VtxBench::SetNsPerFrame(state, total_frames); + } -} // namespace +} // namespace static void BM_CS_ReaderSequentialScan_FBS(benchmark::State& state) { SequentialScanImpl(state, "cs_fbs.vtx"); } -BENCHMARK(BM_CS_ReaderSequentialScan_FBS) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_CS_ReaderSequentialScan_FBS)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; static void BM_CS_ReaderSequentialScan_Proto(benchmark::State& state) { SequentialScanImpl(state, "cs_proto.vtx"); } -BENCHMARK(BM_CS_ReaderSequentialScan_Proto) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_CS_ReaderSequentialScan_Proto)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; // Full user-facing path: accessor + read Player::Transform + Player::Health // + Player::UniqueID per Player entity across every frame. @@ -105,37 +104,40 @@ static void BM_CS_AccessorSequentialScan_FBS(benchmark::State& state) { int64_t total_frames = 0; for (auto _ : state) { auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); break; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + break; + } auto& reader = result.reader; - auto accessor = reader->CreateAccessor(); + auto accessor = reader->CreateAccessor(); auto key_transform = accessor.Get("Player", "Transform"); - auto key_health = accessor.Get("Player", "Health"); + auto key_health = accessor.Get("Player", "Health"); auto key_unique_id = accessor.Get("Player", "UniqueID"); - auto key_steam_id = accessor.Get("Player", "SteamId"); + auto key_steam_id = accessor.Get("Player", "SteamId"); - if (!key_transform.IsValid() || !key_health.IsValid() || - !key_unique_id.IsValid() || !key_steam_id.IsValid()) { + if (!key_transform.IsValid() || !key_health.IsValid() || !key_unique_id.IsValid() || !key_steam_id.IsValid()) { state.SkipWithError("Player keys did not resolve"); break; } total_frames = reader->GetTotalFrames(); - double tx_accum = 0.0; + double tx_accum = 0.0; int64_t hp_accum = 0; - size_t id_count = 0; - int64_t sid_sum = 0; + size_t id_count = 0; + int64_t sid_sum = 0; for (int32_t i = 0; i < total_frames; ++i) { const auto* frame = reader->GetFrameSync(i); - if (!frame) continue; + if (!frame) + continue; for (const auto& bucket : frame->GetBuckets()) { for (const auto& entity : bucket.entities) { VTX::EntityView view(entity); tx_accum += view.Get(key_transform).translation.x; hp_accum += view.Get(key_health); id_count += view.Get(key_unique_id).size(); - sid_sum += view.Get(key_steam_id); + sid_sum += view.Get(key_steam_id); } } } @@ -148,9 +150,7 @@ static void BM_CS_AccessorSequentialScan_FBS(benchmark::State& state) { state.SetItemsProcessed(state.iterations() * total_frames); VtxBench::SetNsPerFrame(state, total_frames); } -BENCHMARK(BM_CS_AccessorSequentialScan_FBS) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_CS_AccessorSequentialScan_FBS)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; // Random access through the accessor: open + keep the reader alive, then // pick uniformly random frames and read Player::Transform + Health per @@ -164,12 +164,15 @@ static void BM_CS_AccessorRandomAccess_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; - auto accessor = reader->CreateAccessor(); + auto accessor = reader->CreateAccessor(); auto key_transform = accessor.Get("Player", "Transform"); - auto key_health = accessor.Get("Player", "Health"); + auto key_health = accessor.Get("Player", "Health"); if (!key_transform.IsValid() || !key_health.IsValid()) { state.SkipWithError("Player keys did not resolve"); return; @@ -180,14 +183,15 @@ static void BM_CS_AccessorRandomAccess_FBS(benchmark::State& state) { std::uniform_int_distribution dist(0, total - 1); constexpr int kAccessesPerIter = 50; - double tx_accum = 0.0; + double tx_accum = 0.0; int64_t hp_accum = 0; for (auto _ : state) { for (int i = 0; i < kAccessesPerIter; ++i) { const int32_t idx = dist(rng); const auto* frame = reader->GetFrameSync(idx); - if (!frame) continue; + if (!frame) + continue; for (const auto& bucket : frame->GetBuckets()) { for (const auto& entity : bucket.entities) { VTX::EntityView view(entity); @@ -215,14 +219,23 @@ static void BM_CS_DifferConsecutiveFrames_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; auto differ = VtxDiff::CreateDifferFacade(VTX::VtxFormat::FlatBuffers); - if (!differ) { state.SkipWithError("CreateDifferFacade failed"); return; } + if (!differ) { + state.SkipWithError("CreateDifferFacade failed"); + return; + } const int32_t total = reader->GetTotalFrames(); - if (total < 2) { state.SkipWithError("fixture has fewer than 2 frames"); return; } + if (total < 2) { + state.SkipWithError("fixture has fewer than 2 frames"); + return; + } int32_t pair_index = 0; const int32_t max_pair = total - 1; diff --git a/benchmarks/bench_differ.cpp b/benchmarks/bench_differ.cpp index 95ebf07..bf79070 100644 --- a/benchmarks/bench_differ.cpp +++ b/benchmarks/bench_differ.cpp @@ -19,16 +19,16 @@ namespace { -std::string FixturePath(const char* name) { - return std::string(VTX_BENCH_FIXTURES_DIR) + "/" + name; -} + std::string FixturePath(const char* name) { + return std::string(VTX_BENCH_FIXTURES_DIR) + "/" + name; + } -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_differ_debug_logs_once{}; + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_differ_debug_logs_once {}; -} // namespace +} // namespace // Diff cost between two consecutive frames. Opens the fixture once and the // differ once; the measured loop only does DiffRawFrames over sliding pairs. diff --git a/benchmarks/bench_property_cache.cpp b/benchmarks/bench_property_cache.cpp index f6d6b40..60699cf 100644 --- a/benchmarks/bench_property_cache.cpp +++ b/benchmarks/bench_property_cache.cpp @@ -20,35 +20,36 @@ namespace { -std::string FixturePath(const char* name) { - return std::string(VTX_BENCH_FIXTURES_DIR) + "/" + name; -} + std::string FixturePath(const char* name) { + return std::string(VTX_BENCH_FIXTURES_DIR) + "/" + name; + } -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_pcache_debug_logs_once{}; + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_pcache_debug_logs_once {}; -// Collect every (struct_name, property_name) pair present in the cache so -// the benchmark can iterate them in a round-robin. -struct ResolutionKey { - std::string struct_name; - std::string property_name; -}; + // Collect every (struct_name, property_name) pair present in the cache so + // the benchmark can iterate them in a round-robin. + struct ResolutionKey { + std::string struct_name; + std::string property_name; + }; -std::vector CollectResolutionKeys(const VTX::PropertyAddressCache& cache) { - std::vector keys; - for (const auto& [struct_name, struct_id] : cache.name_to_id) { - auto it = cache.structs.find(struct_id); - if (it == cache.structs.end()) continue; - for (const auto& [property_name, _addr] : it->second.properties) { - keys.push_back({struct_name, property_name}); + std::vector CollectResolutionKeys(const VTX::PropertyAddressCache& cache) { + std::vector keys; + for (const auto& [struct_name, struct_id] : cache.name_to_id) { + auto it = cache.structs.find(struct_id); + if (it == cache.structs.end()) + continue; + for (const auto& [property_name, _addr] : it->second.properties) { + keys.push_back({struct_name, property_name}); + } } + return keys; } - return keys; -} -} // namespace +} // namespace // Three hash lookups per resolution: name -> struct_id -> schema cache -> // property address. Per-iteration loops through every known (struct, @@ -63,7 +64,7 @@ static void BM_PropertyAddressCacheLookup(benchmark::State& state) { } const auto cache = result.reader->GetPropertyAddressCache(); - const auto keys = CollectResolutionKeys(cache); + const auto keys = CollectResolutionKeys(cache); if (keys.empty()) { state.SkipWithError("empty PropertyAddressCache"); return; @@ -74,9 +75,11 @@ static void BM_PropertyAddressCacheLookup(benchmark::State& state) { for (auto _ : state) { for (const auto& key : keys) { const auto id_it = cache.name_to_id.find(key.struct_name); - if (id_it == cache.name_to_id.end()) continue; + if (id_it == cache.name_to_id.end()) + continue; const auto struct_it = cache.structs.find(id_it->second); - if (struct_it == cache.structs.end()) continue; + if (struct_it == cache.structs.end()) + continue; const auto prop_it = struct_it->second.properties.find(key.property_name); if (prop_it != struct_it->second.properties.end()) { benchmark::DoNotOptimize(prop_it->second); @@ -85,8 +88,7 @@ static void BM_PropertyAddressCacheLookup(benchmark::State& state) { } } - state.SetItemsProcessed(static_cast(state.iterations()) * - static_cast(keys.size())); + state.SetItemsProcessed(static_cast(state.iterations()) * static_cast(keys.size())); state.counters["keys_per_iter"] = static_cast(keys.size()); benchmark::DoNotOptimize(hit_count); } diff --git a/benchmarks/bench_reader.cpp b/benchmarks/bench_reader.cpp index 5b4c48d..dd2e178 100644 --- a/benchmarks/bench_reader.cpp +++ b/benchmarks/bench_reader.cpp @@ -24,23 +24,21 @@ namespace { -constexpr int32_t kFixtureFrameCount = 10'000; + constexpr int32_t kFixtureFrameCount = 10'000; -std::string FixturePath(const char* name) { - return std::string(VTX_BENCH_FIXTURES_DIR) + "/" + name; -} - -// Silence VTX::Logger Debug messages once (e.g. "Chunk N loaded into RAM"). -// Info/Warning/Error still print so real problems remain visible. This runs -// once at static init, before google/benchmark starts its measured loops. -struct SilenceDebugLogsAtInit { - SilenceDebugLogsAtInit() { - VTX::Logger::Instance().SetDebugEnabled(false); + std::string FixturePath(const char* name) { + return std::string(VTX_BENCH_FIXTURES_DIR) + "/" + name; } -}; -const SilenceDebugLogsAtInit silence_debug_logs_at_init{}; -} // namespace + // Silence VTX::Logger Debug messages once (e.g. "Chunk N loaded into RAM"). + // Info/Warning/Error still print so real problems remain visible. This runs + // once at static init, before google/benchmark starts its measured loops. + struct SilenceDebugLogsAtInit { + SilenceDebugLogsAtInit() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsAtInit silence_debug_logs_at_init {}; + +} // namespace // Open the .vtx and linearly iterate every frame, touching the bucket list // so the reader is forced to deserialize every chunk along the way. This diff --git a/benchmarks/bench_rl.cpp b/benchmarks/bench_rl.cpp index c430515..466a011 100644 --- a/benchmarks/bench_rl.cpp +++ b/benchmarks/bench_rl.cpp @@ -30,59 +30,58 @@ namespace { -std::string RlReplayPath(const char* backend_filename) { - return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() - / "samples" / "content" / "reader" / "rl" / backend_filename) - .string(); -} - -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_rl_debug_logs_once{}; - -// Raw reader scan: open + iterate every frame + touch bucket list. -void SequentialScanImpl(benchmark::State& state, const char* filename) { - const std::string path = RlReplayPath(filename); - if (!std::filesystem::exists(path)) { - state.SkipWithError("RL fixture missing (expected under samples/content/reader/rl/)"); - return; + std::string RlReplayPath(const char* backend_filename) { + return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" / + "reader" / "rl" / backend_filename) + .string(); } - VtxBench::WarmFileCache(path); - int64_t total_frames = 0; - for (auto _ : state) { - auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); break; } - total_frames = result.reader->GetTotalFrames(); - int64_t bucket_count = 0; - for (int32_t i = 0; i < total_frames; ++i) { - if (const auto* frame = result.reader->GetFrameSync(i)) { - bucket_count += static_cast(frame->GetBuckets().size()); + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_rl_debug_logs_once {}; + + // Raw reader scan: open + iterate every frame + touch bucket list. + void SequentialScanImpl(benchmark::State& state, const char* filename) { + const std::string path = RlReplayPath(filename); + if (!std::filesystem::exists(path)) { + state.SkipWithError("RL fixture missing (expected under samples/content/reader/rl/)"); + return; + } + VtxBench::WarmFileCache(path); + + int64_t total_frames = 0; + for (auto _ : state) { + auto result = VTX::OpenReplayFile(path); + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + break; } + total_frames = result.reader->GetTotalFrames(); + int64_t bucket_count = 0; + for (int32_t i = 0; i < total_frames; ++i) { + if (const auto* frame = result.reader->GetFrameSync(i)) { + bucket_count += static_cast(frame->GetBuckets().size()); + } + } + benchmark::DoNotOptimize(bucket_count); } - benchmark::DoNotOptimize(bucket_count); - } - state.SetItemsProcessed(state.iterations() * total_frames); - VtxBench::SetNsPerFrame(state, total_frames); -} + state.SetItemsProcessed(state.iterations() * total_frames); + VtxBench::SetNsPerFrame(state, total_frames); + } -} // namespace +} // namespace static void BM_RL_ReaderSequentialScan_FBS(benchmark::State& state) { SequentialScanImpl(state, "rl_fbs.vtx"); } -BENCHMARK(BM_RL_ReaderSequentialScan_FBS) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_RL_ReaderSequentialScan_FBS)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; static void BM_RL_ReaderSequentialScan_Proto(benchmark::State& state) { SequentialScanImpl(state, "rl_proto.vtx"); } -BENCHMARK(BM_RL_ReaderSequentialScan_Proto) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_RL_ReaderSequentialScan_Proto)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; // Full user-facing path: accessor + read 4 keys per entity (Transform, // ActorId, UniqueId, BoostAmount). Entity struct properties are inlined @@ -99,14 +98,17 @@ static void BM_RL_AccessorSequentialScan_FBS(benchmark::State& state) { int64_t total_frames = 0; for (auto _ : state) { auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); break; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + break; + } auto& reader = result.reader; - auto accessor = reader->CreateAccessor(); + auto accessor = reader->CreateAccessor(); auto key_transform = accessor.Get("Entity", "Transform"); - auto key_actor_id = accessor.Get("Entity", "ActorId"); + auto key_actor_id = accessor.Get("Entity", "ActorId"); auto key_unique_id = accessor.Get("Entity", "UniqueId"); - auto key_boost = accessor.Get("VehiclePickup_Boost", "BoostAmount"); + auto key_boost = accessor.Get("VehiclePickup_Boost", "BoostAmount"); if (!key_transform.IsValid() || !key_actor_id.IsValid() || !key_unique_id.IsValid()) { state.SkipWithError("Entity keys did not resolve"); @@ -114,20 +116,21 @@ static void BM_RL_AccessorSequentialScan_FBS(benchmark::State& state) { } total_frames = reader->GetTotalFrames(); - double tx_accum = 0.0; - int64_t aid_accum = 0; - size_t id_count = 0; - float boost_accum = 0.0f; + double tx_accum = 0.0; + int64_t aid_accum = 0; + size_t id_count = 0; + float boost_accum = 0.0f; for (int32_t i = 0; i < total_frames; ++i) { const auto* frame = reader->GetFrameSync(i); - if (!frame) continue; + if (!frame) + continue; for (const auto& bucket : frame->GetBuckets()) { for (const auto& entity : bucket.entities) { VTX::EntityView view(entity); - tx_accum += view.Get(key_transform).translation.x; - aid_accum += view.Get(key_actor_id); - id_count += view.Get(key_unique_id).size(); + tx_accum += view.Get(key_transform).translation.x; + aid_accum += view.Get(key_actor_id); + id_count += view.Get(key_unique_id).size(); boost_accum += view.Get(key_boost); } } @@ -141,9 +144,7 @@ static void BM_RL_AccessorSequentialScan_FBS(benchmark::State& state) { state.SetItemsProcessed(state.iterations() * total_frames); VtxBench::SetNsPerFrame(state, total_frames); } -BENCHMARK(BM_RL_AccessorSequentialScan_FBS) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_RL_AccessorSequentialScan_FBS)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; // Random access via accessor: simulates a replay-viewer scrub jumping // around arbitrary frames and reading Transform + ActorId per entity. @@ -156,12 +157,15 @@ static void BM_RL_AccessorRandomAccess_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; - auto accessor = reader->CreateAccessor(); + auto accessor = reader->CreateAccessor(); auto key_transform = accessor.Get("Entity", "Transform"); - auto key_actor_id = accessor.Get("Entity", "ActorId"); + auto key_actor_id = accessor.Get("Entity", "ActorId"); if (!key_transform.IsValid() || !key_actor_id.IsValid()) { state.SkipWithError("Entity keys did not resolve"); return; @@ -172,18 +176,19 @@ static void BM_RL_AccessorRandomAccess_FBS(benchmark::State& state) { std::uniform_int_distribution dist(0, total - 1); constexpr int kAccessesPerIter = 50; - double tx_accum = 0.0; + double tx_accum = 0.0; int64_t aid_accum = 0; for (auto _ : state) { for (int i = 0; i < kAccessesPerIter; ++i) { const int32_t idx = dist(rng); const auto* frame = reader->GetFrameSync(idx); - if (!frame) continue; + if (!frame) + continue; for (const auto& bucket : frame->GetBuckets()) { for (const auto& entity : bucket.entities) { VTX::EntityView view(entity); - tx_accum += view.Get(key_transform).translation.x; + tx_accum += view.Get(key_transform).translation.x; aid_accum += view.Get(key_actor_id); } } @@ -206,14 +211,23 @@ static void BM_RL_DifferConsecutiveFrames_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; auto differ = VtxDiff::CreateDifferFacade(VTX::VtxFormat::FlatBuffers); - if (!differ) { state.SkipWithError("CreateDifferFacade failed"); return; } + if (!differ) { + state.SkipWithError("CreateDifferFacade failed"); + return; + } const int32_t total = reader->GetTotalFrames(); - if (total < 2) { state.SkipWithError("fixture has fewer than 2 frames"); return; } + if (total < 2) { + state.SkipWithError("fixture has fewer than 2 frames"); + return; + } int32_t pair_index = 0; const int32_t max_pair = total - 1; diff --git a/benchmarks/bench_scenarios.cpp b/benchmarks/bench_scenarios.cpp index ed5a453..648a0f4 100644 --- a/benchmarks/bench_scenarios.cpp +++ b/benchmarks/bench_scenarios.cpp @@ -39,24 +39,24 @@ namespace { -std::string CsReplayPath(const char* backend_filename) { - return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() - / "samples" / "content" / "reader" / "cs" / backend_filename) - .string(); -} + std::string CsReplayPath(const char* backend_filename) { + return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" / + "reader" / "cs" / backend_filename) + .string(); + } -std::string ArenaReplayPath() { - return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() - / "samples" / "content" / "reader" / "arena" / "arena_from_fbs_ds.vtx") - .string(); -} + std::string ArenaReplayPath() { + return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" / + "reader" / "arena" / "arena_from_fbs_ds.vtx") + .string(); + } -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_scenarios_debug_logs_once{}; + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_scenarios_debug_logs_once {}; -} // namespace +} // namespace // ============================================================================ // Partial / windowed scans (CS fixture) @@ -79,7 +79,10 @@ static void BM_CS_PreviewFirst1000Frames_FBS(benchmark::State& state) { constexpr int32_t kPreviewFrames = 1000; for (auto _ : state) { auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); break; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + break; + } const int32_t limit = std::min(kPreviewFrames, result.reader->GetTotalFrames()); int64_t bucket_count = 0; for (int32_t i = 0; i < limit; ++i) { @@ -92,9 +95,7 @@ static void BM_CS_PreviewFirst1000Frames_FBS(benchmark::State& state) { state.SetItemsProcessed(state.iterations() * kPreviewFrames); VtxBench::SetNsPerFrame(state, kPreviewFrames); } -BENCHMARK(BM_CS_PreviewFirst1000Frames_FBS) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_CS_PreviewFirst1000Frames_FBS)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; // "User seeks to 50% and plays 300 frames" -- typical scrubbing-UI shape. // The middle-of-file seek forces a cold chunk load even on a warm OS cache, @@ -111,10 +112,13 @@ static void BM_CS_SeekMiddlePlay300Frames_FBS(benchmark::State& state) { constexpr int32_t kPlayFrames = 300; for (auto _ : state) { auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); break; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + break; + } const int32_t total = result.reader->GetTotalFrames(); const int32_t start = total / 2; - const int32_t end = std::min(total, start + kPlayFrames); + const int32_t end = std::min(total, start + kPlayFrames); int64_t bucket_count = 0; for (int32_t i = start; i < end; ++i) { if (const auto* frame = result.reader->GetFrameSync(i)) { @@ -126,9 +130,7 @@ static void BM_CS_SeekMiddlePlay300Frames_FBS(benchmark::State& state) { state.SetItemsProcessed(state.iterations() * kPlayFrames); VtxBench::SetNsPerFrame(state, kPlayFrames); } -BENCHMARK(BM_CS_SeekMiddlePlay300Frames_FBS) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_CS_SeekMiddlePlay300Frames_FBS)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; // "Timeline thumbnails" -- one frame every 100, covering the whole replay. // Each sample is in a different chunk, so this is effectively a chunk-miss @@ -146,7 +148,10 @@ static void BM_CS_StridedScan_Every100th_FBS(benchmark::State& state) { int64_t samples_total = 0; for (auto _ : state) { auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); break; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + break; + } const int32_t total = result.reader->GetTotalFrames(); int64_t bucket_count = 0; int64_t samples = 0; @@ -162,9 +167,7 @@ static void BM_CS_StridedScan_Every100th_FBS(benchmark::State& state) { state.SetItemsProcessed(state.iterations() * samples_total); VtxBench::SetNsPerFrame(state, samples_total); } -BENCHMARK(BM_CS_StridedScan_Every100th_FBS) - ->Unit(benchmark::kMillisecond) - BENCH_HEAVY_FIXTURE_SUFFIX; +BENCHMARK(BM_CS_StridedScan_Every100th_FBS)->Unit(benchmark::kMillisecond) BENCH_HEAVY_FIXTURE_SUFFIX; // ============================================================================ // Cache-window sweep (CS fixture) @@ -195,14 +198,16 @@ static void BM_CS_AccessorRandomAccess_CacheSweep_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; - reader->SetCacheWindow(static_cast(cache_window), - static_cast(cache_window)); + reader->SetCacheWindow(static_cast(cache_window), static_cast(cache_window)); - auto accessor = reader->CreateAccessor(); + auto accessor = reader->CreateAccessor(); auto key_transform = accessor.Get("Player", "Transform"); - auto key_health = accessor.Get("Player", "Health"); + auto key_health = accessor.Get("Player", "Health"); if (!key_transform.IsValid() || !key_health.IsValid()) { state.SkipWithError("Player keys did not resolve"); return; @@ -213,14 +218,15 @@ static void BM_CS_AccessorRandomAccess_CacheSweep_FBS(benchmark::State& state) { std::uniform_int_distribution dist(0, total - 1); constexpr int kAccessesPerIter = 50; - double tx_accum = 0.0; + double tx_accum = 0.0; int64_t hp_accum = 0; for (auto _ : state) { for (int i = 0; i < kAccessesPerIter; ++i) { const int32_t idx = dist(rng); const auto* frame = reader->GetFrameSync(idx); - if (!frame) continue; + if (!frame) + continue; for (const auto& bucket : frame->GetBuckets()) { for (const auto& entity : bucket.entities) { VTX::EntityView view(entity); @@ -245,7 +251,11 @@ static void BM_CS_AccessorRandomAccess_CacheSweep_FBS(benchmark::State& state) { // Reps aren't useful here: the RNG is seeded (seed=42) so the access // sequence is identical across runs. Single iteration is deterministic. BENCHMARK(BM_CS_AccessorRandomAccess_CacheSweep_FBS) - ->Arg(0)->Arg(2)->Arg(5)->Arg(10)->Arg(20) + ->Arg(0) + ->Arg(2) + ->Arg(5) + ->Arg(10) + ->Arg(20) ->Unit(benchmark::kMicrosecond); // ============================================================================ @@ -263,8 +273,7 @@ BENCHMARK(BM_CS_AccessorRandomAccess_CacheSweep_FBS) // dangling. Copying the bytes once, pre-measured loop, keeps the diff // path free of that footgun. (It also makes the measured number "pure // diff cost" instead of "diff + cache lookup + potential chunk reload".) -static std::vector SnapshotFrame(VTX::IVtxReaderFacade* reader, - int32_t frame_index) { +static std::vector SnapshotFrame(VTX::IVtxReaderFacade* reader, int32_t frame_index) { auto span = reader->GetRawFrameBytes(frame_index); return std::vector(span.begin(), span.end()); } @@ -281,14 +290,23 @@ static void BM_Arena_DifferIdenticalFrames_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; auto differ = VtxDiff::CreateDifferFacade(VTX::VtxFormat::FlatBuffers); - if (!differ) { state.SkipWithError("CreateDifferFacade failed"); return; } + if (!differ) { + state.SkipWithError("CreateDifferFacade failed"); + return; + } auto frame_a = SnapshotFrame(reader.get(), 0); - if (frame_a.empty()) { state.SkipWithError("GetRawFrameBytes returned empty"); return; } + if (frame_a.empty()) { + state.SkipWithError("GetRawFrameBytes returned empty"); + return; + } for (auto _ : state) { auto patch = differ->DiffRawFrames(frame_a, frame_a); @@ -310,14 +328,23 @@ static void BM_Arena_DifferFirstVsLast_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; auto differ = VtxDiff::CreateDifferFacade(VTX::VtxFormat::FlatBuffers); - if (!differ) { state.SkipWithError("CreateDifferFacade failed"); return; } + if (!differ) { + state.SkipWithError("CreateDifferFacade failed"); + return; + } const int32_t total = reader->GetTotalFrames(); - if (total < 2) { state.SkipWithError("fixture has fewer than 2 frames"); return; } + if (total < 2) { + state.SkipWithError("fixture has fewer than 2 frames"); + return; + } // Copy *both* frames out before the measured loop. The first span // becomes invalid the moment the second call evicts its chunk. @@ -348,14 +375,23 @@ static void BM_CS_DifferIdenticalFrames_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; auto differ = VtxDiff::CreateDifferFacade(VTX::VtxFormat::FlatBuffers); - if (!differ) { state.SkipWithError("CreateDifferFacade failed"); return; } + if (!differ) { + state.SkipWithError("CreateDifferFacade failed"); + return; + } auto frame_a = SnapshotFrame(reader.get(), 0); - if (frame_a.empty()) { state.SkipWithError("GetRawFrameBytes returned empty"); return; } + if (frame_a.empty()) { + state.SkipWithError("GetRawFrameBytes returned empty"); + return; + } for (auto _ : state) { auto patch = differ->DiffRawFrames(frame_a, frame_a); @@ -377,14 +413,23 @@ static void BM_CS_DifferFirstVsLast_FBS(benchmark::State& state) { VtxBench::WarmFileCache(path); auto result = VTX::OpenReplayFile(path); - if (!result) { state.SkipWithError("OpenReplayFile failed"); return; } + if (!result) { + state.SkipWithError("OpenReplayFile failed"); + return; + } auto& reader = result.reader; auto differ = VtxDiff::CreateDifferFacade(VTX::VtxFormat::FlatBuffers); - if (!differ) { state.SkipWithError("CreateDifferFacade failed"); return; } + if (!differ) { + state.SkipWithError("CreateDifferFacade failed"); + return; + } const int32_t total = reader->GetTotalFrames(); - if (total < 2) { state.SkipWithError("fixture has fewer than 2 frames"); return; } + if (total < 2) { + state.SkipWithError("fixture has fewer than 2 frames"); + return; + } auto frame_a = SnapshotFrame(reader.get(), 0); auto frame_b = SnapshotFrame(reader.get(), total - 1); diff --git a/benchmarks/bench_schema.cpp b/benchmarks/bench_schema.cpp index 0c65e24..7760ffb 100644 --- a/benchmarks/bench_schema.cpp +++ b/benchmarks/bench_schema.cpp @@ -30,18 +30,18 @@ namespace { -std::string ArenaSchemaPath() { - return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() - / "samples" / "content" / "writer" / "arena" / "arena_schema.json") - .string(); -} + std::string ArenaSchemaPath() { + return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" / + "writer" / "arena" / "arena_schema.json") + .string(); + } -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_schema_debug_logs_once{}; + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_schema_debug_logs_once {}; -} // namespace +} // namespace // Full load: JSON parse + SchemaStruct population + PropertyAddressCache // construction. This is what OpenReplayFile triggers internally. @@ -56,7 +56,10 @@ static void BM_Schema_LoadArena_Both(benchmark::State& state) { for (auto _ : state) { VTX::SchemaRegistry registry; const bool ok = registry.LoadFromJson(path, VTX::SchemaRegistry::ELoadMethod::Both); - if (!ok) { state.SkipWithError("LoadFromJson failed"); break; } + if (!ok) { + state.SkipWithError("LoadFromJson failed"); + break; + } benchmark::DoNotOptimize(registry); } state.SetItemsProcessed(state.iterations()); @@ -76,7 +79,10 @@ static void BM_Schema_LoadArena_BufferOnly(benchmark::State& state) { for (auto _ : state) { VTX::SchemaRegistry registry; const bool ok = registry.LoadFromJson(path, VTX::SchemaRegistry::ELoadMethod::LoadToBuffer); - if (!ok) { state.SkipWithError("LoadFromJson failed"); break; } + if (!ok) { + state.SkipWithError("LoadFromJson failed"); + break; + } benchmark::DoNotOptimize(registry); } state.SetItemsProcessed(state.iterations()); diff --git a/benchmarks/bench_utils.h b/benchmarks/bench_utils.h index 4dbcdab..429b8a8 100644 --- a/benchmarks/bench_utils.h +++ b/benchmarks/bench_utils.h @@ -24,40 +24,39 @@ namespace VtxBench { -// Read the whole file into a throwaway buffer so the OS file cache is -// warm. Cheap on repeats (~tens of ms even for 91 MB once cached). -// Call once in the benchmark body before the measured loop. -// -// The scratch buffer is heap-allocated on purpose: a 1 MB char array on -// the stack blows Windows' default 1 MB thread stack and the process -// terminates silently with no stderr output. 64 KB is plenty -- the OS -// read-ahead keeps I/O saturated even with a small working buffer. -inline void WarmFileCache(const std::string& path) { - std::ifstream in(path, std::ios::binary); - if (!in) return; - constexpr std::streamsize kChunk = 64 * 1024; // 64 KB, safely on stack - char buf[kChunk]; - while (in.read(buf, kChunk) || in.gcount() > 0) { - benchmark::DoNotOptimize(buf); + // Read the whole file into a throwaway buffer so the OS file cache is + // warm. Cheap on repeats (~tens of ms even for 91 MB once cached). + // Call once in the benchmark body before the measured loop. + // + // The scratch buffer is heap-allocated on purpose: a 1 MB char array on + // the stack blows Windows' default 1 MB thread stack and the process + // terminates silently with no stderr output. 64 KB is plenty -- the OS + // read-ahead keeps I/O saturated even with a small working buffer. + inline void WarmFileCache(const std::string& path) { + std::ifstream in(path, std::ios::binary); + if (!in) + return; + constexpr std::streamsize kChunk = 64 * 1024; // 64 KB, safely on stack + char buf[kChunk]; + while (in.read(buf, kChunk) || in.gcount() > 0) { + benchmark::DoNotOptimize(buf); + } } -} -// Expose "nanoseconds of CPU time per frame" as a first-class counter. -// google/benchmark's items_per_second divides by CPU time; this flips the -// ratio and scales it to ns so the output reads directly as "X ns/frame". -inline void SetNsPerFrame(benchmark::State& state, int64_t total_frames) { - state.counters["ns_per_frame"] = benchmark::Counter( - static_cast(total_frames), - benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kInvert, - benchmark::Counter::kIs1000); -} + // Expose "nanoseconds of CPU time per frame" as a first-class counter. + // google/benchmark's items_per_second divides by CPU time; this flips the + // ratio and scales it to ns so the output reads directly as "X ns/frame". + inline void SetNsPerFrame(benchmark::State& state, int64_t total_frames) { + state.counters["ns_per_frame"] = benchmark::Counter( + static_cast(total_frames), + benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kInvert, benchmark::Counter::kIs1000); + } -} // namespace VtxBench +} // namespace VtxBench // Apply to heavy fixture scans so mean/median/stddev/CV always appear. // // BENCHMARK(BM_CS_ReaderSequentialScan_FBS) // ->Unit(benchmark::kMillisecond) // BENCH_HEAVY_FIXTURE_SUFFIX; -#define BENCH_HEAVY_FIXTURE_SUFFIX \ - ->Repetitions(5)->ReportAggregatesOnly(true) +#define BENCH_HEAVY_FIXTURE_SUFFIX ->Repetitions(5)->ReportAggregatesOnly(true) diff --git a/benchmarks/bench_writer.cpp b/benchmarks/bench_writer.cpp index f1a90f2..9eac4d4 100644 --- a/benchmarks/bench_writer.cpp +++ b/benchmarks/bench_writer.cpp @@ -18,41 +18,41 @@ namespace { -constexpr int kFramesPerIteration = 1'000; + constexpr int kFramesPerIteration = 1'000; -std::string TempOutputPath() { - return (std::filesystem::temp_directory_path() / "vtx_bench_writer_out.vtx").string(); -} + std::string TempOutputPath() { + return (std::filesystem::temp_directory_path() / "vtx_bench_writer_out.vtx").string(); + } -std::string ArenaSchemaPath() { - // benchmarks/ is two levels above the schema in samples/content/writer/arena/. - // VTX_BENCH_FIXTURES_DIR = /benchmarks/fixtures so we walk up twice. - return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() - / "samples" / "content" / "writer" / "arena" / "arena_schema.json") - .string(); -} + std::string ArenaSchemaPath() { + // benchmarks/ is two levels above the schema in samples/content/writer/arena/. + // VTX_BENCH_FIXTURES_DIR = /benchmarks/fixtures so we walk up twice. + return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" / + "writer" / "arena" / "arena_schema.json") + .string(); + } -struct SilenceDebugLogsOnce { - SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } -}; -const SilenceDebugLogsOnce silence_writer_debug_logs_once{}; + struct SilenceDebugLogsOnce { + SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); } + }; + const SilenceDebugLogsOnce silence_writer_debug_logs_once {}; -} // namespace +} // namespace // End-to-end throughput: create writer, record N frames, finalize. static void BM_WriterThroughput(benchmark::State& state) { const std::string schema_path = ArenaSchemaPath(); - const std::string out_path = TempOutputPath(); + const std::string out_path = TempOutputPath(); for (auto _ : state) { VTX::WriterFacadeConfig config; - config.output_filepath = out_path; + config.output_filepath = out_path; config.schema_json_path = schema_path; - config.replay_name = "BenchmarkWriter"; - config.replay_uuid = "bench-0001"; - config.default_fps = 60.0f; + config.replay_name = "BenchmarkWriter"; + config.replay_uuid = "bench-0001"; + config.default_fps = 60.0f; config.chunk_max_frames = 500; - config.use_compression = true; + config.use_compression = true; auto writer = VTX::CreateFlatBuffersWriterFacade(config); if (!writer) { @@ -69,9 +69,9 @@ static void BM_WriterThroughput(benchmark::State& state) { entity.float_properties.push_back(static_cast(i) * 1.5f); VTX::Transform t; - t.translation = { static_cast(i), 0.0, 50.0 }; - t.rotation = { 0.0f, 0.0f, 0.0f, 1.0f }; - t.scale = { 1.0, 1.0, 1.0 }; + t.translation = {static_cast(i), 0.0, 50.0}; + t.rotation = {0.0f, 0.0f, 0.0f, 1.0f}; + t.scale = {1.0, 1.0, 1.0}; entity.transform_properties.push_back(t); bucket.unique_ids.push_back("player_" + std::to_string(i % 10)); diff --git a/samples/advance_write.cpp b/samples/advance_write.cpp index 355d28d..8d47519 100644 --- a/samples/advance_write.cpp +++ b/samples/advance_write.cpp @@ -61,263 +61,209 @@ namespace { -VTX::Vector ToVtxVector(const ::arena_pb::Vec3& value) -{ - return {value.x(), value.y(), value.z()}; -} + VTX::Vector ToVtxVector(const ::arena_pb::Vec3& value) { + return {value.x(), value.y(), value.z()}; + } -VTX::Quat ToVtxQuat(const ::arena_pb::Rotation& value) -{ - return {value.x(), value.y(), value.z(), value.w()}; -} + VTX::Quat ToVtxQuat(const ::arena_pb::Rotation& value) { + return {value.x(), value.y(), value.z(), value.w()}; + } -VTX::Vector ToVtxVector(const ::arena_fb::Vec3& value) -{ - return {value.x(), value.y(), value.z()}; -} + VTX::Vector ToVtxVector(const ::arena_fb::Vec3& value) { + return {value.x(), value.y(), value.z()}; + } -VTX::Quat ToVtxQuat(const ::arena_fb::Rotation& value) -{ - return {value.x(), value.y(), value.z(), value.w()}; -} + VTX::Quat ToVtxQuat(const ::arena_fb::Rotation& value) { + return {value.x(), value.y(), value.z(), value.w()}; + } } // namespace namespace VTX { -// =================================================================== -// Protobuf bindings -- driven by VTX::GenericProtobufLoader -// =================================================================== -// Each Transfer() method maps one protobuf message into a -// VTX::PropertyContainer by delegating scalar/struct/array copies to the -// loader, which resolves field slots via SchemaRegistry. - -template <> -struct ProtoBinding<::arena_pb::Player> { - static void Transfer( - const ::arena_pb::Player& src, - VTX::PropertyContainer& dest, - VTX::GenericProtobufLoader& loader, - const std::string& schema_name) - { - loader.LoadField(dest, schema_name, ArenaSchema::Player::UniqueID, src.unique_id()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Name, src.name()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Team, src.team()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Health, src.health()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Armor, src.armor()); - if (src.has_position()) { - loader.LoadField(dest, schema_name, ArenaSchema::Player::Position, ToVtxVector(src.position())); + // =================================================================== + // Protobuf bindings -- driven by VTX::GenericProtobufLoader + // =================================================================== + // Each Transfer() method maps one protobuf message into a + // VTX::PropertyContainer by delegating scalar/struct/array copies to the + // loader, which resolves field slots via SchemaRegistry. + + template <> + struct ProtoBinding<::arena_pb::Player> { + static void Transfer(const ::arena_pb::Player& src, VTX::PropertyContainer& dest, + VTX::GenericProtobufLoader& loader, const std::string& schema_name) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::UniqueID, src.unique_id()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Name, src.name()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Team, src.team()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Health, src.health()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Armor, src.armor()); + if (src.has_position()) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::Position, ToVtxVector(src.position())); + } + if (src.has_rotation()) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::Rotation, ToVtxQuat(src.rotation())); + } + if (src.has_velocity()) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::Velocity, ToVtxVector(src.velocity())); + } + loader.LoadField(dest, schema_name, ArenaSchema::Player::IsAlive, src.is_alive()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Score, src.score()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Deaths, src.deaths()); } - if (src.has_rotation()) { - loader.LoadField(dest, schema_name, ArenaSchema::Player::Rotation, ToVtxQuat(src.rotation())); + }; + + template <> + struct ProtoBinding<::arena_pb::Projectile> { + static void Transfer(const ::arena_pb::Projectile& src, VTX::PropertyContainer& dest, + VTX::GenericProtobufLoader& loader, const std::string& schema_name) { + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::UniqueID, src.unique_id()); + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::OwnerID, src.owner_id()); + if (src.has_position()) { + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Position, ToVtxVector(src.position())); + } + if (src.has_velocity()) { + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Velocity, ToVtxVector(src.velocity())); + } + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Damage, src.damage()); + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Type, src.type()); } - if (src.has_velocity()) { - loader.LoadField(dest, schema_name, ArenaSchema::Player::Velocity, ToVtxVector(src.velocity())); + }; + + template <> + struct ProtoBinding<::arena_pb::MatchState> { + static void Transfer(const ::arena_pb::MatchState& src, VTX::PropertyContainer& dest, + VTX::GenericProtobufLoader& loader, const std::string& schema_name) { + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::UniqueID, std::string("match_001")); + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::ScoreTeam1, src.score_team1()); + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::ScoreTeam2, src.score_team2()); + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::Round, src.round()); + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::Phase, src.phase()); + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::TimeRemaining, src.time_remaining()); } - loader.LoadField(dest, schema_name, ArenaSchema::Player::IsAlive, src.is_alive()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Score, src.score()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Deaths, src.deaths()); - } -}; + }; -template <> -struct ProtoBinding<::arena_pb::Projectile> { - static void Transfer( - const ::arena_pb::Projectile& src, - VTX::PropertyContainer& dest, - VTX::GenericProtobufLoader& loader, - const std::string& schema_name) - { - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::UniqueID, src.unique_id()); - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::OwnerID, src.owner_id()); - if (src.has_position()) { - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Position, ToVtxVector(src.position())); - } - if (src.has_velocity()) { - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Velocity, ToVtxVector(src.velocity())); - } - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Damage, src.damage()); - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Type, src.type()); - } -}; + template <> + struct ProtoBinding<::arena_pb::FrameData> { + static void TransferToFrame(const ::arena_pb::FrameData& src, VTX::Frame& dest, + VTX::GenericProtobufLoader& loader, const std::string& schema_name) { + (void)schema_name; -template <> -struct ProtoBinding<::arena_pb::MatchState> { - static void Transfer( - const ::arena_pb::MatchState& src, - VTX::PropertyContainer& dest, - VTX::GenericProtobufLoader& loader, - const std::string& schema_name) - { - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::UniqueID, std::string("match_001")); - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::ScoreTeam1, src.score_team1()); - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::ScoreTeam2, src.score_team2()); - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::Round, src.round()); - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::Phase, src.phase()); - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::TimeRemaining, src.time_remaining()); - } -}; + dest = VTX::Frame {}; + VTX::Bucket& entity_bucket = dest.GetBucket("entity"); + entity_bucket.entities.clear(); + entity_bucket.unique_ids.clear(); -template <> -struct ProtoBinding<::arena_pb::FrameData> { - static void TransferToFrame( - const ::arena_pb::FrameData& src, - VTX::Frame& dest, - VTX::GenericProtobufLoader& loader, - const std::string& schema_name) - { - (void)schema_name; - - dest = VTX::Frame{}; - VTX::Bucket& entity_bucket = dest.GetBucket("entity"); - entity_bucket.entities.clear(); - entity_bucket.unique_ids.clear(); - - loader.AppendActorList( - entity_bucket, - ArenaSchema::Player::StructName, - src.players(), - [](const ::arena_pb::Player& player) { return player.unique_id(); }); - - loader.AppendActorList( - entity_bucket, - ArenaSchema::Projectile::StructName, - src.projectiles(), - [](const ::arena_pb::Projectile& projectile) { return projectile.unique_id(); }); - - if (src.has_match_state()) { - loader.AppendSingleActor( - entity_bucket, - ArenaSchema::MatchState::StructName, - src.match_state(), - [](const ::arena_pb::MatchState&) { return std::string("match_001"); }); - } - } -}; + loader.AppendActorList(entity_bucket, ArenaSchema::Player::StructName, src.players(), + [](const ::arena_pb::Player& player) { return player.unique_id(); }); -// =================================================================== -// FlatBuffers bindings -- driven by VTX::GenericFlatBufferLoader -// =================================================================== -// Transfer() receives a pointer to the generated FBS table (zero-copy -// access into the mapped buffer). Field slots are resolved once via -// PropertyAddressCache -- subsequent frames hit the cached addresses. - -template <> -struct FlatBufferBinding<::arena_fb::Player> { - static void Transfer( - const ::arena_fb::Player* src, - VTX::PropertyContainer& dest, - VTX::GenericFlatBufferLoader& loader, - const std::string& schema_name) - { - if (src->unique_id()) { - loader.LoadField(dest, schema_name, ArenaSchema::Player::UniqueID, src->unique_id()->str()); - } - if (src->name()) { - loader.LoadField(dest, schema_name, ArenaSchema::Player::Name, src->name()->str()); - } - loader.LoadField(dest, schema_name, ArenaSchema::Player::Team, src->team()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Health, src->health()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Armor, src->armor()); - if (src->position()) { - loader.LoadField(dest, schema_name, ArenaSchema::Player::Position, ToVtxVector(*src->position())); - } - if (src->rotation()) { - loader.LoadField(dest, schema_name, ArenaSchema::Player::Rotation, ToVtxQuat(*src->rotation())); - } - if (src->velocity()) { - loader.LoadField(dest, schema_name, ArenaSchema::Player::Velocity, ToVtxVector(*src->velocity())); - } - loader.LoadField(dest, schema_name, ArenaSchema::Player::IsAlive, src->is_alive()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Score, src->score()); - loader.LoadField(dest, schema_name, ArenaSchema::Player::Deaths, src->deaths()); - } -}; + loader.AppendActorList(entity_bucket, ArenaSchema::Projectile::StructName, src.projectiles(), + [](const ::arena_pb::Projectile& projectile) { return projectile.unique_id(); }); -template <> -struct FlatBufferBinding<::arena_fb::Projectile> { - static void Transfer( - const ::arena_fb::Projectile* src, - VTX::PropertyContainer& dest, - VTX::GenericFlatBufferLoader& loader, - const std::string& schema_name) - { - if (src->unique_id()) { - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::UniqueID, src->unique_id()->str()); + if (src.has_match_state()) { + loader.AppendSingleActor(entity_bucket, ArenaSchema::MatchState::StructName, src.match_state(), + [](const ::arena_pb::MatchState&) { return std::string("match_001"); }); + } } - if (src->owner_id()) { - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::OwnerID, src->owner_id()->str()); + }; + + // =================================================================== + // FlatBuffers bindings -- driven by VTX::GenericFlatBufferLoader + // =================================================================== + // Transfer() receives a pointer to the generated FBS table (zero-copy + // access into the mapped buffer). Field slots are resolved once via + // PropertyAddressCache -- subsequent frames hit the cached addresses. + + template <> + struct FlatBufferBinding<::arena_fb::Player> { + static void Transfer(const ::arena_fb::Player* src, VTX::PropertyContainer& dest, + VTX::GenericFlatBufferLoader& loader, const std::string& schema_name) { + if (src->unique_id()) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::UniqueID, src->unique_id()->str()); + } + if (src->name()) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::Name, src->name()->str()); + } + loader.LoadField(dest, schema_name, ArenaSchema::Player::Team, src->team()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Health, src->health()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Armor, src->armor()); + if (src->position()) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::Position, ToVtxVector(*src->position())); + } + if (src->rotation()) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::Rotation, ToVtxQuat(*src->rotation())); + } + if (src->velocity()) { + loader.LoadField(dest, schema_name, ArenaSchema::Player::Velocity, ToVtxVector(*src->velocity())); + } + loader.LoadField(dest, schema_name, ArenaSchema::Player::IsAlive, src->is_alive()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Score, src->score()); + loader.LoadField(dest, schema_name, ArenaSchema::Player::Deaths, src->deaths()); } - if (src->position()) { - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Position, ToVtxVector(*src->position())); + }; + + template <> + struct FlatBufferBinding<::arena_fb::Projectile> { + static void Transfer(const ::arena_fb::Projectile* src, VTX::PropertyContainer& dest, + VTX::GenericFlatBufferLoader& loader, const std::string& schema_name) { + if (src->unique_id()) { + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::UniqueID, src->unique_id()->str()); + } + if (src->owner_id()) { + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::OwnerID, src->owner_id()->str()); + } + if (src->position()) { + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Position, ToVtxVector(*src->position())); + } + if (src->velocity()) { + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Velocity, ToVtxVector(*src->velocity())); + } + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Damage, src->damage()); + if (src->type()) { + loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Type, src->type()->str()); + } } - if (src->velocity()) { - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Velocity, ToVtxVector(*src->velocity())); + }; + + template <> + struct FlatBufferBinding<::arena_fb::MatchState> { + static void Transfer(const ::arena_fb::MatchState* src, VTX::PropertyContainer& dest, + VTX::GenericFlatBufferLoader& loader, const std::string& schema_name) { + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::UniqueID, std::string("match_001")); + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::ScoreTeam1, src->score_team1()); + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::ScoreTeam2, src->score_team2()); + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::Round, src->round()); + if (src->phase()) { + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::Phase, src->phase()->str()); + } + loader.LoadField(dest, schema_name, ArenaSchema::MatchState::TimeRemaining, src->time_remaining()); } - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Damage, src->damage()); - if (src->type()) { - loader.LoadField(dest, schema_name, ArenaSchema::Projectile::Type, src->type()->str()); + }; + + template <> + struct FlatBufferBinding<::arena_fb::FrameData> { + static void TransferToFrame(const ::arena_fb::FrameData* src, VTX::Frame& dest, + VTX::GenericFlatBufferLoader& loader, const std::string& schema_name) { + (void)schema_name; + + dest = VTX::Frame {}; + VTX::Bucket& entity_bucket = dest.GetBucket("entity"); + entity_bucket.entities.clear(); + entity_bucket.unique_ids.clear(); + + loader.AppendActorList(entity_bucket, ArenaSchema::Player::StructName, src->players(), + [](const ::arena_fb::Player* player) { + return player->unique_id() ? player->unique_id()->str() : std::string {}; + }); + + loader.AppendActorList(entity_bucket, ArenaSchema::Projectile::StructName, src->projectiles(), + [](const ::arena_fb::Projectile* projectile) { + return projectile->unique_id() ? projectile->unique_id()->str() : std::string {}; + }); + + loader.AppendSingleEntity(entity_bucket, ArenaSchema::MatchState::StructName, src->match_state(), + [](const ::arena_fb::MatchState*) { return std::string("match_001"); }); } - } -}; - -template <> -struct FlatBufferBinding<::arena_fb::MatchState> { - static void Transfer( - const ::arena_fb::MatchState* src, - VTX::PropertyContainer& dest, - VTX::GenericFlatBufferLoader& loader, - const std::string& schema_name) - { - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::UniqueID, std::string("match_001")); - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::ScoreTeam1, src->score_team1()); - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::ScoreTeam2, src->score_team2()); - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::Round, src->round()); - if (src->phase()) { - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::Phase, src->phase()->str()); - } - loader.LoadField(dest, schema_name, ArenaSchema::MatchState::TimeRemaining, src->time_remaining()); - } -}; - -template <> -struct FlatBufferBinding<::arena_fb::FrameData> { - static void TransferToFrame( - const ::arena_fb::FrameData* src, - VTX::Frame& dest, - VTX::GenericFlatBufferLoader& loader, - const std::string& schema_name) - { - (void)schema_name; - - dest = VTX::Frame{}; - VTX::Bucket& entity_bucket = dest.GetBucket("entity"); - entity_bucket.entities.clear(); - entity_bucket.unique_ids.clear(); - - loader.AppendActorList( - entity_bucket, - ArenaSchema::Player::StructName, - src->players(), - [](const ::arena_fb::Player* player) { - return player->unique_id() ? player->unique_id()->str() : std::string{}; - }); - - loader.AppendActorList( - entity_bucket, - ArenaSchema::Projectile::StructName, - src->projectiles(), - [](const ::arena_fb::Projectile* projectile) { - return projectile->unique_id() ? projectile->unique_id()->str() : std::string{}; - }); - - loader.AppendSingleEntity( - entity_bucket, - ArenaSchema::MatchState::StructName, - src->match_state(), - [](const ::arena_fb::MatchState*) { return std::string("match_001"); }); - } -}; + }; } // namespace VTX @@ -334,8 +280,7 @@ class ArenaJsonDataSource : public VTX::IFrameDataSource { explicit ArenaJsonDataSource(std::string filepath) : filepath_(std::move(filepath)) {} - bool Initialize() override - { + bool Initialize() override { std::ifstream ifs(filepath_); if (!ifs.is_open()) { VTX_ERROR("[JSON ] Could not open: {}", filepath_); @@ -355,10 +300,7 @@ class ArenaJsonDataSource : public VTX::IFrameDataSource { return true; } - bool GetNextFrame( - VTX::Frame& out_frame, - VTX::GameTime::GameTimeRegister& out_time) override - { + bool GetNextFrame(VTX::Frame& out_frame, VTX::GameTime::GameTimeRegister& out_time) override { if (cursor_ >= total_) { return false; } @@ -366,7 +308,7 @@ class ArenaJsonDataSource : public VTX::IFrameDataSource { const ArenaFrame& af = replay_.frames[cursor_++]; out_frame = ArenaToVtx::MapFrame(af); - out_time = {af.game_time,std::nullopt, VTX::GameTime::EFilterType::OnlyIncreasing}; + out_time = {af.game_time, std::nullopt, VTX::GameTime::EFilterType::OnlyIncreasing}; return true; } @@ -392,10 +334,10 @@ class ArenaJsonDataSource : public VTX::IFrameDataSource { class ArenaProtoDataSource : public VTX::IFrameDataSource { public: ArenaProtoDataSource(std::string filepath, VTX::SchemaRegistry& schema) - : filepath_(std::move(filepath)), loader_(schema, false) {} + : filepath_(std::move(filepath)) + , loader_(schema, false) {} - bool Initialize() override - { + bool Initialize() override { std::ifstream ifs(filepath_, std::ios::binary); if (!ifs.is_open()) { VTX_ERROR("[Proto] Could not open: {}", filepath_); @@ -411,19 +353,16 @@ class ArenaProtoDataSource : public VTX::IFrameDataSource { return true; } - bool GetNextFrame( - VTX::Frame& out_frame, - VTX::GameTime::GameTimeRegister& out_time) override - { + bool GetNextFrame(VTX::Frame& out_frame, VTX::GameTime::GameTimeRegister& out_time) override { if (cursor_ >= total_) { return false; } const auto& frame = replay_.frames(static_cast(cursor_++)); - out_frame = VTX::Frame{}; + out_frame = VTX::Frame {}; loader_.LoadFrame(frame, out_frame, "ArenaFrame"); - out_time = {frame.game_time(),std::nullopt, VTX::GameTime::EFilterType::OnlyIncreasing}; + out_time = {frame.game_time(), std::nullopt, VTX::GameTime::EFilterType::OnlyIncreasing}; return true; } @@ -449,10 +388,10 @@ class ArenaProtoDataSource : public VTX::IFrameDataSource { class ArenaFbsDataSource : public VTX::IFrameDataSource { public: ArenaFbsDataSource(std::string filepath, const VTX::PropertyAddressCache& cache) - : filepath_(std::move(filepath)), loader_(cache, false) {} + : filepath_(std::move(filepath)) + , loader_(cache, false) {} - bool Initialize() override - { + bool Initialize() override { std::ifstream ifs(filepath_, std::ios::binary | std::ios::ate); if (!ifs.is_open()) { VTX_ERROR("[FBS ] Could not open: {}", filepath_); @@ -475,19 +414,16 @@ class ArenaFbsDataSource : public VTX::IFrameDataSource { return true; } - bool GetNextFrame( - VTX::Frame& out_frame, - VTX::GameTime::GameTimeRegister& out_time) override - { + bool GetNextFrame(VTX::Frame& out_frame, VTX::GameTime::GameTimeRegister& out_time) override { if (cursor_ >= total_) { return false; } const auto* frame = replay_->frames()->Get(static_cast(cursor_++)); - out_frame = VTX::Frame{}; + out_frame = VTX::Frame {}; loader_.LoadFrame(frame, out_frame, "ArenaFrame"); - out_time = {frame->game_time(),std::nullopt, VTX::GameTime::EFilterType::OnlyIncreasing}; + out_time = {frame->game_time(), std::nullopt, VTX::GameTime::EFilterType::OnlyIncreasing}; return true; } @@ -507,16 +443,10 @@ class ArenaFbsDataSource : public VTX::IFrameDataSource { // Pipeline driver -- initialise source, stream frames into the writer // =================================================================== -using WriterFactoryFn = - std::unique_ptr(*)(const VTX::WriterFacadeConfig&); +using WriterFactoryFn = std::unique_ptr (*)(const VTX::WriterFacadeConfig&); -static bool RunPipeline( - VTX::IFrameDataSource& source, - const std::string& output_path, - const std::string& schema_path, - const std::string& uuid, - WriterFactoryFn factory) -{ +static bool RunPipeline(VTX::IFrameDataSource& source, const std::string& output_path, const std::string& schema_path, + const std::string& uuid, WriterFactoryFn factory) { if (!source.Initialize()) { VTX_ERROR("Data source Initialize() failed."); return false; @@ -551,8 +481,7 @@ static bool RunPipeline( return true; } -int main() -{ +int main() { const std::string schema = "content/writer/arena/arena_schema.json"; const std::string writer_dir = "content/writer/arena"; const std::string reader_dir = "content/reader/arena"; @@ -568,30 +497,18 @@ int main() VTX_INFO("--- 1. JSON data source ---"); ArenaJsonDataSource json_ds(writer_dir + "/arena_replay_data.json"); - RunPipeline( - json_ds, - reader_dir + "/arena_from_json_ds.vtx", - schema, - "arena-adv-json-0001", - VTX::CreateFlatBuffersWriterFacade); + RunPipeline(json_ds, reader_dir + "/arena_from_json_ds.vtx", schema, "arena-adv-json-0001", + VTX::CreateFlatBuffersWriterFacade); VTX_INFO("--- 2. Protobuf data source ---"); ArenaProtoDataSource proto_ds(writer_dir + "/arena_replay_data.proto.bin", arena_schema); - RunPipeline( - proto_ds, - reader_dir + "/arena_from_proto_ds.vtx", - schema, - "arena-adv-proto-0001", - VTX::CreateProtobuffWriterFacade); + RunPipeline(proto_ds, reader_dir + "/arena_from_proto_ds.vtx", schema, "arena-adv-proto-0001", + VTX::CreateProtobuffWriterFacade); VTX_INFO("--- 3. FlatBuffers data source ---"); ArenaFbsDataSource fbs_ds(writer_dir + "/arena_replay_data.fbs.bin", arena_schema.GetPropertyCache()); - RunPipeline( - fbs_ds, - reader_dir + "/arena_from_fbs_ds.vtx", - schema, - "arena-adv-fbs-0001", - VTX::CreateFlatBuffersWriterFacade); + RunPipeline(fbs_ds, reader_dir + "/arena_from_fbs_ds.vtx", schema, "arena-adv-fbs-0001", + VTX::CreateFlatBuffersWriterFacade); VTX_INFO("=== Complete ==="); VTX_INFO("Outputs (content/reader/arena/):"); diff --git a/samples/arena_mappings.h b/samples/arena_mappings.h index 27ad23e..8258255 100644 --- a/samples/arena_mappings.h +++ b/samples/arena_mappings.h @@ -34,55 +34,59 @@ // Arena game data model (matches the JSON data source structure) // =================================================================== -struct ArenaVec3 { double x = 0, y = 0, z = 0; }; -struct ArenaQuat { float x = 0, y = 0, z = 0, w = 1; }; +struct ArenaVec3 { + double x = 0, y = 0, z = 0; +}; +struct ArenaQuat { + float x = 0, y = 0, z = 0, w = 1; +}; struct ArenaPlayer { std::string unique_id; std::string name; - int team = 0; - float health = 100.0f; - float armor = 50.0f; - ArenaVec3 position; - ArenaQuat rotation; - ArenaVec3 velocity; - bool is_alive = true; - int score = 0; - int deaths = 0; + int team = 0; + float health = 100.0f; + float armor = 50.0f; + ArenaVec3 position; + ArenaQuat rotation; + ArenaVec3 velocity; + bool is_alive = true; + int score = 0; + int deaths = 0; }; struct ArenaProjectile { std::string unique_id; std::string owner_id; - ArenaVec3 position; - ArenaVec3 velocity; - float damage = 25.0f; - std::string type = "bullet"; + ArenaVec3 position; + ArenaVec3 velocity; + float damage = 25.0f; + std::string type = "bullet"; }; struct ArenaMatchState { - int score_team1 = 0; - int score_team2 = 0; - int round = 1; + int score_team1 = 0; + int score_team2 = 0; + int round = 1; std::string phase; - float time_remaining = 0.0f; + float time_remaining = 0.0f; }; struct ArenaFrame { - int frame_index = 0; - float game_time = 0.0f; - int64_t utc_ticks = 0; - std::vector players; + int frame_index = 0; + float game_time = 0.0f; + int64_t utc_ticks = 0; + std::vector players; std::vector projectiles; - ArenaMatchState match_state; + ArenaMatchState match_state; }; struct ArenaReplayJson { - std::string replay_name; - int total_frames = 0; - int fps = 60; - double duration_seconds = 0.0; - std::vector frames; + std::string replay_name; + int total_frames = 0; + int fps = 60; + double duration_seconds = 0.0; + std::vector frames; }; // =================================================================== @@ -95,42 +99,29 @@ struct ArenaReplayJson { template <> struct VTX::JsonMapping { static constexpr auto GetFields() { - return std::make_tuple( - MakeField("x", &ArenaVec3::x), - MakeField("y", &ArenaVec3::y), - MakeField("z", &ArenaVec3::z) - ); + return std::make_tuple(MakeField("x", &ArenaVec3::x), MakeField("y", &ArenaVec3::y), + MakeField("z", &ArenaVec3::z)); } }; template <> struct VTX::JsonMapping { static constexpr auto GetFields() { - return std::make_tuple( - MakeField("x", &ArenaQuat::x), - MakeField("y", &ArenaQuat::y), - MakeField("z", &ArenaQuat::z), - MakeField("w", &ArenaQuat::w) - ); + return std::make_tuple(MakeField("x", &ArenaQuat::x), MakeField("y", &ArenaQuat::y), + MakeField("z", &ArenaQuat::z), MakeField("w", &ArenaQuat::w)); } }; template <> struct VTX::JsonMapping { static constexpr auto GetFields() { - return std::make_tuple( - MakeField("unique_id", &ArenaPlayer::unique_id), - MakeField("name", &ArenaPlayer::name), - MakeField("team", &ArenaPlayer::team), - MakeField("health", &ArenaPlayer::health), - MakeField("armor", &ArenaPlayer::armor), - MakeField("position", &ArenaPlayer::position), - MakeField("rotation", &ArenaPlayer::rotation), - MakeField("velocity", &ArenaPlayer::velocity), - MakeField("is_alive", &ArenaPlayer::is_alive), - MakeField("score", &ArenaPlayer::score), - MakeField("deaths", &ArenaPlayer::deaths) - ); + return std::make_tuple(MakeField("unique_id", &ArenaPlayer::unique_id), MakeField("name", &ArenaPlayer::name), + MakeField("team", &ArenaPlayer::team), MakeField("health", &ArenaPlayer::health), + MakeField("armor", &ArenaPlayer::armor), MakeField("position", &ArenaPlayer::position), + MakeField("rotation", &ArenaPlayer::rotation), + MakeField("velocity", &ArenaPlayer::velocity), + MakeField("is_alive", &ArenaPlayer::is_alive), MakeField("score", &ArenaPlayer::score), + MakeField("deaths", &ArenaPlayer::deaths)); } }; @@ -138,26 +129,19 @@ template <> struct VTX::JsonMapping { static constexpr auto GetFields() { return std::make_tuple( - MakeField("unique_id", &ArenaProjectile::unique_id), - MakeField("owner_id", &ArenaProjectile::owner_id), - MakeField("position", &ArenaProjectile::position), - MakeField("velocity", &ArenaProjectile::velocity), - MakeField("damage", &ArenaProjectile::damage), - MakeField("type", &ArenaProjectile::type) - ); + MakeField("unique_id", &ArenaProjectile::unique_id), MakeField("owner_id", &ArenaProjectile::owner_id), + MakeField("position", &ArenaProjectile::position), MakeField("velocity", &ArenaProjectile::velocity), + MakeField("damage", &ArenaProjectile::damage), MakeField("type", &ArenaProjectile::type)); } }; template <> struct VTX::JsonMapping { static constexpr auto GetFields() { - return std::make_tuple( - MakeField("score_team1", &ArenaMatchState::score_team1), - MakeField("score_team2", &ArenaMatchState::score_team2), - MakeField("round", &ArenaMatchState::round), - MakeField("phase", &ArenaMatchState::phase), - MakeField("time_remaining", &ArenaMatchState::time_remaining) - ); + return std::make_tuple(MakeField("score_team1", &ArenaMatchState::score_team1), + MakeField("score_team2", &ArenaMatchState::score_team2), + MakeField("round", &ArenaMatchState::round), MakeField("phase", &ArenaMatchState::phase), + MakeField("time_remaining", &ArenaMatchState::time_remaining)); } }; @@ -165,26 +149,20 @@ template <> struct VTX::JsonMapping { static constexpr auto GetFields() { return std::make_tuple( - MakeField("frame_index", &ArenaFrame::frame_index), - MakeField("game_time", &ArenaFrame::game_time), - MakeField("utc_ticks", &ArenaFrame::utc_ticks), - MakeField("players", &ArenaFrame::players), - MakeField("projectiles", &ArenaFrame::projectiles), - MakeField("match_state", &ArenaFrame::match_state) - ); + MakeField("frame_index", &ArenaFrame::frame_index), MakeField("game_time", &ArenaFrame::game_time), + MakeField("utc_ticks", &ArenaFrame::utc_ticks), MakeField("players", &ArenaFrame::players), + MakeField("projectiles", &ArenaFrame::projectiles), MakeField("match_state", &ArenaFrame::match_state)); } }; template <> struct VTX::JsonMapping { static constexpr auto GetFields() { - return std::make_tuple( - MakeField("replay_name", &ArenaReplayJson::replay_name), - MakeField("total_frames", &ArenaReplayJson::total_frames), - MakeField("fps", &ArenaReplayJson::fps), - MakeField("duration_seconds", &ArenaReplayJson::duration_seconds), - MakeField("frames", &ArenaReplayJson::frames) - ); + return std::make_tuple(MakeField("replay_name", &ArenaReplayJson::replay_name), + MakeField("total_frames", &ArenaReplayJson::total_frames), + MakeField("fps", &ArenaReplayJson::fps), + MakeField("duration_seconds", &ArenaReplayJson::duration_seconds), + MakeField("frames", &ArenaReplayJson::frames)); } }; @@ -196,63 +174,73 @@ struct VTX::JsonMapping { namespace ArenaToVtx { -inline VTX::Vector ToVtxVector(const ArenaVec3& v) { return {v.x, v.y, v.z}; } -inline VTX::Quat ToVtxQuat(const ArenaQuat& q) { return {q.x, q.y, q.z, q.w}; } + inline VTX::Vector ToVtxVector(const ArenaVec3& v) { + return {v.x, v.y, v.z}; + } + inline VTX::Quat ToVtxQuat(const ArenaQuat& q) { + return {q.x, q.y, q.z, q.w}; + } -/// Player → entity_type_id 0 -/// string[0]=UniqueID string[1]=Name -/// int32[0]=Team int32[1]=Score int32[2]=Deaths -/// float[0]=Health float[1]=Armor -/// vector[0]=Position vector[1]=Velocity -/// quat[0]=Rotation -/// bool[0]=IsAlive -inline VTX::PropertyContainer MapPlayer(const ArenaPlayer& p) { - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = { p.unique_id, p.name }; - pc.int32_properties = { p.team, p.score, p.deaths }; - pc.float_properties = { p.health, p.armor }; - pc.vector_properties = { ToVtxVector(p.position), ToVtxVector(p.velocity) }; - pc.quat_properties = { ToVtxQuat(p.rotation) }; - pc.bool_properties = { p.is_alive }; - return pc; -} + /// Player → entity_type_id 0 + /// string[0]=UniqueID string[1]=Name + /// int32[0]=Team int32[1]=Score int32[2]=Deaths + /// float[0]=Health float[1]=Armor + /// vector[0]=Position vector[1]=Velocity + /// quat[0]=Rotation + /// bool[0]=IsAlive + inline VTX::PropertyContainer MapPlayer(const ArenaPlayer& p) { + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {p.unique_id, p.name}; + pc.int32_properties = {p.team, p.score, p.deaths}; + pc.float_properties = {p.health, p.armor}; + pc.vector_properties = {ToVtxVector(p.position), ToVtxVector(p.velocity)}; + pc.quat_properties = {ToVtxQuat(p.rotation)}; + pc.bool_properties = {p.is_alive}; + return pc; + } -/// Projectile → entity_type_id 1 -/// string[0]=UniqueID string[1]=OwnerID string[2]=Type -/// vector[0]=Position vector[1]=Velocity -/// float[0]=Damage -inline VTX::PropertyContainer MapProjectile(const ArenaProjectile& pr) { - VTX::PropertyContainer pc; - pc.entity_type_id = 1; - pc.string_properties = { pr.unique_id, pr.owner_id, pr.type }; - pc.vector_properties = { ToVtxVector(pr.position), ToVtxVector(pr.velocity) }; - pc.float_properties = { pr.damage }; - return pc; -} + /// Projectile → entity_type_id 1 + /// string[0]=UniqueID string[1]=OwnerID string[2]=Type + /// vector[0]=Position vector[1]=Velocity + /// float[0]=Damage + inline VTX::PropertyContainer MapProjectile(const ArenaProjectile& pr) { + VTX::PropertyContainer pc; + pc.entity_type_id = 1; + pc.string_properties = {pr.unique_id, pr.owner_id, pr.type}; + pc.vector_properties = {ToVtxVector(pr.position), ToVtxVector(pr.velocity)}; + pc.float_properties = {pr.damage}; + return pc; + } -/// MatchState → entity_type_id 2 -/// string[0]=UniqueID string[1]=Phase -/// int32[0]=ScoreTeam1 int32[1]=ScoreTeam2 int32[2]=Round -/// float[0]=TimeRemaining -inline VTX::PropertyContainer MapMatchState(const ArenaMatchState& m) { - VTX::PropertyContainer pc; - pc.entity_type_id = 2; - pc.string_properties = { "match_001", m.phase }; - pc.int32_properties = { m.score_team1, m.score_team2, m.round }; - pc.float_properties = { m.time_remaining }; - return pc; -} + /// MatchState → entity_type_id 2 + /// string[0]=UniqueID string[1]=Phase + /// int32[0]=ScoreTeam1 int32[1]=ScoreTeam2 int32[2]=Round + /// float[0]=TimeRemaining + inline VTX::PropertyContainer MapMatchState(const ArenaMatchState& m) { + VTX::PropertyContainer pc; + pc.entity_type_id = 2; + pc.string_properties = {"match_001", m.phase}; + pc.int32_properties = {m.score_team1, m.score_team2, m.round}; + pc.float_properties = {m.time_remaining}; + return pc; + } -/// Full frame → single "entity" bucket. -inline VTX::Frame MapFrame(const ArenaFrame& af) { - VTX::Frame frame; - VTX::Bucket& bucket = frame.CreateBucket("entity"); - for (const auto& p : af.players) { bucket.unique_ids.push_back(p.unique_id); bucket.entities.push_back(MapPlayer(p)); } - for (const auto& pr : af.projectiles) { bucket.unique_ids.push_back(pr.unique_id); bucket.entities.push_back(MapProjectile(pr)); } - bucket.unique_ids.push_back("match_001"); - bucket.entities.push_back(MapMatchState(af.match_state)); - return frame; -} + /// Full frame → single "entity" bucket. + inline VTX::Frame MapFrame(const ArenaFrame& af) { + VTX::Frame frame; + VTX::Bucket& bucket = frame.CreateBucket("entity"); + for (const auto& p : af.players) { + bucket.unique_ids.push_back(p.unique_id); + bucket.entities.push_back(MapPlayer(p)); + } + for (const auto& pr : af.projectiles) { + bucket.unique_ids.push_back(pr.unique_id); + bucket.entities.push_back(MapProjectile(pr)); + } + bucket.unique_ids.push_back("match_001"); + bucket.entities.push_back(MapMatchState(af.match_state)); + return frame; + } } // namespace ArenaToVtx diff --git a/samples/basic_diff.cpp b/samples/basic_diff.cpp index cf283ce..fa6b3f5 100644 --- a/samples/basic_diff.cpp +++ b/samples/basic_diff.cpp @@ -25,30 +25,34 @@ namespace { -const char* OpName(VtxDiff::DiffOperation op) { - switch (op) { - case VtxDiff::DiffOperation::Add: return "Add"; - case VtxDiff::DiffOperation::Remove: return "Remove"; - case VtxDiff::DiffOperation::Replace: return "Replace"; - case VtxDiff::DiffOperation::ReplaceRange: return "ReplaceRange"; + const char* OpName(VtxDiff::DiffOperation op) { + switch (op) { + case VtxDiff::DiffOperation::Add: + return "Add"; + case VtxDiff::DiffOperation::Remove: + return "Remove"; + case VtxDiff::DiffOperation::Replace: + return "Replace"; + case VtxDiff::DiffOperation::ReplaceRange: + return "ReplaceRange"; + } + return "?"; } - return "?"; -} -std::string PathToString(const VtxDiff::DiffIndexPath& p) { - std::string s = "["; - for (size_t i = 0; i < p.count; ++i) { - if (i > 0) s += ","; - s += std::to_string(p.indices[i]); + std::string PathToString(const VtxDiff::DiffIndexPath& p) { + std::string s = "["; + for (size_t i = 0; i < p.count; ++i) { + if (i > 0) + s += ","; + s += std::to_string(p.indices[i]); + } + s += "]"; + return s; } - s += "]"; - return s; -} } // namespace -int main(int argc, char* argv[]) -{ +int main(int argc, char* argv[]) { std::string filepath = "content/reader/arena/arena_from_fbs_ds.vtx"; bool fail_on_empty = false; @@ -99,7 +103,7 @@ int main(int argc, char* argv[]) // ---- Run the diff ---------------------------------------------------- VtxDiff::DiffOptions opts; opts.compare_floats_with_epsilon = true; - opts.float_epsilon = 1e-5f; + opts.float_epsilon = 1e-5f; VtxDiff::PatchIndex patch = differ->DiffRawFrames(bytes_a, raw_b, opts); @@ -110,15 +114,11 @@ int main(int argc, char* argv[]) constexpr size_t kDetailLimit = 20; for (size_t i = 0; i < patch.operations.size() && i < kDetailLimit; ++i) { const auto& op = patch.operations[i]; - VTX_INFO(" {:>12} container={} path={} actor='{}'", - OpName(op.Operation), - VtxDiff::TypeToFieldName(op.ContainerType), - PathToString(op.Path), - op.ActorId); + VTX_INFO(" {:>12} container={} path={} actor='{}'", OpName(op.Operation), + VtxDiff::TypeToFieldName(op.ContainerType), PathToString(op.Path), op.ActorId); } if (patch.operations.size() > kDetailLimit) { - VTX_INFO(" ... {} more operation(s) suppressed", - patch.operations.size() - kDetailLimit); + VTX_INFO(" ... {} more operation(s) suppressed", patch.operations.size() - kDetailLimit); } if (fail_on_empty && patch.operations.empty()) { diff --git a/samples/basic_read.cpp b/samples/basic_read.cpp index 5711d63..f36ac9a 100644 --- a/samples/basic_read.cpp +++ b/samples/basic_read.cpp @@ -22,11 +22,8 @@ #include -int main(int argc, char* argv[]) -{ - const std::string filepath = (argc > 1) - ? argv[1] - : "content/reader/arena/arena_from_fbs_ds.vtx"; +int main(int argc, char* argv[]) { + const std::string filepath = (argc > 1) ? argv[1] : "content/reader/arena/arena_from_fbs_ds.vtx"; auto result = VTX::OpenReplayFile(filepath); diff --git a/samples/basic_write.cpp b/samples/basic_write.cpp index a2241b7..e0c2a1b 100644 --- a/samples/basic_write.cpp +++ b/samples/basic_write.cpp @@ -18,26 +18,21 @@ #include -int main(int argc, char* argv[]) -{ - const std::string schema_path = (argc > 1) - ? argv[1] - : "content/writer/arena/arena_schema.json"; - const std::string output_path = (argc > 2) - ? argv[2] - : "sample_output.vtx"; +int main(int argc, char* argv[]) { + const std::string schema_path = (argc > 1) ? argv[1] : "content/writer/arena/arena_schema.json"; + const std::string output_path = (argc > 2) ? argv[2] : "sample_output.vtx"; const int frame_count = (argc > 3) ? std::max(1, std::atoi(argv[3])) : 100; // Configure the writer. VTX::WriterFacadeConfig config; - config.output_filepath = output_path; + config.output_filepath = output_path; config.schema_json_path = schema_path; - config.replay_name = "BasicWriteSample"; - config.replay_uuid = "sample-0001"; - config.default_fps = 60.0f; + config.replay_name = "BasicWriteSample"; + config.replay_uuid = "sample-0001"; + config.default_fps = 60.0f; config.chunk_max_frames = 500; - config.use_compression = true; + config.use_compression = true; // Create a FlatBuffers writer (use CreateProtobuffWriterFacade for Protobuf). auto writer = VTX::CreateFlatBuffersWriterFacade(config); @@ -61,9 +56,9 @@ int main(int argc, char* argv[]) // Add a transform property (index 0). VTX::Transform t; - t.translation = { static_cast(i), 0.0, 50.0 }; - t.rotation = { 0.0f, 0.0f, 0.0f, 1.0f }; - t.scale = { 1.0, 1.0, 1.0 }; + t.translation = {static_cast(i), 0.0, 50.0}; + t.rotation = {0.0f, 0.0f, 0.0f, 1.0f}; + t.scale = {1.0, 1.0, 1.0}; entity.transform_properties.push_back(t); bucket.unique_ids.push_back("player_" + std::to_string(i % 10)); @@ -79,6 +74,6 @@ int main(int argc, char* argv[]) writer->Flush(); writer->Stop(); - VTX_INFO("Wrote {} frames to {}",frame_count, output_path); + VTX_INFO("Wrote {} frames to {}", frame_count, output_path); return 0; } diff --git a/samples/generate_replay.cpp b/samples/generate_replay.cpp index bf8826c..f8b6490 100644 --- a/samples/generate_replay.cpp +++ b/samples/generate_replay.cpp @@ -59,27 +59,27 @@ // SECTION 1 — Constants // =================================================================== -static constexpr int TOTAL_FRAMES = 3600; -static constexpr float FPS = 60.0f; -static constexpr float DT = 1.0f / 60.0f; -static constexpr int NUM_PLAYERS = 10; // 5v5 -static constexpr double ARENA_MIN = -50.0; -static constexpr double ARENA_MAX = 50.0; -static constexpr int RESPAWN_FRAMES = 180; // 3 s +static constexpr int TOTAL_FRAMES = 3600; +static constexpr float FPS = 60.0f; +static constexpr float DT = 1.0f / 60.0f; +static constexpr int NUM_PLAYERS = 10; // 5v5 +static constexpr double ARENA_MIN = -50.0; +static constexpr double ARENA_MAX = 50.0; +static constexpr int RESPAWN_FRAMES = 180; // 3 s -static constexpr int64_t UTC_TICKS_PER_FRAME = 166'666LL; // 10^7 / 60 +static constexpr int64_t UTC_TICKS_PER_FRAME = 166'666LL; // 10^7 / 60 // Phase boundaries. -static constexpr int WARMUP_END = 300; +static constexpr int WARMUP_END = 300; static constexpr int PLAYING_END = 3300; // Combat cadence (active during "playing" only). -static constexpr int PROJ_INTERVAL = 300; -static constexpr int PROJ_OFFSET = 150; -static constexpr int KILL_INTERVAL = 600; -static constexpr int KILL_OFFSET = 300; +static constexpr int PROJ_INTERVAL = 300; +static constexpr int PROJ_OFFSET = 150; +static constexpr int KILL_INTERVAL = 600; +static constexpr int KILL_OFFSET = 300; static constexpr int KILL_MIN_FRAME = 600; -static constexpr int PROJ_LIFETIME = 90; +static constexpr int PROJ_LIFETIME = 90; // =================================================================== @@ -89,13 +89,13 @@ static constexpr int PROJ_LIFETIME = 90; struct PlayerSim { std::string unique_id; std::string name; - int team = 1; + int team = 1; float health = 100.0f, armor = 50.0f; double pos_x = 0, pos_y = 0, pos_z = 0; double vel_x = 0, vel_y = 0, vel_z = 0; - float rot_x = 0, rot_y = 0, rot_z = 0, rot_w = 1; - bool is_alive = true; - int score = 0, deaths = 0, respawn_timer = 0; + float rot_x = 0, rot_y = 0, rot_z = 0, rot_w = 1; + bool is_alive = true; + int score = 0, deaths = 0, respawn_timer = 0; // Movement params (set once at init). double base_x = 0, base_z = 0; double freq_x = 0, freq_z = 0; @@ -106,7 +106,7 @@ struct ProjectileSim { std::string unique_id, owner_id; double pos_x = 0, pos_y = 0, pos_z = 0; double vel_x = 0, vel_y = 0, vel_z = 0; - float damage = 25.0f; + float damage = 25.0f; std::string type = "bullet"; int lifetime = PROJ_LIFETIME; }; @@ -118,10 +118,10 @@ struct MatchSim { }; struct FrameSnapshot { - std::vector players; + std::vector players; std::vector projectiles; - MatchSim match; - float game_time = 0.0f; + MatchSim match; + float game_time = 0.0f; int64_t utc_ticks = 0; }; @@ -131,17 +131,14 @@ struct FrameSnapshot { // =================================================================== static int64_t GetUtcNowTicks() { - auto ns = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); + auto ns = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count(); return ns / 100; } -static std::vector RunSimulation(int64_t base_utc) -{ - static const char* const kNames[NUM_PLAYERS] = { - "Alpha","Bravo","Charlie","Delta","Echo", - "Foxtrot","Golf","Hotel","India","Juliet" - }; +static std::vector RunSimulation(int64_t base_utc) { + static const char* const kNames[NUM_PLAYERS] = {"Alpha", "Bravo", "Charlie", "Delta", "Echo", + "Foxtrot", "Golf", "Hotel", "India", "Juliet"}; uint64_t rng = 42; auto rand_d = [&](double lo, double hi) { @@ -154,14 +151,16 @@ static std::vector RunSimulation(int64_t base_utc) for (int i = 0; i < NUM_PLAYERS; ++i) { auto& p = players[i]; p.unique_id = "player_" + std::to_string(i); - p.name = kNames[i]; - p.team = (i < 5) ? 1 : 2; + p.name = kNames[i]; + p.team = (i < 5) ? 1 : 2; p.base_x = ARENA_MIN + (ARENA_MAX - ARENA_MIN) * (i + 0.5) / NUM_PLAYERS; p.base_z = (p.team == 1) ? -20.0 : 20.0; - p.pos_x = p.base_x; - p.pos_z = p.base_z; - p.freq_x = rand_d(0.3, 1.2); p.freq_z = rand_d(0.3, 1.2); - p.phase_x = rand_d(0.0, 6.28); p.phase_z = rand_d(0.0, 6.28); + p.pos_x = p.base_x; + p.pos_z = p.base_z; + p.freq_x = rand_d(0.3, 1.2); + p.freq_z = rand_d(0.3, 1.2); + p.phase_x = rand_d(0.0, 6.28); + p.phase_z = rand_d(0.0, 6.28); } MatchSim match; @@ -175,23 +174,41 @@ static std::vector RunSimulation(int64_t base_utc) const float t = static_cast(f) * DT; // Phase. - if (f < WARMUP_END) { match.phase = "warmup"; match.time_remaining = (WARMUP_END - f) * DT; } - else if (f < PLAYING_END) { match.phase = "playing"; match.time_remaining = (PLAYING_END - f) * DT; } - else { match.phase = "roundend"; match.time_remaining = (TOTAL_FRAMES- f) * DT; } + if (f < WARMUP_END) { + match.phase = "warmup"; + match.time_remaining = (WARMUP_END - f) * DT; + } else if (f < PLAYING_END) { + match.phase = "playing"; + match.time_remaining = (PLAYING_END - f) * DT; + } else { + match.phase = "roundend"; + match.time_remaining = (TOTAL_FRAMES - f) * DT; + } // Movement. for (auto& p : players) { if (!p.is_alive) { - if (--p.respawn_timer <= 0) { p.is_alive = true; p.health = 100; p.armor = 50; p.pos_x = p.base_x; p.pos_z = p.base_z; } - p.vel_x = p.vel_y = p.vel_z = 0; continue; + if (--p.respawn_timer <= 0) { + p.is_alive = true; + p.health = 100; + p.armor = 50; + p.pos_x = p.base_x; + p.pos_z = p.base_z; + } + p.vel_x = p.vel_y = p.vel_z = 0; + continue; } double px = p.pos_x, pz = p.pos_z; p.pos_x = std::clamp(p.base_x + 15.0 * std::sin(p.freq_x * t + p.phase_x), ARENA_MIN, ARENA_MAX); p.pos_z = std::clamp(p.base_z + 10.0 * std::sin(p.freq_z * t + p.phase_z), ARENA_MIN, ARENA_MAX); - p.vel_x = (p.pos_x - px) / DT; p.vel_z = (p.pos_z - pz) / DT; p.vel_y = 0; + p.vel_x = (p.pos_x - px) / DT; + p.vel_z = (p.pos_z - pz) / DT; + p.vel_y = 0; if (std::abs(p.vel_x) > 0.001 || std::abs(p.vel_z) > 0.001) { float yaw = std::atan2(float(p.vel_x), float(p.vel_z)); - p.rot_y = std::sin(yaw * 0.5f); p.rot_w = std::cos(yaw * 0.5f); p.rot_x = p.rot_z = 0; + p.rot_y = std::sin(yaw * 0.5f); + p.rot_w = std::cos(yaw * 0.5f); + p.rot_x = p.rot_z = 0; } } @@ -203,7 +220,9 @@ static std::vector RunSimulation(int64_t base_utc) ProjectileSim proj; proj.unique_id = "proj_" + std::to_string(next_proj_id++); proj.owner_id = players[si].unique_id; - proj.pos_x = players[si].pos_x; proj.pos_y = 1.0; proj.pos_z = players[si].pos_z; + proj.pos_x = players[si].pos_x; + proj.pos_y = 1.0; + proj.pos_z = players[si].pos_z; proj.vel_z = (players[si].team == 1) ? 30.0 : -30.0; projectiles.push_back(std::move(proj)); } @@ -212,20 +231,33 @@ static std::vector RunSimulation(int64_t base_utc) int ki = ((f / KILL_INTERVAL) * 3) % NUM_PLAYERS; int vi = (ki + 5) % NUM_PLAYERS; if (players[ki].is_alive && players[vi].is_alive) { - players[vi].health = 0; players[vi].armor = 0; players[vi].is_alive = false; - players[vi].respawn_timer = RESPAWN_FRAMES; players[vi].deaths++; + players[vi].health = 0; + players[vi].armor = 0; + players[vi].is_alive = false; + players[vi].respawn_timer = RESPAWN_FRAMES; + players[vi].deaths++; players[ki].score++; - if (players[ki].team == 1) match.score_team1++; else match.score_team2++; + if (players[ki].team == 1) + match.score_team1++; + else + match.score_team2++; } } } // Advance projectiles. - for (auto& pr : projectiles) { pr.pos_x += pr.vel_x*DT; pr.pos_y += pr.vel_y*DT; pr.pos_z += pr.vel_z*DT; pr.lifetime--; } - std::erase_if(projectiles, [](auto& p){ return p.lifetime <= 0; }); + for (auto& pr : projectiles) { + pr.pos_x += pr.vel_x * DT; + pr.pos_y += pr.vel_y * DT; + pr.pos_z += pr.vel_z * DT; + pr.lifetime--; + } + std::erase_if(projectiles, [](auto& p) { return p.lifetime <= 0; }); FrameSnapshot snap; - snap.players = players; snap.projectiles = projectiles; snap.match = match; + snap.players = players; + snap.projectiles = projectiles; + snap.match = match; snap.game_time = t; snap.utc_ticks = base_utc + int64_t(f) * UTC_TICKS_PER_FRAME; frames.push_back(std::move(snap)); @@ -239,46 +271,57 @@ static std::vector RunSimulation(int64_t base_utc) // =================================================================== // ---- 4a: JSON ---- -static void ExportJsonSource(const std::vector& frames, const std::string& path) -{ +static void ExportJsonSource(const std::vector& frames, const std::string& path) { nlohmann::json root; - root["replay_name"] = "Arena Sample Replay"; - root["total_frames"] = TOTAL_FRAMES; - root["fps"] = int(FPS); + root["replay_name"] = "Arena Sample Replay"; + root["total_frames"] = TOTAL_FRAMES; + root["fps"] = int(FPS); root["duration_seconds"] = double(TOTAL_FRAMES) / FPS; - auto& jf = root["frames"]; jf = nlohmann::json::array(); + auto& jf = root["frames"]; + jf = nlohmann::json::array(); for (size_t i = 0; i < frames.size(); ++i) { const auto& s = frames[i]; nlohmann::json f; - f["frame_index"] = i; f["game_time"] = s.game_time; f["utc_ticks"] = s.utc_ticks; + f["frame_index"] = i; + f["game_time"] = s.game_time; + f["utc_ticks"] = s.utc_ticks; f["players"] = nlohmann::json::array(); for (const auto& p : s.players) - f["players"].push_back({{"unique_id",p.unique_id},{"name",p.name},{"team",p.team}, - {"health",p.health},{"armor",p.armor}, - {"position",{{"x",p.pos_x},{"y",p.pos_y},{"z",p.pos_z}}}, - {"rotation",{{"x",p.rot_x},{"y",p.rot_y},{"z",p.rot_z},{"w",p.rot_w}}}, - {"velocity",{{"x",p.vel_x},{"y",p.vel_y},{"z",p.vel_z}}}, - {"is_alive",p.is_alive},{"score",p.score},{"deaths",p.deaths}}); + f["players"].push_back({{"unique_id", p.unique_id}, + {"name", p.name}, + {"team", p.team}, + {"health", p.health}, + {"armor", p.armor}, + {"position", {{"x", p.pos_x}, {"y", p.pos_y}, {"z", p.pos_z}}}, + {"rotation", {{"x", p.rot_x}, {"y", p.rot_y}, {"z", p.rot_z}, {"w", p.rot_w}}}, + {"velocity", {{"x", p.vel_x}, {"y", p.vel_y}, {"z", p.vel_z}}}, + {"is_alive", p.is_alive}, + {"score", p.score}, + {"deaths", p.deaths}}); f["projectiles"] = nlohmann::json::array(); for (const auto& pr : s.projectiles) - f["projectiles"].push_back({{"unique_id",pr.unique_id},{"owner_id",pr.owner_id}, - {"position",{{"x",pr.pos_x},{"y",pr.pos_y},{"z",pr.pos_z}}}, - {"velocity",{{"x",pr.vel_x},{"y",pr.vel_y},{"z",pr.vel_z}}}, - {"damage",pr.damage},{"type",pr.type}}); - - f["match_state"] = {{"score_team1",s.match.score_team1},{"score_team2",s.match.score_team2}, - {"round",s.match.round},{"phase",s.match.phase},{"time_remaining",s.match.time_remaining}}; + f["projectiles"].push_back({{"unique_id", pr.unique_id}, + {"owner_id", pr.owner_id}, + {"position", {{"x", pr.pos_x}, {"y", pr.pos_y}, {"z", pr.pos_z}}}, + {"velocity", {{"x", pr.vel_x}, {"y", pr.vel_y}, {"z", pr.vel_z}}}, + {"damage", pr.damage}, + {"type", pr.type}}); + + f["match_state"] = {{"score_team1", s.match.score_team1}, + {"score_team2", s.match.score_team2}, + {"round", s.match.round}, + {"phase", s.match.phase}, + {"time_remaining", s.match.time_remaining}}; jf.push_back(std::move(f)); } std::ofstream(path) << root.dump(2); } // ---- 4b: Protobuf binary ---- -static void ExportProtoSource(const std::vector& frames, const std::string& path) -{ +static void ExportProtoSource(const std::vector& frames, const std::string& path) { ::arena_pb::ArenaReplay replay; replay.set_replay_name("Arena Sample Replay"); replay.set_total_frames(TOTAL_FRAMES); @@ -294,23 +337,49 @@ static void ExportProtoSource(const std::vector& frames, const st for (const auto& p : s.players) { auto* pp = fd->add_players(); - pp->set_unique_id(p.unique_id); pp->set_name(p.name); pp->set_team(p.team); - pp->set_health(p.health); pp->set_armor(p.armor); - auto* pos = pp->mutable_position(); pos->set_x(p.pos_x); pos->set_y(p.pos_y); pos->set_z(p.pos_z); - auto* rot = pp->mutable_rotation(); rot->set_x(p.rot_x); rot->set_y(p.rot_y); rot->set_z(p.rot_z); rot->set_w(p.rot_w); - auto* vel = pp->mutable_velocity(); vel->set_x(p.vel_x); vel->set_y(p.vel_y); vel->set_z(p.vel_z); - pp->set_is_alive(p.is_alive); pp->set_score(p.score); pp->set_deaths(p.deaths); + pp->set_unique_id(p.unique_id); + pp->set_name(p.name); + pp->set_team(p.team); + pp->set_health(p.health); + pp->set_armor(p.armor); + auto* pos = pp->mutable_position(); + pos->set_x(p.pos_x); + pos->set_y(p.pos_y); + pos->set_z(p.pos_z); + auto* rot = pp->mutable_rotation(); + rot->set_x(p.rot_x); + rot->set_y(p.rot_y); + rot->set_z(p.rot_z); + rot->set_w(p.rot_w); + auto* vel = pp->mutable_velocity(); + vel->set_x(p.vel_x); + vel->set_y(p.vel_y); + vel->set_z(p.vel_z); + pp->set_is_alive(p.is_alive); + pp->set_score(p.score); + pp->set_deaths(p.deaths); } for (const auto& pr : s.projectiles) { auto* pp = fd->add_projectiles(); - pp->set_unique_id(pr.unique_id); pp->set_owner_id(pr.owner_id); - auto* pos = pp->mutable_position(); pos->set_x(pr.pos_x); pos->set_y(pr.pos_y); pos->set_z(pr.pos_z); - auto* vel = pp->mutable_velocity(); vel->set_x(pr.vel_x); vel->set_y(pr.vel_y); vel->set_z(pr.vel_z); - pp->set_damage(pr.damage); pp->set_type(pr.type); + pp->set_unique_id(pr.unique_id); + pp->set_owner_id(pr.owner_id); + auto* pos = pp->mutable_position(); + pos->set_x(pr.pos_x); + pos->set_y(pr.pos_y); + pos->set_z(pr.pos_z); + auto* vel = pp->mutable_velocity(); + vel->set_x(pr.vel_x); + vel->set_y(pr.vel_y); + vel->set_z(pr.vel_z); + pp->set_damage(pr.damage); + pp->set_type(pr.type); } auto* ms = fd->mutable_match_state(); - ms->set_score_team1(s.match.score_team1); ms->set_score_team2(s.match.score_team2); - ms->set_round(s.match.round); ms->set_phase(s.match.phase); ms->set_time_remaining(s.match.time_remaining); + ms->set_score_team1(s.match.score_team1); + ms->set_score_team2(s.match.score_team2); + ms->set_round(s.match.round); + ms->set_phase(s.match.phase); + ms->set_time_remaining(s.match.time_remaining); } std::ofstream ofs(path, std::ios::binary); @@ -318,43 +387,52 @@ static void ExportProtoSource(const std::vector& frames, const st } // ---- 4c: FlatBuffers binary ---- -static void ExportFbsSource(const std::vector& frames, const std::string& path) -{ +static void ExportFbsSource(const std::vector& frames, const std::string& path) { // Build using the Object API (ArenaReplayT, FrameDataT, PlayerT, ...). ::arena_fb::ArenaReplayT replay; - replay.replay_name = "Arena Sample Replay"; - replay.total_frames = TOTAL_FRAMES; - replay.fps = int(FPS); + replay.replay_name = "Arena Sample Replay"; + replay.total_frames = TOTAL_FRAMES; + replay.fps = int(FPS); replay.duration_seconds = double(TOTAL_FRAMES) / FPS; for (size_t i = 0; i < frames.size(); ++i) { const auto& s = frames[i]; auto fd = std::make_unique<::arena_fb::FrameDataT>(); fd->frame_index = int(i); - fd->game_time = s.game_time; - fd->utc_ticks = s.utc_ticks; + fd->game_time = s.game_time; + fd->utc_ticks = s.utc_ticks; for (const auto& p : s.players) { auto pp = std::make_unique<::arena_fb::PlayerT>(); - pp->unique_id = p.unique_id; pp->name = p.name; pp->team = p.team; - pp->health = p.health; pp->armor = p.armor; + pp->unique_id = p.unique_id; + pp->name = p.name; + pp->team = p.team; + pp->health = p.health; + pp->armor = p.armor; pp->position = std::make_unique<::arena_fb::Vec3>(p.pos_x, p.pos_y, p.pos_z); pp->rotation = std::make_unique<::arena_fb::Rotation>(p.rot_x, p.rot_y, p.rot_z, p.rot_w); pp->velocity = std::make_unique<::arena_fb::Vec3>(p.vel_x, p.vel_y, p.vel_z); - pp->is_alive = p.is_alive; pp->score = p.score; pp->deaths = p.deaths; + pp->is_alive = p.is_alive; + pp->score = p.score; + pp->deaths = p.deaths; fd->players.push_back(std::move(pp)); } for (const auto& pr : s.projectiles) { auto pp = std::make_unique<::arena_fb::ProjectileT>(); - pp->unique_id = pr.unique_id; pp->owner_id = pr.owner_id; + pp->unique_id = pr.unique_id; + pp->owner_id = pr.owner_id; pp->position = std::make_unique<::arena_fb::Vec3>(pr.pos_x, pr.pos_y, pr.pos_z); pp->velocity = std::make_unique<::arena_fb::Vec3>(pr.vel_x, pr.vel_y, pr.vel_z); - pp->damage = pr.damage; pp->type = pr.type; + pp->damage = pr.damage; + pp->type = pr.type; fd->projectiles.push_back(std::move(pp)); } auto ms = std::make_unique<::arena_fb::MatchStateT>(); - ms->score_team1 = s.match.score_team1; ms->score_team2 = s.match.score_team2; - ms->round = s.match.round; ms->phase = s.match.phase; ms->time_remaining = s.match.time_remaining; + ms->score_team1 = s.match.score_team1; + ms->score_team2 = s.match.score_team2; + ms->round = s.match.round; + ms->phase = s.match.phase; + ms->time_remaining = s.match.time_remaining; fd->match_state = std::move(ms); replay.frames.push_back(std::move(fd)); @@ -369,17 +447,16 @@ static void ExportFbsSource(const std::vector& frames, const std: // =================================================================== -int main() -{ - const std::string schema_path = "content/writer/arena/arena_schema.json"; - const std::string writer_dir = "content/writer/arena"; - const std::string reader_dir = "content/reader/arena"; +int main() { + const std::string schema_path = "content/writer/arena/arena_schema.json"; + const std::string writer_dir = "content/writer/arena"; + const std::string reader_dir = "content/reader/arena"; std::filesystem::create_directories(writer_dir); std::filesystem::create_directories(reader_dir); VTX_INFO("=== Arena Replay Generator ==="); - VTX_INFO("Simulating {} frames ({:.1f}s @ {:.0f} FPS)...", TOTAL_FRAMES, double(TOTAL_FRAMES)/FPS, FPS); + VTX_INFO("Simulating {} frames ({:.1f}s @ {:.0f} FPS)...", TOTAL_FRAMES, double(TOTAL_FRAMES) / FPS, FPS); // ---- Phase 1: Simulate ---- // Use a fixed historical timestamp so the data is reproducible across @@ -389,9 +466,9 @@ int main() VTX_INFO("Simulation complete: {} frames, {} players.", int(sim_frames.size()), NUM_PLAYERS); // ---- Phase 2: Export 3 data-source files ---- - const std::string json_src = writer_dir + "/arena_replay_data.json"; + const std::string json_src = writer_dir + "/arena_replay_data.json"; const std::string proto_src = writer_dir + "/arena_replay_data.proto.bin"; - const std::string fbs_src = writer_dir + "/arena_replay_data.fbs.bin"; + const std::string fbs_src = writer_dir + "/arena_replay_data.fbs.bin"; ExportJsonSource(sim_frames, json_src); VTX_INFO("Exported data source: {}", json_src); diff --git a/sdk/include/vtx/common/adapters/flatbuffer/flatbuffer_adapter.h b/sdk/include/vtx/common/adapters/flatbuffer/flatbuffer_adapter.h index 3b41975..1c23a92 100644 --- a/sdk/include/vtx/common/adapters/flatbuffer/flatbuffer_adapter.h +++ b/sdk/include/vtx/common/adapters/flatbuffer/flatbuffer_adapter.h @@ -36,8 +36,8 @@ namespace VTX { */ template struct FastField { - CppMemberType CppType::* cpp_member; // &BBChampion::Kills - FbReturnType(FbTable::* fb_accessor)() const; // &FB::Champion::kills() + CppMemberType CppType::*cpp_member; // &BBChampion::Kills + FbReturnType (FbTable::*fb_accessor)() const; // &FB::Champion::kills() }; /** @@ -47,8 +47,8 @@ namespace VTX { * @return FastField instance. */ template - constexpr auto MapFB(CppMemberType CppType::* cpp_ptr, FbReturnType(FbTable::* fb_ptr)() const) { - return FastField{cpp_ptr, fb_ptr}; + constexpr auto MapFB(CppMemberType CppType::*cpp_ptr, FbReturnType (FbTable::*fb_ptr)() const) { + return FastField {cpp_ptr, fb_ptr}; } /** @@ -61,23 +61,21 @@ namespace VTX { public: template static T Load(const typename FlatBufferMapping::FbType* fb_table) { - T instance{}; + T instance {}; - if (!fb_table) return instance; + if (!fb_table) + return instance; // Retrieve the compile-time mapping tuple constexpr auto mapping = FlatBufferMapping::GetFields(); // Iterate over all mapped fields and process them - std::apply([&](auto&&... fields) { - (ProcessField(instance, fb_table, fields), ...); - }, mapping); + std::apply([&](auto&&... fields) { (ProcessField(instance, fb_table, fields), ...); }, mapping); return instance; } private: - /** * @brief Processes a single field mapping. * @details Calls the FB accessor, converts the type if necessary, and assigns to the C++ member. @@ -99,29 +97,26 @@ namespace VTX { /** @brief Conversion for arithmetic types (int, float, etc.). */ template - static std::enable_if_t, Target> - Convert(Source fb_val) { + static std::enable_if_t, Target> Convert(Source fb_val) { return static_cast(fb_val); } /** @brief Conversion for Enum types. */ template - static std::enable_if_t, Target> - Convert(Source fb_val) { + static std::enable_if_t, Target> Convert(Source fb_val) { return static_cast(fb_val); } /** @brief Conversion for Strings (FlatBuffer String* -> std::string). */ template static std::enable_if_t, std::string> - Convert(const flatbuffers::String* fb_str) { + Convert(const flatbuffers::String* fb_str) { return fb_str ? fb_str->str() : ""; } /** @brief Recursive conversion for nested structures (Sub-tables). */ template - static std::enable_if_t, Target> - Convert(const FbSubTable* fb_sub_table) { + static std::enable_if_t, Target> Convert(const FbSubTable* fb_sub_table) { Target result; if (fb_sub_table) { result = Load(fb_sub_table); @@ -135,9 +130,10 @@ namespace VTX { */ template static std::enable_if_t, Target> - Convert(const flatbuffers::Vector>* fb_vector) { + Convert(const flatbuffers::Vector>* fb_vector) { Target result; - if (!fb_vector) return result; + if (!fb_vector) + return result; using ElementType = typename Target::value_type; @@ -148,8 +144,7 @@ namespace VTX { if constexpr (has_fb_mapping_v) { result.push_back(Load(fb_element)); - } - else { + } else { result.push_back(static_cast(*fb_element)); } } @@ -161,10 +156,10 @@ namespace VTX { * @brief Conversion for Vectors of Pointers (Structs in FB are often pointers). */ template - static std::enable_if_t, Target> - Convert(const flatbuffers::Vector* fb_vec) { + static std::enable_if_t, Target> Convert(const flatbuffers::Vector* fb_vec) { Target result; - if (!fb_vec) return result; + if (!fb_vec) + return result; result.reserve(fb_vec->size()); for (const U* element : *fb_vec) { @@ -174,4 +169,4 @@ namespace VTX { return result; } }; -} +} // namespace VTX diff --git a/sdk/include/vtx/common/adapters/json/json_adapter.h b/sdk/include/vtx/common/adapters/json/json_adapter.h index 908a4b4..c0a1059 100644 --- a/sdk/include/vtx/common/adapters/json/json_adapter.h +++ b/sdk/include/vtx/common/adapters/json/json_adapter.h @@ -14,7 +14,7 @@ #pragma once #include -namespace VTX{ +namespace VTX { /** * @class JsonAdapter @@ -24,100 +24,99 @@ namespace VTX{ */ class JsonAdapter { const nlohmann::json* node = nullptr; - public: - JsonAdapter() {} - /** + public: + JsonAdapter() {} + /** * @brief Explicit constructor. * @param j Constant reference to the JSON node to wrap. */ - JsonAdapter(const nlohmann::json& j) : node(&j) {} + JsonAdapter(const nlohmann::json& j) + : node(&j) {} - /** + /** * @brief Checks if a specific key exists in the current object. * @param key The key string to search for. * @return true If the node is an object and contains the key. * @return false If the node is not an object or the key is missing. */ - bool HasKey(const std::string& key) const { - if (!node->is_object()) { - return false; - } - return node->contains(key); + bool HasKey(const std::string& key) const { + if (!node->is_object()) { + return false; } + return node->contains(key); + } - /** + /** * @brief Extracts a value of the specified type. * @tparam T The target type (int, float, std::string, bool, etc.). * @return T The converted value from the JSON node. * @throws nlohmann::json::type_error If the conversion is invalid. */ - template - T GetValue() const { - return node->get(); - } + template + T GetValue() const { + return node->get(); + } - /** + /** * @brief Retrieves a child adapter for a specific key. * @param key The key of the child node. * @return JsonAdapter A new adapter instance pointing to the child node. * @throws nlohmann::json::out_of_range If the key does not exist. */ - JsonAdapter GetChild(const std::string& key) const { - if (!IsMap()) return JsonAdapter(); - if (!node->contains(key)) return JsonAdapter(); - return JsonAdapter(node->at(key)); - } + JsonAdapter GetChild(const std::string& key) const { + if (!IsMap()) + return JsonAdapter(); + if (!node->contains(key)) + return JsonAdapter(); + return JsonAdapter(node->at(key)); + } - /** + /** * @brief Checks if the current node represents an array. * @return true If it is an array, false otherwise. */ - bool IsArray() const { - return node->is_array(); - } + bool IsArray() const { return node->is_array(); } - /** + /** * @brief Gets the number of elements in the array. * @return size_t Number of elements. */ - size_t Size() const { - return node->size(); - } + size_t Size() const { return node->size(); } - /** + /** * @brief Retrieves an adapter for a specific element in an array. * @param index The zero-based index of the element. * @return JsonAdapter Adapter for the element at the given index. */ - JsonAdapter GetElement(size_t index) const { - if (!node || !node->is_array()) { - throw std::runtime_error("JsonAdapter: GetElement attempted to access a none array object"); - } - // Usamos .at() que es más seguro (lanza excepción si el índice no existe) - return JsonAdapter(node->at(index)); + JsonAdapter GetElement(size_t index) const { + if (!node || !node->is_array()) { + throw std::runtime_error("JsonAdapter: GetElement attempted to access a none array object"); } + // Usamos .at() que es más seguro (lanza excepción si el índice no existe) + return JsonAdapter(node->at(index)); + } - /** + /** * @brief Checks if the current node represents a map/object. * @return true If it is an object, false otherwise. */ - bool IsMap() const { return node->is_object(); } + bool IsMap() const { return node->is_object(); } - /** + /** * @brief Retrieves all keys from the current JSON object. * @return std::vector A list of all keys. */ - std::vector GetKeys() const { - std::vector keys; - for (auto& [key, val] : node->items()) { - keys.push_back(key); - } - return keys; + std::vector GetKeys() const { + std::vector keys; + for (auto& [key, val] : node->items()) { + keys.push_back(key); } + return keys; + } - /** + /** * @brief Converts a string key from the JSON map to a specific target type. * @details Useful for deserializing `std::map` or `std::map` where * JSON only supports string keys. @@ -125,22 +124,19 @@ namespace VTX{ * @param key_str The key as a string. * @return KeyType The parsed key value. */ - template - KeyType ParseKey(const std::string& key_str) const { - if constexpr (std::is_same_v) { - return key_str; - } - else if constexpr (std::is_integral_v) { - return static_cast(std::stoll(key_str)); - } - else if constexpr (std::is_enum_v) { - // Attempt to deserialize the enum directly from the string using nlohmann - return nlohmann::json(key_str).get(); - } - else { - static_assert(std::is_void_v, "Key type_max_indices not supported by JsonReader."); - return KeyType{}; - } + template + KeyType ParseKey(const std::string& key_str) const { + if constexpr (std::is_same_v) { + return key_str; + } else if constexpr (std::is_integral_v) { + return static_cast(std::stoll(key_str)); + } else if constexpr (std::is_enum_v) { + // Attempt to deserialize the enum directly from the string using nlohmann + return nlohmann::json(key_str).get(); + } else { + static_assert(std::is_void_v, "Key type_max_indices not supported by JsonReader."); + return KeyType {}; } + } }; -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/common/adapters/json/json_policy.h b/sdk/include/vtx/common/adapters/json/json_policy.h index 7c22a38..8120e6b 100644 --- a/sdk/include/vtx/common/adapters/json/json_policy.h +++ b/sdk/include/vtx/common/adapters/json/json_policy.h @@ -22,10 +22,10 @@ namespace VTX { template struct Field { const char* json_key; - Type Class::* member_ptr; + Type Class::*member_ptr; }; - + /** * @brief Helper function to create a Field object with automatic type deduction. * @details Simplifies the syntax for defining mappings. @@ -37,7 +37,7 @@ namespace VTX { * @return Field A initialized Field structure. */ template - constexpr auto MakeField(const char* name, Type Class::* ptr) { - return Field{name, ptr}; + constexpr auto MakeField(const char* name, Type Class::*ptr) { + return Field {name, ptr}; } -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/common/adapters/protobuff/protobuff_adapter.h b/sdk/include/vtx/common/adapters/protobuff/protobuff_adapter.h index 461172c..4c42696 100644 --- a/sdk/include/vtx/common/adapters/protobuff/protobuff_adapter.h +++ b/sdk/include/vtx/common/adapters/protobuff/protobuff_adapter.h @@ -23,15 +23,14 @@ namespace VTX { const google::protobuf::FieldDescriptor* field_desc_; // Null if root or full obj public: - explicit ProtobufAdapter(const google::protobuf::Message& msg) - : message_(msg), field_desc_(nullptr) { - } + : message_(msg) + , field_desc_(nullptr) {} //submessage constructor, used internally ProtobufAdapter(const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* fd) - : message_(msg), field_desc_(fd) { - } + : message_(msg) + , field_desc_(fd) {} bool HasKey(const std::string& key) const { const auto* descriptor = message_.GetDescriptor(); @@ -40,8 +39,7 @@ namespace VTX { template T GetValue() const { - - return T{}; + return T {}; } @@ -49,17 +47,15 @@ namespace VTX { const auto* descriptor = message_.GetDescriptor(); const auto* field = descriptor->FindFieldByName(key); - if (!field) throw std::runtime_error("Field not found: " + key); + if (!field) + throw std::runtime_error("Field not found: " + key); const auto* reflection = message_.GetReflection(); if (field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { if (field->is_repeated()) { - return ProtobufAdapter(message_, field); - } - else - { + } else { const auto& sub_message = reflection->GetMessage(message_, field); return ProtobufAdapter(sub_message); } @@ -71,29 +67,25 @@ namespace VTX { template T GetValueFromField() const { - if (!field_desc) return T{}; // Error + if (!field_desc) + return T {}; // Error const auto* ref = message.GetReflection(); if constexpr (std::is_same_v || std::is_same_v) { return ref->GetInt32(message_, field_desc); - } - else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { return ref->GetInt64(message_, field_desc); - } - else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { return ref->GetBool(message_, field_desc); - } - else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { return ref->GetFloat(message_, field_desc); - } - else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { return ref->GetDouble(message_, field_desc); - } - else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { return ref->GetString(message_, field_desc); } - return T{}; + return T {}; } template @@ -101,9 +93,7 @@ namespace VTX { return GetValueFromField(); } - bool IsArray() const { - return field_desc_ && field_desc_->is_repeated(); - } + bool IsArray() const { return field_desc_ && field_desc_->is_repeated(); } size_t Size() const { if (IsArray()) { @@ -113,7 +103,8 @@ namespace VTX { } ProtobufAdapter GetElement(size_t index) const { - if (!IsArray()) throw std::runtime_error("Not an array"); + if (!IsArray()) + throw std::runtime_error("Not an array"); const auto* ref = message_.GetReflection(); diff --git a/sdk/include/vtx/common/readers/frame_reader/flatbuffer_loader.h b/sdk/include/vtx/common/readers/frame_reader/flatbuffer_loader.h index 31e585e..d74187c 100644 --- a/sdk/include/vtx/common/readers/frame_reader/flatbuffer_loader.h +++ b/sdk/include/vtx/common/readers/frame_reader/flatbuffer_loader.h @@ -12,23 +12,25 @@ namespace VTX { - template + template struct FlatBufferBinding { - static_assert(sizeof(T) == 0, "ERROR: Missing Bindings for this FlatBuffer type."); + static_assert(sizeof(T) == 0, "ERROR: Missing Bindings for this FlatBuffer type."); }; class GenericFlatBufferLoader { private: - const PropertyAddressCache* cache_; + const PropertyAddressCache* cache_; bool debug_mode_; public: - explicit GenericFlatBufferLoader(const PropertyAddressCache& cache, bool debug = false) - : cache_(&cache), debug_mode_(debug) {} - + explicit GenericFlatBufferLoader(const PropertyAddressCache& cache, bool debug = false) + : cache_(&cache) + , debug_mode_(debug) {} + template void Load(const FBType* src, PropertyContainer& dest, const std::string& struct_name) { - if (!src) return; + if (!src) + return; if (dest.entity_type_id == -1) { auto it = cache_->name_to_id.find(struct_name); @@ -43,13 +45,16 @@ namespace VTX { template void LoadFrame(const FrameFBType* src, VTX::Frame& dest, const std::string& schemaName) { - if (!src) return; + if (!src) + return; FlatBufferBinding::TransferToFrame(src, dest, *this, schemaName); } template - void AppendActorList(VTX::Bucket& targetBlock, const std::string& schemaType, const FBVectorType* src_vector, IdExtractorFunc idExtractor) { - if (!src_vector || src_vector->size() == 0) return; + void AppendActorList(VTX::Bucket& targetBlock, const std::string& schemaType, const FBVectorType* src_vector, + IdExtractorFunc idExtractor) { + if (!src_vector || src_vector->size() == 0) + return; targetBlock.entities.reserve(targetBlock.entities.size() + src_vector->size()); for (auto it = src_vector->begin(); it != src_vector->end(); ++it) { @@ -58,48 +63,79 @@ namespace VTX { } template - void AppendSingleEntity(VTX::Bucket& targetBlock, const std::string& schemaType, const FBType* src_item, IdExtractorFunc idExtractor) { - if (!src_item) return; + void AppendSingleEntity(VTX::Bucket& targetBlock, const std::string& schemaType, const FBType* src_item, + IdExtractorFunc idExtractor) { + if (!src_item) + return; ExtractActorWithIdFunc(src_item, targetBlock, schemaType, idExtractor); } - + template - void LoadField(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, const T& value) { - + void LoadField(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, + const T& value) { auto struct_it = cache_->structs.find(dest.entity_type_id); - if (struct_it == cache_->structs.end()) return; + if (struct_it == cache_->structs.end()) + return; auto prop_it = struct_it->second.properties.find(field_name); - if (prop_it == struct_it->second.properties.end()) return; + if (prop_it == struct_it->second.properties.end()) + return; const PropertyAddress& addr = prop_it->second; switch (addr.type_id) { - case FieldType::String: StoreValue(dest.string_properties, addr.index, value); break; - case FieldType::Int8: - case FieldType::Int32: - case FieldType::Enum: StoreValue(dest.int32_properties, addr.index, value); break; - case FieldType::Int64: StoreValue(dest.int64_properties, addr.index, value); break; - case FieldType::Float: StoreValue(dest.float_properties, addr.index, value); break; - case FieldType::Double: StoreValue(dest.double_properties, addr.index, value); break; - case FieldType::Bool: StoreValue(dest.bool_properties, addr.index, value); break; - case FieldType::Vector: StoreValue(dest.vector_properties, addr.index, value); break; - case FieldType::Quat: StoreValue(dest.quat_properties, addr.index, value); break; - case FieldType::Transform: StoreValue(dest.transform_properties, addr.index, value); break; - case FieldType::FloatRange: StoreValue(dest.range_properties, addr.index, value); break; - case FieldType::Struct: StoreValue(dest.any_struct_properties, addr.index, value); break; - default: break; + case FieldType::String: + StoreValue(dest.string_properties, addr.index, value); + break; + case FieldType::Int8: + case FieldType::Int32: + case FieldType::Enum: + StoreValue(dest.int32_properties, addr.index, value); + break; + case FieldType::Int64: + StoreValue(dest.int64_properties, addr.index, value); + break; + case FieldType::Float: + StoreValue(dest.float_properties, addr.index, value); + break; + case FieldType::Double: + StoreValue(dest.double_properties, addr.index, value); + break; + case FieldType::Bool: + StoreValue(dest.bool_properties, addr.index, value); + break; + case FieldType::Vector: + StoreValue(dest.vector_properties, addr.index, value); + break; + case FieldType::Quat: + StoreValue(dest.quat_properties, addr.index, value); + break; + case FieldType::Transform: + StoreValue(dest.transform_properties, addr.index, value); + break; + case FieldType::FloatRange: + StoreValue(dest.range_properties, addr.index, value); + break; + case FieldType::Struct: + StoreValue(dest.any_struct_properties, addr.index, value); + break; + default: + break; } } - void LoadBlob(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, const void* data, size_t byte_size) { - if (!data || byte_size == 0) return; + void LoadBlob(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, + const void* data, size_t byte_size) { + if (!data || byte_size == 0) + return; auto struct_it = cache_->structs.find(dest.entity_type_id); - if (struct_it == cache_->structs.end()) return; + if (struct_it == cache_->structs.end()) + return; auto prop_it = struct_it->second.properties.find(field_name); - if (prop_it == struct_it->second.properties.end()) return; + if (prop_it == struct_it->second.properties.end()) + return; int32_t idx = prop_it->second.index; const uint8_t* byte_data = static_cast(data); @@ -110,14 +146,18 @@ namespace VTX { } template - void LoadStruct(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, const NestedFBType* src_nested) { - if (!src_nested) return; + void LoadStruct(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, + const NestedFBType* src_nested) { + if (!src_nested) + return; auto struct_it = cache_->structs.find(dest.entity_type_id); - if (struct_it == cache_->structs.end()) return; + if (struct_it == cache_->structs.end()) + return; auto prop_it = struct_it->second.properties.find(field_name); - if (prop_it == struct_it->second.properties.end()) return; + if (prop_it == struct_it->second.properties.end()) + return; int32_t index = prop_it->second.index; const std::string& child_schema = prop_it->second.child_type_name; @@ -127,40 +167,45 @@ namespace VTX { } template - void LoadArray(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, const FBVectorType* src_array) { - if (!src_array || src_array->size() == 0) return; + void LoadArray(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, + const FBVectorType* src_array) { + if (!src_array || src_array->size() == 0) + return; auto struct_it = cache_->structs.find(dest.entity_type_id); - if (struct_it == cache_->structs.end()) return; + if (struct_it == cache_->structs.end()) + return; auto prop_it = struct_it->second.properties.find(field_name); - if (prop_it == struct_it->second.properties.end()) return; + if (prop_it == struct_it->second.properties.end()) + return; const int32_t idx = prop_it->second.index; const FieldType type_id = prop_it->second.type_id; - - const std::string& child_schema = prop_it->second.child_type_name; + + const std::string& child_schema = prop_it->second.child_type_name; const VTX::FieldContainerType container = prop_it->second.container_type; - + using IteratorT = typename FBVectorType::const_iterator; using ElementT = typename std::iterator_traits::value_type; - + if constexpr (std::is_pointer_v) { for (auto it = src_array->begin(); it != src_array->end(); ++it) { - const auto* item = *it; - if (!item) continue; + const auto* item = *it; + if (!item) + continue; if (type_id == FieldType::Struct) { PropertyContainer nested_container; - Load(item, nested_container, child_schema); - + Load(item, nested_container, child_schema); + if (nested_container.entity_type_id != -1) { if (container == VTX::FieldContainerType::Map) { - EnsureSize(dest.map_properties, idx); - + std::string map_key; - if (!nested_container.string_properties.empty() && !nested_container.string_properties[0].empty()) { + if (!nested_container.string_properties.empty() && + !nested_container.string_properties[0].empty()) { map_key = nested_container.string_properties[0]; } else if (!nested_container.int32_properties.empty()) { map_key = std::to_string(nested_container.int32_properties[0]); @@ -170,14 +215,14 @@ namespace VTX { dest.map_properties[idx].keys.push_back(map_key); dest.map_properties[idx].values.push_back(nested_container); - + } else { dest.any_struct_arrays.PushBack(idx, nested_container); } } } else { PropertyContainer temp; - Load(item, temp, child_schema); + Load(item, temp, child_schema); PushToFlatArray(dest, type_id, idx, temp); } } @@ -185,51 +230,78 @@ namespace VTX { FillFlatArray(dest, type_id, idx, src_array); } } - + private: template void FillFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, const FBVectorT* src) { - if (!src) return; + if (!src) + return; for (auto it = src->begin(); it != src->end(); ++it) { - const auto& val = *it; + const auto& val = *it; switch (type) { - case FieldType::Int8: - case FieldType::Int32: - case FieldType::Enum: dest.int32_arrays.PushBack(idx, static_cast(val)); break; - case FieldType::Int64: dest.int64_arrays.PushBack(idx, static_cast(val)); break; - case FieldType::Float: dest.float_arrays.PushBack(idx, static_cast(val)); break; - case FieldType::Double: dest.double_arrays.PushBack(idx, static_cast(val)); break; - case FieldType::Bool: dest.bool_arrays.PushBack(idx, static_cast(val)); break; - case FieldType::String: - if constexpr (std::is_assignable_v) { - dest.string_arrays.PushBack(idx, val); - } else { - dest.string_arrays.PushBack(idx, std::to_string(val)); - } - break; - default: break; + case FieldType::Int8: + case FieldType::Int32: + case FieldType::Enum: + dest.int32_arrays.PushBack(idx, static_cast(val)); + break; + case FieldType::Int64: + dest.int64_arrays.PushBack(idx, static_cast(val)); + break; + case FieldType::Float: + dest.float_arrays.PushBack(idx, static_cast(val)); + break; + case FieldType::Double: + dest.double_arrays.PushBack(idx, static_cast(val)); + break; + case FieldType::Bool: + dest.bool_arrays.PushBack(idx, static_cast(val)); + break; + case FieldType::String: + if constexpr (std::is_assignable_v) { + dest.string_arrays.PushBack(idx, val); + } else { + dest.string_arrays.PushBack(idx, std::to_string(val)); + } + break; + default: + break; } } } - - void PushToFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, const PropertyContainer& temp) const { + + void PushToFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, + const PropertyContainer& temp) const { switch (type) { - case FieldType::Vector: if (!temp.vector_properties.empty()) dest.vector_arrays.PushBack(idx, temp.vector_properties[0]); break; - case FieldType::Quat: if (!temp.quat_properties.empty()) dest.quat_arrays.PushBack(idx, temp.quat_properties[0]); break; - case FieldType::Transform: if (!temp.transform_properties.empty()) dest.transform_arrays.PushBack(idx, temp.transform_properties[0]); break; - case FieldType::FloatRange: if (!temp.range_properties.empty()) dest.range_arrays.PushBack(idx, temp.range_properties[0]); break; - default: break; + case FieldType::Vector: + if (!temp.vector_properties.empty()) + dest.vector_arrays.PushBack(idx, temp.vector_properties[0]); + break; + case FieldType::Quat: + if (!temp.quat_properties.empty()) + dest.quat_arrays.PushBack(idx, temp.quat_properties[0]); + break; + case FieldType::Transform: + if (!temp.transform_properties.empty()) + dest.transform_arrays.PushBack(idx, temp.transform_properties[0]); + break; + case FieldType::FloatRange: + if (!temp.range_properties.empty()) + dest.range_arrays.PushBack(idx, temp.range_properties[0]); + break; + default: + break; } } template - void ExtractActorWithIdFunc(const ActorPtrT src, VTX::Bucket& block, const std::string& schemaType, IdExtractorFunc idExtractor) { + void ExtractActorWithIdFunc(const ActorPtrT src, VTX::Bucket& block, const std::string& schemaType, + IdExtractorFunc idExtractor) { PropertyContainer& entity = block.entities.emplace_back(); Load(src, entity, schemaType); std::string uid = idExtractor(src); block.unique_ids.push_back(uid); } - + template inline void EnsureSize(Vec& v, size_t index) { if (v.size() <= index) { @@ -240,15 +312,14 @@ namespace VTX { template inline void StoreValue(Vec& vector, size_t index, const V& val) { EnsureSize(vector, index); - + if constexpr (std::is_same_v) { if constexpr (std::is_convertible_v) { vector[index] = val; } else if constexpr (std::is_arithmetic_v) { vector[index] = std::to_string(val); } - } - else if constexpr (std::is_assignable_v) { + } else if constexpr (std::is_assignable_v) { vector[index] = static_cast(val); } } diff --git a/sdk/include/vtx/common/readers/frame_reader/protobuff_loader.h b/sdk/include/vtx/common/readers/frame_reader/protobuff_loader.h index d4b1b4a..f7c0d23 100644 --- a/sdk/include/vtx/common/readers/frame_reader/protobuff_loader.h +++ b/sdk/include/vtx/common/readers/frame_reader/protobuff_loader.h @@ -13,7 +13,8 @@ namespace VTX { // Forward declaration for the binding logic used by the loader - template struct ProtoBinding; + template + struct ProtoBinding; /** * @class GenericProtobufLoader @@ -32,9 +33,10 @@ namespace VTX { * @param schema The registry containing field and type metadata. * @param debug Enable or disable debug logging. */ - explicit GenericProtobufLoader(const SchemaRegistry& schema, bool debug = false) - : schema_(&schema), debug_mode_(debug) {} - + explicit GenericProtobufLoader(const SchemaRegistry& schema, bool debug = false) + : schema_(&schema) + , debug_mode_(debug) {} + /** * @brief Loads data from a Protobuf message into a native PropertyContainer. * @tparam ProtoType The Protobuf message type generated by protoc. @@ -49,7 +51,7 @@ namespace VTX { } dest.entity_type_id = schema_->GetStructTypeId(struct_name); ProtoBinding::Transfer(src, dest, *this, struct_name); - dest.content_hash = Helpers::CalculateContainerHash(dest); + dest.content_hash = Helpers::CalculateContainerHash(dest); } /** @@ -74,8 +76,10 @@ namespace VTX { * @param idExtractor Instance of the ID extractor function. */ template - void AppendActorList(VTX::Bucket& targetBlock, const std::string& schemaType, const RepeatedProtoType& src_array, IdExtractorFunc idExtractor) { - if (src_array.empty()) return; + void AppendActorList(VTX::Bucket& targetBlock, const std::string& schemaType, + const RepeatedProtoType& src_array, IdExtractorFunc idExtractor) { + if (src_array.empty()) + return; targetBlock.entities.reserve(targetBlock.entities.size() + src_array.size()); @@ -94,10 +98,11 @@ namespace VTX { * @param idExtractor The ID extractor function. */ template - void AppendSingleActor(VTX::Bucket& targetBlock, const std::string& schemaType, const ProtoType& src_item, IdExtractorFunc idExtractor) { + void AppendSingleActor(VTX::Bucket& targetBlock, const std::string& schemaType, const ProtoType& src_item, + IdExtractorFunc idExtractor) { ExtractActorWithIdFunc(src_item, targetBlock, schemaType, idExtractor); } - + /** * @brief Sets a single field value in a PropertyContainer based on schema metadata. * @tparam T The native C++ type of the value. @@ -108,11 +113,13 @@ namespace VTX { * @note Performs type conversion and bounds checking via EnsureSize. */ template - void LoadField(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, const T& value) { + void LoadField(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, + const T& value) { const auto* field_info = schema_->GetField(struct_name, field_name); - + if (!field_info) { - if (debug_mode_) std::cout << " [WARNING] Field not found: " << struct_name << "::" << field_name << "\n"; + if (debug_mode_) + std::cout << " [WARNING] Field not found: " << struct_name << "::" << field_name << "\n"; return; } @@ -120,27 +127,50 @@ namespace VTX { VTX::FieldType target_type = field_info->type_id; if (debug_mode_) { - std::cout << " [SET] " << struct_name << "::" << field_name + std::cout << " [SET] " << struct_name << "::" << field_name << " (EnumID: " << static_cast(target_type) << ")" << " -> Index: " << idx << "\n"; } switch (target_type) { - case FieldType::String: StoreValue(dest.string_properties, idx, value); break; - case FieldType::Int8: - case FieldType::Int32: - case FieldType::Enum: StoreValue(dest.int32_properties, idx, value); break; - case FieldType::Int64: StoreValue(dest.int64_properties, idx, value); break; - case FieldType::Float: StoreValue(dest.float_properties, idx, value); break; - case FieldType::Double: StoreValue(dest.double_properties, idx, value); break; - case FieldType::Bool: StoreValue(dest.bool_properties, idx, value); break; - case FieldType::Vector: StoreValue(dest.vector_properties, idx, value); break; - case FieldType::Quat: StoreValue(dest.quat_properties, idx, value); break; - case FieldType::Transform: StoreValue(dest.transform_properties, idx, value); break; - case FieldType::FloatRange: StoreValue(dest.range_properties, idx, value); break; - case FieldType::Struct: StoreValue(dest.any_struct_properties, idx, value); break; - case FieldType::None: - default: break; + case FieldType::String: + StoreValue(dest.string_properties, idx, value); + break; + case FieldType::Int8: + case FieldType::Int32: + case FieldType::Enum: + StoreValue(dest.int32_properties, idx, value); + break; + case FieldType::Int64: + StoreValue(dest.int64_properties, idx, value); + break; + case FieldType::Float: + StoreValue(dest.float_properties, idx, value); + break; + case FieldType::Double: + StoreValue(dest.double_properties, idx, value); + break; + case FieldType::Bool: + StoreValue(dest.bool_properties, idx, value); + break; + case FieldType::Vector: + StoreValue(dest.vector_properties, idx, value); + break; + case FieldType::Quat: + StoreValue(dest.quat_properties, idx, value); + break; + case FieldType::Transform: + StoreValue(dest.transform_properties, idx, value); + break; + case FieldType::FloatRange: + StoreValue(dest.range_properties, idx, value); + break; + case FieldType::Struct: + StoreValue(dest.any_struct_properties, idx, value); + break; + case FieldType::None: + default: + break; } } @@ -153,23 +183,26 @@ namespace VTX { * @param src_nested The source nested Protobuf message. */ template - void LoadStruct(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, const NestedProtoType& src_nested) { + void LoadStruct(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, + const NestedProtoType& src_nested) { const auto* field_info = schema_->GetField(struct_name, field_name); - if (!field_info) return; + if (!field_info) + return; int32_t index = field_info->index; - std::string child_type_name = field_info->struct_type; + std::string child_type_name = field_info->struct_type; if (debug_mode_) { - std::cout << " [STRUCT] " << struct_name << "::" << field_name - << " -> Index: " << index << " (SchemaType: " << child_type_name << ")" << "\n"; + std::cout << " [STRUCT] " << struct_name << "::" << field_name << " -> Index: " << index + << " (SchemaType: " << child_type_name << ")" + << "\n"; } EnsureSize(dest.any_struct_properties, index); Load(src_nested, dest.any_struct_properties[index], child_type_name); } - /** + /** * @brief Fills a flattened SoA (Structure of Arrays) from a repeated Protobuf primitive field. * @details Iterates through the source collection and uses the PushBack method of FlatArray * to maintain the data and offset integrity within the PropertyContainer. @@ -183,119 +216,124 @@ namespace VTX { void FillFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, const RepeatedT& src) { for (const auto& val : src) { // Compile-time check: is the value a string-like type? - constexpr bool is_string_type = std::is_convertible_v || + constexpr bool is_string_type = std::is_convertible_v || std::is_same_v, std::string>; switch (type) { - case FieldType::Int8: - case FieldType::Int32: - case FieldType::Enum: - if constexpr (!is_string_type) { - dest.int32_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) std::cerr << "[LOADER] Error: Trying to cast a string to Int32 array.\n"; - } - break; - - case FieldType::Int64: - if constexpr (!is_string_type) { - dest.int64_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) std::cerr << "[LOADER] Error: Trying to cast a string to Int64 array.\n"; - } - break; - - case FieldType::Float: - if constexpr (!is_string_type) { - dest.float_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) std::cerr << "[LOADER] Error: Trying to cast a string to Float array.\n"; - } - break; - - case FieldType::Double: - if constexpr (!is_string_type) { - dest.double_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) std::cerr << "[LOADER] Error: Trying to cast a string to Double array.\n"; - } - break; - - case FieldType::Bool: - if constexpr (!is_string_type) { - dest.bool_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) std::cerr << "[LOADER] Error: Trying to cast a string to Bool array.\n"; - } - break; - - case FieldType::String: - if constexpr (is_string_type) { - dest.string_arrays.PushBack(idx, std::string(val)); - } else if constexpr (std::is_arithmetic_v>) { - dest.string_arrays.PushBack(idx, std::to_string(val)); - } - break; - - default: - if (debug_mode_) { - std::cerr << "[LOADER] FillFlatArray: FieldType " << static_cast(type) - << " is not a supported primitive for flat arrays.\n"; - } - break; - } - } - } - - /** - * @brief Helper to push a complex math object that was temporarily loaded into its SoA FlatArray. - * @details This is used when an array contains complex types (Vector, Quat, etc.) that the - * loader first processes into a single PropertyContainer before flattening them into the - * main SoA structure. - * * @param dest The target PropertyContainer holding the SoA arrays. - * @param type The VTX FieldType identifying which array to target. - * @param idx The specific sub-array index within the FlatArray. - * @param temp The temporary container where the single object was initially loaded. - */ - void PushToFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, const PropertyContainer& temp) const - { - switch (type) { - case FieldType::Vector: - if (!temp.vector_properties.empty()) { - dest.vector_arrays.PushBack(idx, temp.vector_properties[0]); + case FieldType::Int8: + case FieldType::Int32: + case FieldType::Enum: + if constexpr (!is_string_type) { + dest.int32_arrays.PushBack(idx, static_cast(val)); + } else { + if (debug_mode_) + std::cerr << "[LOADER] Error: Trying to cast a string to Int32 array.\n"; } break; - case FieldType::Quat: - if (!temp.quat_properties.empty()) { - dest.quat_arrays.PushBack(idx, temp.quat_properties[0]); + case FieldType::Int64: + if constexpr (!is_string_type) { + dest.int64_arrays.PushBack(idx, static_cast(val)); + } else { + if (debug_mode_) + std::cerr << "[LOADER] Error: Trying to cast a string to Int64 array.\n"; + } + break; + + case FieldType::Float: + if constexpr (!is_string_type) { + dest.float_arrays.PushBack(idx, static_cast(val)); + } else { + if (debug_mode_) + std::cerr << "[LOADER] Error: Trying to cast a string to Float array.\n"; } break; - case FieldType::Transform: - if (!temp.transform_properties.empty()) { - dest.transform_arrays.PushBack(idx, temp.transform_properties[0]); + case FieldType::Double: + if constexpr (!is_string_type) { + dest.double_arrays.PushBack(idx, static_cast(val)); + } else { + if (debug_mode_) + std::cerr << "[LOADER] Error: Trying to cast a string to Double array.\n"; } break; - case FieldType::FloatRange: - if (!temp.range_properties.empty()) { - dest.range_arrays.PushBack(idx, temp.range_properties[0]); + case FieldType::Bool: + if constexpr (!is_string_type) { + dest.bool_arrays.PushBack(idx, static_cast(val)); + } else { + if (debug_mode_) + std::cerr << "[LOADER] Error: Trying to cast a string to Bool array.\n"; } break; - case FieldType::Struct: - if (temp.entity_type_id != -1) { - dest.any_struct_arrays.PushBack(idx, temp); + case FieldType::String: + if constexpr (is_string_type) { + dest.string_arrays.PushBack(idx, std::string(val)); + } else if constexpr (std::is_arithmetic_v>) { + dest.string_arrays.PushBack(idx, std::to_string(val)); } break; default: if (debug_mode_) { - std::cerr << "[LOADER] PushToFlatArray: Type " << static_cast(type) - << " is not a complex math type_max_indices supported by SoA flattening.\n"; + std::cerr << "[LOADER] FillFlatArray: FieldType " << static_cast(type) + << " is not a supported primitive for flat arrays.\n"; } break; + } + } + } + + /** + * @brief Helper to push a complex math object that was temporarily loaded into its SoA FlatArray. + * @details This is used when an array contains complex types (Vector, Quat, etc.) that the + * loader first processes into a single PropertyContainer before flattening them into the + * main SoA structure. + * * @param dest The target PropertyContainer holding the SoA arrays. + * @param type The VTX FieldType identifying which array to target. + * @param idx The specific sub-array index within the FlatArray. + * @param temp The temporary container where the single object was initially loaded. + */ + void PushToFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, + const PropertyContainer& temp) const { + switch (type) { + case FieldType::Vector: + if (!temp.vector_properties.empty()) { + dest.vector_arrays.PushBack(idx, temp.vector_properties[0]); + } + break; + + case FieldType::Quat: + if (!temp.quat_properties.empty()) { + dest.quat_arrays.PushBack(idx, temp.quat_properties[0]); + } + break; + + case FieldType::Transform: + if (!temp.transform_properties.empty()) { + dest.transform_arrays.PushBack(idx, temp.transform_properties[0]); + } + break; + + case FieldType::FloatRange: + if (!temp.range_properties.empty()) { + dest.range_arrays.PushBack(idx, temp.range_properties[0]); + } + break; + + case FieldType::Struct: + if (temp.entity_type_id != -1) { + dest.any_struct_arrays.PushBack(idx, temp); + } + break; + + default: + if (debug_mode_) { + std::cerr << "[LOADER] PushToFlatArray: Type " << static_cast(type) + << " is not a complex math type_max_indices supported by SoA flattening.\n"; + } + break; } } @@ -308,10 +346,11 @@ namespace VTX { * @param src_array The source collection of values/messages. */ template - void LoadArray(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, const RepeatedProtoType& src_array) { - + void LoadArray(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, + const RepeatedProtoType& src_array) { const auto* field_info = schema_->GetField(struct_name, field_name); - if (!field_info || src_array.empty()) return; + if (!field_info || src_array.empty()) + return; using ElementT = typename RepeatedProtoType::value_type; const int32_t idx = field_info->index; @@ -330,26 +369,27 @@ namespace VTX { PushToFlatArray(dest, field_info->type_id, idx, temp); } } - } - else { + } else { FillFlatArray(dest, field_info->type_id, idx, src_array); } } - + /** * @brief Injects a memory block (Blob) directly into byte_array_properties */ - void LoadBlob(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, const void* data, size_t byte_size) { + void LoadBlob(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, + const void* data, size_t byte_size) { const auto* field_info = schema_->GetField(struct_name, field_name); - if (!field_info || byte_size == 0) return; + if (!field_info || byte_size == 0) + return; const uint8_t* byte_data = static_cast(data); - + for (size_t i = 0; i < byte_size; ++i) { dest.byte_array_properties.PushBack(field_info->index, byte_data[i]); } } - + private: /** * @brief Internal helper to load an actor and extract its ID. @@ -361,15 +401,15 @@ namespace VTX { * @param idExtractor Function instance. */ template - void ExtractActorWithIdFunc(const ActorT& src, VTX::Bucket& block, const std::string& schemaType, IdExtractorFunc idExtractor) { - + void ExtractActorWithIdFunc(const ActorT& src, VTX::Bucket& block, const std::string& schemaType, + IdExtractorFunc idExtractor) { PropertyContainer& entity = block.entities.emplace_back(); Load(src, entity, schemaType); Helpers::CalculateContainerHash(entity); std::string uid = idExtractor(src); block.unique_ids.push_back(uid); } - + /** * @brief Internal utility to resize a vector to accommodate a specific index. * @tparam Vec Vector type. @@ -391,7 +431,5 @@ namespace VTX { else if constexpr (std::is_assignable_v) vector[index] = static_cast(val); } - - }; -} +} // namespace VTX diff --git a/sdk/include/vtx/common/readers/frame_reader/type_traits.h b/sdk/include/vtx/common/readers/frame_reader/type_traits.h index 33553ea..03e1f12 100644 --- a/sdk/include/vtx/common/readers/frame_reader/type_traits.h +++ b/sdk/include/vtx/common/readers/frame_reader/type_traits.h @@ -19,34 +19,36 @@ namespace VTX { /** * @brief Base struct for std::vector detection (false case). * @tparam T The type to check. */ - template + template struct is_vector : std::false_type {}; /** * @brief Specialization for std::vector detection (true case). * @tparam T The element type. * @tparam A The allocator type. */ - template + template struct is_vector> : std::true_type {}; /** * @brief Helper variable template. True if T is a std::vector. * @tparam T The type to check. */ - template + template inline constexpr bool is_vector_v = is_vector::value; /** * @brief Base struct for std::map detection (false case). */ - template struct is_map : std::false_type {}; + template + struct is_map : std::false_type {}; /** * @brief Specialization for std::map detection (true case). */ - template + template struct is_map> : std::true_type {}; /** * @brief Helper variable template. True if T is a std::map. */ - template inline constexpr bool is_map_v = is_map::value; + template + inline constexpr bool is_map_v = is_map::value; /** @@ -64,37 +66,40 @@ namespace VTX { * @tparam T The type to inspect. * @tparam void Placeholder for SFINAE. */ - template + template struct HasJsonMapping : std::false_type {}; /** * @brief Specialization for types that HAVE a valid JsonMapping. */ - template + template struct HasJsonMapping::GetFields())>> : std::true_type {}; /** * @brief Helper variable. True if T has a registered JSON mapping. */ - template inline constexpr bool has_mapping_v = HasJsonMapping::value; + template + inline constexpr bool has_mapping_v = HasJsonMapping::value; /** * @brief Base template for FlatBuffer mappings. * @details Users must specialize this struct to link C++ types with FlatBuffer tables. */ - template struct FlatBufferMapping; + template + struct FlatBufferMapping; /** * @brief SFINAE Meta-function to detect if a type has a defined FlatBufferMapping. * @details Checks if `VTX::FlatBufferMapping::GetFields()` is a valid expression. */ - template + template struct HasFbMapping : std::false_type {}; - template + template struct HasFbMapping::GetFields())>> : std::true_type {}; /** * @brief Helper variable. True if T has a registered FlatBuffer mapping. */ - template inline constexpr bool has_fb_mapping_v = HasFbMapping::value; -} \ No newline at end of file + template + inline constexpr bool has_fb_mapping_v = HasFbMapping::value; +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/common/readers/frame_reader/universal_deserializer.h b/sdk/include/vtx/common/readers/frame_reader/universal_deserializer.h index 30ef8eb..cfe9390 100644 --- a/sdk/include/vtx/common/readers/frame_reader/universal_deserializer.h +++ b/sdk/include/vtx/common/readers/frame_reader/universal_deserializer.h @@ -57,10 +57,9 @@ namespace VTX { * @tparam ErrorPolicy Policy class determining how to handle missing keys or type mismatches (e.g., Strict, Lenient). * @tparam NamingPolicy Policy class determining how to transform key names before lookup (e.g., Exact, Lowercase). */ - template + template class UniversalDeserializer { public: - /** * @brief Main entry point to load data into a struct. * @details This function orchestrates the process: @@ -72,9 +71,8 @@ namespace VTX { * @param adapter The adapter instance pointing to the current data node. * @return T A fully populated instance of the struct. */ - template + template static T Load(const FormatAdapter& adapter) { - // Compile-time check to ensure T has been registered with the reflection system. using RawAdapter = std::decay_t; using Selector = MappingSelector; @@ -82,22 +80,19 @@ namespace VTX { // Compile-time validation static_assert(Selector::HasMapping, - "ERROR: The target struct T does not have a registered Mapping for this format."); + "ERROR: The target struct T does not have a registered Mapping for this format."); - T instance{}; + T instance {}; // Retrieve fields from the selected mapping (JsonMapping or FlatBufferMapping) constexpr auto mapping = Mapping::GetFields(); - std::apply([&](auto&&... fields) { - (ProcessField(instance, adapter, fields), ...); - }, mapping); + std::apply([&](auto&&... fields) { (ProcessField(instance, adapter, fields), ...); }, mapping); return instance; } private: - /** * @brief Recursive function to deserialize a specific value based on its type. * @details Uses `if constexpr` to select the correct loading strategy (Vector, Map, Struct, or Primitive). @@ -107,11 +102,11 @@ namespace VTX { * @param out[out] Reference to the variable where the value will be stored. */ template - static void DeserializeValue(const FormatAdapter& adapter, T& out) - { + static void DeserializeValue(const FormatAdapter& adapter, T& out) { //std::vector if constexpr (is_vector_v) { - if (!adapter.IsArray()) return; + if (!adapter.IsArray()) + return; using ElementType = typename T::value_type; for (size_t i = 0; i < adapter.Size(); ++i) { ElementType element; @@ -121,7 +116,8 @@ namespace VTX { } //std::map else if constexpr (is_map_v) { - if (!adapter.IsMap()) return; + if (!adapter.IsMap()) + return; using KeyType = typename T::key_type; using MappedType = typename T::mapped_type; @@ -141,10 +137,9 @@ namespace VTX { out = Load(adapter); } //basic types (int, float, bool, std::string) - else - { - static_assert(std::is_default_constructible_v, - "ERROR: T is not a basic type_max_indices or vector or map, or is not registered as StructMapping."); + else { + static_assert(std::is_default_constructible_v, "ERROR: T is not a basic type_max_indices or vector " + "or map, or is not registered as StructMapping."); out = adapter.template GetValue(); } @@ -162,7 +157,6 @@ namespace VTX { */ template static void ProcessField(T& instance, const FormatAdapter& adapter, const FieldType& field) { - // Transform the code name to the data name (e.g. "MyVar" -> "my_var") std::string target_key = NamingPolicy::Transform(field.json_key); @@ -174,6 +168,5 @@ namespace VTX { DeserializeValue(adapter.GetChild(target_key), instance.*(field.member_ptr)); } - }; -} +} // namespace VTX diff --git a/sdk/include/vtx/common/readers/frame_reader/vtx_loader.h b/sdk/include/vtx/common/readers/frame_reader/vtx_loader.h index a3f70f4..601afc1 100644 --- a/sdk/include/vtx/common/readers/frame_reader/vtx_loader.h +++ b/sdk/include/vtx/common/readers/frame_reader/vtx_loader.h @@ -25,4 +25,4 @@ namespace VTX { T Load(const FbPointer* fb_data) { return FlatBufferAdapter::Load(fb_data); } -} +} // namespace VTX diff --git a/sdk/include/vtx/common/readers/json_file_reader.h b/sdk/include/vtx/common/readers/json_file_reader.h index bfdaa1b..4e4087b 100644 --- a/sdk/include/vtx/common/readers/json_file_reader.h +++ b/sdk/include/vtx/common/readers/json_file_reader.h @@ -24,10 +24,8 @@ namespace VTX { json j; try { file >> j; - } - catch (const json::parse_error& e) { - std::cerr << "[Error Parser] JSON malformed " << filepath - << "\nDetalles: " << e.what() << std::endl; + } catch (const json::parse_error& e) { + std::cerr << "[Error Parser] JSON malformed " << filepath << "\nDetalles: " << e.what() << std::endl; return std::nullopt; } @@ -46,5 +44,5 @@ namespace VTX { return buffer.str(); } - }; -} \ No newline at end of file + }; // namespace JsonFileReader +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/common/readers/schema_reader/game_schema_types.h b/sdk/include/vtx/common/readers/schema_reader/game_schema_types.h index fd4d74c..4c36337 100644 --- a/sdk/include/vtx/common/readers/schema_reader/game_schema_types.h +++ b/sdk/include/vtx/common/readers/schema_reader/game_schema_types.h @@ -30,51 +30,48 @@ namespace VTX { Transform, FloatRange, Struct, - Enum + Enum }; - enum class FieldContainerType : uint8_t { - None = 0, - Array, - Map - }; - + enum class FieldContainerType : uint8_t { None = 0, Array, Map }; + /** * @brief Metadata of a field. */ struct SchemaMeta { - std::string type; ///< Bucket type_max_indices string (e.g., "int32", "float", "vector","struct"). For debugging - std::string key_type; ///< If this is a Map, the type_max_indices of the key (e.g., "string", "enum"). - std::string category; ///< Organizational category (e.g., "Stats", "Transform"). - std::string display_name; ///< Human-readable name for UI. + std::string type; ///< Bucket type_max_indices string (e.g., "int32", "float", "vector","struct"). For debugging + std::string key_type; ///< If this is a Map, the type_max_indices of the key (e.g., "string", "enum"). + std::string category; ///< Organizational category (e.g., "Stats", "Transform"). + std::string display_name; ///< Human-readable name for UI. std::string tooltip; ///< Description or help text. - std::string default_value; ///< Default value as a string. + std::string default_value; ///< Default value as a string. int32_t version; ///< Version of this specific field definition. - + //internal only, schema resolver will generate this - int32_t fixed_array_dim; ///< If > 0, indicates this field is a fixed-size array. + int32_t fixed_array_dim; ///< If > 0, indicates this field is a fixed-size array. }; /** * @brief Definition of a single property/field within a struct. */ - struct SchemaField { - std::string name; ///< Internal variable name (e.g., "health_current"). - std::string struct_type; ///< If type_max_indices is a struct, the struct name - FieldType type_id; ///< Bucket type_max_indices string (e.g., "int32", "float", "vector"). For quick swithc-case - FieldType key_id; - FieldContainerType container_type; ///< Bucket type_max_indices `container(e.g., "struct", "array", ",a"). For quick swithc-case - SchemaMeta meta; ///< Additional UI metadata. - + struct SchemaField { + std::string name; ///< Internal variable name (e.g., "health_current"). + std::string struct_type; ///< If type_max_indices is a struct, the struct name + FieldType type_id; ///< Bucket type_max_indices string (e.g., "int32", "float", "vector"). For quick swithc-case + FieldType key_id; + FieldContainerType + container_type; ///< Bucket type_max_indices `container(e.g., "struct", "array", ",a"). For quick swithc-case + SchemaMeta meta; ///< Additional UI metadata. + //internal only, schema resolver will generate this - int32_t index; ///< The index in the generic PropertyContainer arrays. + int32_t index; ///< The index in the generic PropertyContainer arrays. }; /** * @brief Definition of a complex structure (like a Class or Struct in C++). */ struct SchemaStruct { - std::string struct_name; ///< Name of the structure. + std::string struct_name; ///< Name of the structure. std::vector fields; ///< List of fields contained in this structure. /** * @brief Max indices required for each type in this struct. @@ -95,6 +92,6 @@ namespace VTX { return result; } }; - + } // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/common/readers/schema_reader/schema_registry.h b/sdk/include/vtx/common/readers/schema_reader/schema_registry.h index 68812b2..11eda19 100644 --- a/sdk/include/vtx/common/readers/schema_reader/schema_registry.h +++ b/sdk/include/vtx/common/readers/schema_reader/schema_registry.h @@ -19,20 +19,15 @@ #include "vtx/common/vtx_types.h" namespace VTX { - - + + /** * @class SchemaRegistry * @brief Singleton-like registry that holds all loaded structure definitions. */ class SchemaRegistry { public: - enum class ELoadMethod: uint32_t - { - LoadToBuffer = 0, - LoadToSchema, - Both - }; + enum class ELoadMethod : uint32_t { LoadToBuffer = 0, LoadToSchema, Both }; /** * @brief Default constructor. Initializes the registry as valid. */ @@ -44,7 +39,7 @@ namespace VTX { * @return true If loading and parsing were successful. * @return false If the file could not be opened or parsed. */ - bool LoadFromJson(const std::string& json_path,ELoadMethod = ELoadMethod::Both); + bool LoadFromJson(const std::string& json_path, ELoadMethod = ELoadMethod::Both); /** * @brief @@ -53,7 +48,7 @@ namespace VTX { * @return */ bool LoadFromRawString(const std::string& raw_json); - + /** * @brief Retrieves the numeric index for a specific field within a struct. * @param structName The name of the structure (e.g., "PlayerState"). @@ -81,28 +76,26 @@ namespace VTX { * @return */ const VTX::SchemaField* GetField(const std::string& struct_name, const std::string& field_name) const; - + /** * @brief Retrieves the unique integer ID for a structure (matches the generated Enum). * @param name The name of the structure. * @return int32_t The TypeId, or -1 if the struct is not registered. */ int32_t GetStructTypeId(const std::string& name) const; - + /**/ - std::string GetContentAsString() const{return json_content_;} - - const VTX::PropertyAddressCache& GetPropertyCache() const - { - return property_cache_; - } + std::string GetContentAsString() const { return json_content_; } + + const VTX::PropertyAddressCache& GetPropertyCache() const { return property_cache_; } + private: std::string json_content_; std::unordered_map structs_; ///< Storage map: Struct Name -> Definition. std::unordered_map struct_type_ids_; VTX::PropertyAddressCache property_cache_; - int32_t current_type_id_=0; - bool b_is_valid_;///< Internal validity flag. + int32_t current_type_id_ = 0; + bool b_is_valid_; ///< Internal validity flag. }; - + } // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/common/vtx_concepts.h b/sdk/include/vtx/common/vtx_concepts.h index 77fc239..b489950 100644 --- a/sdk/include/vtx/common/vtx_concepts.h +++ b/sdk/include/vtx/common/vtx_concepts.h @@ -5,96 +5,84 @@ #include "vtx_types.h" -namespace VTX -{ +namespace VTX { struct PropertyAddressCache; struct ChunkIndexData; struct SessionFooter; - + template - struct SchemaAdapter - { + struct SchemaAdapter { static void BuildCache(const SchemaT&, PropertyAddressCache&) = delete; }; - //List of supported types in vtx - template - concept VtxScalarType = - std::same_as || std::same_as || std::same_as || std::same_as || - std::same_as || std::same_as || std::same_as || - std::same_as || std::same_as || - std::same_as || std::same_as || - std::same_as || std::same_as; + //List of supported types in vtx + template + concept VtxScalarType = + std::same_as || std::same_as || std::same_as || std::same_as || + std::same_as || std::same_as || std::same_as || + std::same_as || std::same_as || std::same_as || + std::same_as || std::same_as || + std::same_as; //Concepts for arrays, uint8 is used for bools - template + template concept VtxArrayType = VtxScalarType || std::same_as; - /*Concept that defines how a vtx writer policy must look*/ - template - concept IVtxWriterPolicy = requires( - const typename T::HeaderType& header_config, - const typename T::SchemaType& schema, - std::vector>& frames, - int32_t chunk_index, - bool is_compressed, - const std::vector& seek_table, - const SessionFooter& footer_data - ) - { - typename T::FrameType; - typename T::SchemaType; - typename T::HeaderType; - - requires std::semiregular; - - { T::GetMagicBytes() } -> std::convertible_to; - { T::SerializeHeader(header_config, schema) } -> std::convertible_to; - { T::SerializeChunk(frames, chunk_index,is_compressed) } -> std::convertible_to; - { T::SerializeFooter(seek_table, footer_data) } -> std::convertible_to; - }; - + template + concept IVtxWriterPolicy = + requires(const typename T::HeaderType& header_config, const typename T::SchemaType& schema, + std::vector>& frames, int32_t chunk_index, bool is_compressed, + const std::vector& seek_table, const SessionFooter& footer_data) { + typename T::FrameType; + typename T::SchemaType; + typename T::HeaderType; + + requires std::semiregular; + + { T::GetMagicBytes() } -> std::convertible_to; + { T::SerializeHeader(header_config, schema) } -> std::convertible_to; + { T::SerializeChunk(frames, chunk_index, is_compressed) } -> std::convertible_to; + { T::SerializeFooter(seek_table, footer_data) } -> std::convertible_to; + }; + /*Concept that defines how a vtx reader_ policy must look*/ - template - concept IVtxReaderPolicy = requires( - std::string buffer, - const typename T::HeaderType& header, - const typename T::FooterType& footer, - std::vector index_table, - VTX::GameTime::VTXGameTimes& game_times, - int32_t chunk_idx, - std::stop_token stop_token, - std::vector& out_native_frames, - std::vector& out_decompressed_blob, - std::vector>& out_raw_frames_spans) - { - typename T::HeaderType; - typename T::FooterType; - typename T::SchemaType; - - requires std::semiregular; - requires std::semiregular; - requires std::semiregular; - - { T::ParseHeader(buffer) } -> std::same_as; - { T::ParseFooter(buffer) } -> std::same_as; - - { T::GetTotalFrames(footer) } -> std::convertible_to; - { T::GetSchema(header) } -> std::convertible_to; - { T::GetVTXHeader(header) } -> std::convertible_to; - { T::GetVTXFooter(footer) } -> std::convertible_to< VTX::FileFooter>; - { T::ProcessChunkData(chunk_idx, buffer, stop_token,out_native_frames,out_decompressed_blob,out_raw_frames_spans) } -> std::same_as; - }; + template + concept IVtxReaderPolicy = + requires(std::string buffer, const typename T::HeaderType& header, const typename T::FooterType& footer, + std::vector index_table, VTX::GameTime::VTXGameTimes& game_times, + int32_t chunk_idx, std::stop_token stop_token, std::vector& out_native_frames, + std::vector& out_decompressed_blob, + std::vector>& out_raw_frames_spans) { + typename T::HeaderType; + typename T::FooterType; + typename T::SchemaType; + + requires std::semiregular; + requires std::semiregular; + requires std::semiregular; + + { T::ParseHeader(buffer) } -> std::same_as; + { T::ParseFooter(buffer) } -> std::same_as; + + { T::GetTotalFrames(footer) } -> std::convertible_to; + { T::GetSchema(header) } -> std::convertible_to; + { T::GetVTXHeader(header) } -> std::convertible_to; + { T::GetVTXFooter(footer) } -> std::convertible_to; + { + T::ProcessChunkData(chunk_idx, buffer, stop_token, out_native_frames, out_decompressed_blob, + out_raw_frames_spans) + } -> std::same_as; + }; //Schema adapter concept //Is mandatory to implement a BuildCache method , receives schema and cache and return void template concept SchemaAdaptable = requires(const T& schema, PropertyAddressCache& cache) { - { SchemaAdapter::BuildCache(schema, cache) } -> std::same_as; - }; -} + { SchemaAdapter::BuildCache(schema, cache) } -> std::same_as; + }; +} // namespace VTX diff --git a/sdk/include/vtx/common/vtx_entity_registry.h b/sdk/include/vtx/common/vtx_entity_registry.h index 002d5e6..5972cca 100644 --- a/sdk/include/vtx/common/vtx_entity_registry.h +++ b/sdk/include/vtx/common/vtx_entity_registry.h @@ -7,9 +7,7 @@ namespace VTX { class EntityTypeRegistry { public: - static void RegisterName(int32_t type_id, const std::string& name) { - GetInternalMap()[type_id] = name; - } + static void RegisterName(int32_t type_id, const std::string& name) { GetInternalMap()[type_id] = name; } static std::string GetName(int32_t type_id) { auto& map = GetInternalMap(); @@ -20,9 +18,7 @@ namespace VTX { return "Type_" + std::to_string(type_id); } - static void Clear() { - GetInternalMap().clear(); - } + static void Clear() { GetInternalMap().clear(); } private: static std::unordered_map& GetInternalMap() { diff --git a/sdk/include/vtx/common/vtx_error_policy.h b/sdk/include/vtx/common/vtx_error_policy.h index 12a95af..05983cd 100644 --- a/sdk/include/vtx/common/vtx_error_policy.h +++ b/sdk/include/vtx/common/vtx_error_policy.h @@ -6,7 +6,7 @@ * @brief Generic error policies for reading operations * @author Zenos Interactive */ -namespace VTX{ +namespace VTX { /** * @struct StrictErrorPolicy @@ -20,9 +20,7 @@ namespace VTX{ * @param key The name of the missing key. * @throw std::runtime_error Always throws. */ - static void OnMissingKey(const std::string& key) { - throw std::runtime_error("Missing mandatory key : " + key); - } + static void OnMissingKey(const std::string& key) { throw std::runtime_error("Missing mandatory key : " + key); } /** * @brief Called when the data type in JSON does not match the C++ target type. @@ -66,11 +64,9 @@ namespace VTX{ */ struct SilentErrorPolicy { /** @brief No-op implementation. */ - static void OnMissingKey(const std::string& key) { - } + static void OnMissingKey(const std::string& key) {} /** @brief No-op implementation. */ - static void OnTypeMismatch(const std::string& key) { - } + static void OnTypeMismatch(const std::string& key) {} }; // ========================================================== @@ -109,4 +105,4 @@ namespace VTX{ return key; } }; -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/common/vtx_logger.h b/sdk/include/vtx/common/vtx_logger.h index 207b2ea..8d1f65d 100644 --- a/sdk/include/vtx/common/vtx_logger.h +++ b/sdk/include/vtx/common/vtx_logger.h @@ -21,20 +21,15 @@ namespace VTX { // Replicates UE_LOG macros // ========================================================== - constexpr const char* ANSI_COLOR_RESET = "\033[0m"; - constexpr const char* ANSI_COLOR_INFO = "\033[32m"; // Green - constexpr const char* ANSI_COLOR_WARN = "\033[33m"; // Yellow - constexpr const char* ANSI_COLOR_ERROR = "\033[31m"; // Red - constexpr const char* ANSI_COLOR_DEBUG = "\033[36m"; // Cian + constexpr const char* ANSI_COLOR_RESET = "\033[0m"; + constexpr const char* ANSI_COLOR_INFO = "\033[32m"; // Green + constexpr const char* ANSI_COLOR_WARN = "\033[33m"; // Yellow + constexpr const char* ANSI_COLOR_ERROR = "\033[31m"; // Red + constexpr const char* ANSI_COLOR_DEBUG = "\033[36m"; // Cian class Logger { public: - enum class Level { - Info, - Warning, - Error, - Debug - }; + enum class Level { Info, Warning, Error, Debug }; struct Entry { Level level = Level::Info; @@ -66,13 +61,9 @@ namespace VTX { // print). Useful for benchmarks, production deployments, or noisy // workloads where per-chunk trace output would dominate timings. // Thread-safe; can be toggled any time. - void SetDebugEnabled(bool enabled) { - debug_enabled_.store(enabled, std::memory_order_relaxed); - } + void SetDebugEnabled(bool enabled) { debug_enabled_.store(enabled, std::memory_order_relaxed); } - bool IsDebugEnabled() const { - return debug_enabled_.load(std::memory_order_relaxed); - } + bool IsDebugEnabled() const { return debug_enabled_.load(std::memory_order_relaxed); } void Log(Level LogLvl, const std::string& Message) { if (LogLvl == Level::Debug && !IsDebugEnabled()) { @@ -95,11 +86,8 @@ namespace VTX { entry.timestamp = timestamp_stream.str(); entry.message = Message; - std::cout << GetColorCode(LogLvl) - << "[" << entry.timestamp << "] " - << "[" << ToString(LogLvl) << "] " - << Message - << ANSI_COLOR_RESET << std::endl; + std::cout << GetColorCode(LogLvl) << "[" << entry.timestamp << "] " + << "[" << ToString(LogLvl) << "] " << Message << ANSI_COLOR_RESET << std::endl; sinks_copy.reserve(sinks_.size()); for (const auto& [_, sink] : sinks_) { @@ -112,32 +100,33 @@ namespace VTX { } } - template + template void Info(std::string_view format_str, Args&&... args) { Log(Level::Info, std::vformat(format_str, std::make_format_args(args...))); } - template + template void Warn(std::string_view format_str, Args&&... args) { Log(Level::Warning, std::vformat(format_str, std::make_format_args(args...))); } - template + template void Error(std::string_view format_str, Args&&... args) { Log(Level::Error, std::vformat(format_str, std::make_format_args(args...))); } - template + template void Debug(std::string_view format_str, Args&&... args) { Log(Level::Debug, std::vformat(format_str, std::make_format_args(args...))); } + private: std::mutex mute_; std::unordered_map sinks_; SinkId next_sink_id_ = 1; - std::atomic debug_enabled_{true}; + std::atomic debug_enabled_ {true}; - template + template void LogFormatted(Level log_level, const char* message, Args... args) { if constexpr (sizeof...(args) == 0) { Log(log_level, message); @@ -159,27 +148,36 @@ namespace VTX { const char* ToString(Level LogLvl) const { switch (LogLvl) { - case Level::Info: return "INFO"; - case Level::Warning: return "WARN"; - case Level::Error: return "ERROR"; - case Level::Debug: return "DEBUG"; - default: return "LOG"; + case Level::Info: + return "INFO"; + case Level::Warning: + return "WARN"; + case Level::Error: + return "ERROR"; + case Level::Debug: + return "DEBUG"; + default: + return "LOG"; } } const char* GetColorCode(Level LogLvl) const { switch (LogLvl) { - case Level::Info: return ANSI_COLOR_INFO; - case Level::Warning: return ANSI_COLOR_WARN; - case Level::Error: return ANSI_COLOR_ERROR; - case Level::Debug: return ANSI_COLOR_DEBUG; - default: return ANSI_COLOR_RESET; + case Level::Info: + return ANSI_COLOR_INFO; + case Level::Warning: + return ANSI_COLOR_WARN; + case Level::Error: + return ANSI_COLOR_ERROR; + case Level::Debug: + return ANSI_COLOR_DEBUG; + default: + return ANSI_COLOR_RESET; } } - }; - #define VTX_INFO(...) VTX::Logger::Instance().Info(__VA_ARGS__) - #define VTX_WARN(...) VTX::Logger::Instance().Warn(__VA_ARGS__) - #define VTX_ERROR(...) VTX::Logger::Instance().Error(__VA_ARGS__) - #define VTX_DEBUG(...) VTX::Logger::Instance().Debug(__VA_ARGS__) +#define VTX_INFO(...) VTX::Logger::Instance().Info(__VA_ARGS__) +#define VTX_WARN(...) VTX::Logger::Instance().Warn(__VA_ARGS__) +#define VTX_ERROR(...) VTX::Logger::Instance().Error(__VA_ARGS__) +#define VTX_DEBUG(...) VTX::Logger::Instance().Debug(__VA_ARGS__) } // namespace VTX diff --git a/sdk/include/vtx/common/vtx_optimized_bone.h b/sdk/include/vtx/common/vtx_optimized_bone.h index f47a116..4a8a62a 100644 --- a/sdk/include/vtx/common/vtx_optimized_bone.h +++ b/sdk/include/vtx/common/vtx_optimized_bone.h @@ -6,23 +6,21 @@ #include #include -#include "vtx_types.h" +#include "vtx_types.h" -namespace VTX -{ +namespace VTX { /** * @brief Equivalent to UE's FBoneCompressionSettings. * Defines the ranges and precision modes for packing the Transform. */ struct BoneCompressionSettings { - float position_minimum = -10000.0f; float position_range = 20000.0f; // Max - Min - + float scale_minimum = 0.0f; float scale_range = 10.0f; - - Vector default_scale = { 1.0, 1.0, 1.0 }; + + Vector default_scale = {1.0, 1.0, 1.0}; bool use_high_precision_position_no_scale = false; inline float GetPositionRange() const { return position_range; } @@ -34,51 +32,43 @@ namespace VTX * A 16-byte (128-bit) container for a full Transform (Quat, Pos, Scale). * Layout: [41 Bits Rotation] [87 Bits Payload] */ - struct alignas(16) OptimizedBoneData - { + struct alignas(16) OptimizedBoneData { /** Raw bit-storage. Exactly 16 bytes. * alignas(16) ensures this can be loaded into SIMD registers efficiently. */ uint32_t packed_transform[4]; /** Packs a standard VTX::Transform into the 16-byte bitstream. */ - void Pack(const Transform& SourceTransform, const BoneCompressionSettings& Settings) - { + void Pack(const Transform& SourceTransform, const BoneCompressionSettings& Settings) { // Reset all bits to zero packed_transform[0] = packed_transform[1] = packed_transform[2] = packed_transform[3] = 0; // --- SECTION 1: ROTATION (Fixed 41 Bits) --- Quat NormalizedQuat = SourceTransform.rotation; - + // Normalize Quat - float invLen = 1.0f / std::sqrt(NormalizedQuat.x * NormalizedQuat.x + - NormalizedQuat.y * NormalizedQuat.y + - NormalizedQuat.z * NormalizedQuat.z + - NormalizedQuat.w * NormalizedQuat.w); + float invLen = 1.0f / std::sqrt(NormalizedQuat.x * NormalizedQuat.x + NormalizedQuat.y * NormalizedQuat.y + + NormalizedQuat.z * NormalizedQuat.z + NormalizedQuat.w * NormalizedQuat.w); NormalizedQuat.x *= invLen; NormalizedQuat.y *= invLen; NormalizedQuat.z *= invLen; NormalizedQuat.w *= invLen; // Ensure W is always positive (Quaternions are double-cover, -Q == Q) - if (NormalizedQuat.w < 0.0f) - { + if (NormalizedQuat.w < 0.0f) { NormalizedQuat.x *= -1.0f; NormalizedQuat.y *= -1.0f; NormalizedQuat.z *= -1.0f; NormalizedQuat.w *= -1.0f; } - const std::array Components = { - NormalizedQuat.x, NormalizedQuat.y, NormalizedQuat.z, NormalizedQuat.w - }; + const std::array Components = {NormalizedQuat.x, NormalizedQuat.y, NormalizedQuat.z, + NormalizedQuat.w}; // Find the index of the largest absolute component int32_t LargestIndex = 0; float LargestValue = std::abs(Components[0]); - for (int32_t i = 1; i < 4; i++) - { - if (std::abs(Components[i]) > LargestValue) - { + for (int32_t i = 1; i < 4; i++) { + if (std::abs(Components[i]) > LargestValue) { LargestValue = std::abs(Components[i]); LargestIndex = i; } @@ -88,12 +78,13 @@ namespace VTX const float SignMultiplier = (Components[LargestIndex] < 0.0f) ? -1.0f : 1.0f; // Pack the index (2 bits) and the other 3 components (13 bits each) - constexpr float RotationPackFactor = 5791.9103f; + constexpr float RotationPackFactor = 5791.9103f; const uint32_t PackedRotationIndex = static_cast(LargestIndex); - + auto PackComponent = [&](int offset) -> uint32_t { float comp = Components[(LargestIndex + offset) % 4] * SignMultiplier; - return std::clamp(static_cast(std::round((comp + 0.707107f) * RotationPackFactor)), 0u, 8191u); + return std::clamp(static_cast(std::round((comp + 0.707107f) * RotationPackFactor)), 0u, + 8191u); }; const uint32_t Quat1 = PackComponent(1); @@ -110,13 +101,14 @@ namespace VTX const float PosRange = std::max(0.0001f, Settings.GetPositionRange()); const float InvPosRange = 1.0f / PosRange; - if (Settings.use_high_precision_position_no_scale) - { + if (Settings.use_high_precision_position_no_scale) { // Mode: 21 bits per Position axis (0 to 2,097,151) - const float PosScaleFactor = 2097151.0f * InvPosRange; + const float PosScaleFactor = 2097151.0f * InvPosRange; auto PackPos = [&](double locAxis) -> uint32_t { - return std::clamp(static_cast(std::round((static_cast(locAxis) - Settings.position_minimum) * PosScaleFactor)), 0u, 2097151u); + return std::clamp(static_cast(std::round( + (static_cast(locAxis) - Settings.position_minimum) * PosScaleFactor)), + 0u, 2097151u); }; const uint32_t PackedPosX = PackPos(Location.x); @@ -126,14 +118,14 @@ namespace VTX packed_transform[1] |= (PackedPosX << 9); packed_transform[2] = PackedPosY | ((PackedPosZ & 0x7FF) << 21); packed_transform[3] = (PackedPosZ >> 11); - } - else - { + } else { // Mode: 16 bits Position (0 to 65535) + 8 bits Scale (0 to 255) - const float PosScaleFactor = 65535.0f * InvPosRange; + const float PosScaleFactor = 65535.0f * InvPosRange; auto PackPos = [&](double locAxis) -> uint32_t { - return std::clamp(static_cast(std::round((static_cast(locAxis) - Settings.position_minimum) * PosScaleFactor)), 0u, 65535u); + return std::clamp(static_cast(std::round( + (static_cast(locAxis) - Settings.position_minimum) * PosScaleFactor)), + 0u, 65535u); }; const uint32_t PackedPosX = PackPos(Location.x); @@ -145,7 +137,9 @@ namespace VTX const float ScalePackFactor = 255.0f * (1.0f / ScaleRange); auto PackScale = [&](double scaleAxis) -> uint32_t { - return std::clamp(static_cast(std::round((static_cast(scaleAxis) - Settings.scale_minimum) * ScalePackFactor)), 0u, 255u); + return std::clamp(static_cast(std::round( + (static_cast(scaleAxis) - Settings.scale_minimum) * ScalePackFactor)), + 0u, 255u); }; const uint32_t PackedScaleX = PackScale(Scale.x); @@ -159,8 +153,7 @@ namespace VTX } /** Unpacks the 16-byte buffer into a usable VTX::Transform. */ - void Unpack(const BoneCompressionSettings& Settings, Transform& OutTransform) const - { + void Unpack(const BoneCompressionSettings& Settings, Transform& OutTransform) const { // --- STEP 1: EXTRACT ROTATION --- const uint32_t QuatIndex = packed_transform[0] & 0x3; const uint32_t RawQuat1 = (packed_transform[0] >> 2) & 0x1FFF; @@ -170,8 +163,8 @@ namespace VTX std::array QuatComponents = {0.0f, 0.0f, 0.0f, 0.0f}; constexpr float QuatUnpackFactor = 0.000172654f; - auto DequantizeQuat = [](const uint32_t Value) { - return (static_cast(Value) * QuatUnpackFactor) - 0.707107f; + auto DequantizeQuat = [](const uint32_t Value) { + return (static_cast(Value) * QuatUnpackFactor) - 0.707107f; }; QuatComponents[(QuatIndex + 1) % 4] = DequantizeQuat(RawQuat1); @@ -179,23 +172,20 @@ namespace VTX QuatComponents[(QuatIndex + 3) % 4] = DequantizeQuat(RawQuat3); // Reconstruct the largest component using the property X^2 + Y^2 + Z^2 + W^2 = 1 - const float SquaredSum = QuatComponents[(QuatIndex + 1) % 4] * QuatComponents[(QuatIndex + 1) % 4] + - QuatComponents[(QuatIndex + 2) % 4] * QuatComponents[(QuatIndex + 2) % 4] + + const float SquaredSum = QuatComponents[(QuatIndex + 1) % 4] * QuatComponents[(QuatIndex + 1) % 4] + + QuatComponents[(QuatIndex + 2) % 4] * QuatComponents[(QuatIndex + 2) % 4] + QuatComponents[(QuatIndex + 3) % 4] * QuatComponents[(QuatIndex + 3) % 4]; QuatComponents[QuatIndex] = std::sqrt(std::max(0.0f, 1.0f - SquaredSum)); - - OutTransform.rotation = { - QuatComponents[0], QuatComponents[1], QuatComponents[2], QuatComponents[3] - }; - + + OutTransform.rotation = {QuatComponents[0], QuatComponents[1], QuatComponents[2], QuatComponents[3]}; + // --- STEP 2: EXTRACT POSITION & SCALE --- Vector OutPosition; Vector OutScale = Settings.default_scale; const float PosRange = Settings.GetPositionRange(); - if (Settings.use_high_precision_position_no_scale) - { + if (Settings.use_high_precision_position_no_scale) { // 21-bit Extraction const uint32_t RawPosX = (packed_transform[1] >> 9) & 0x1FFFFF; const uint32_t RawPosY = (packed_transform[2] & 0x1FFFFF); @@ -203,12 +193,13 @@ namespace VTX const float PosUnpackFactor = PosRange * 0.000000476837f; - OutPosition.x = static_cast(static_cast(RawPosX) * PosUnpackFactor + Settings.position_minimum); - OutPosition.y = static_cast(static_cast(RawPosY) * PosUnpackFactor + Settings.position_minimum); - OutPosition.z = static_cast(static_cast(RawPosZ) * PosUnpackFactor + Settings.position_minimum); - } - else - { + OutPosition.x = + static_cast(static_cast(RawPosX) * PosUnpackFactor + Settings.position_minimum); + OutPosition.y = + static_cast(static_cast(RawPosY) * PosUnpackFactor + Settings.position_minimum); + OutPosition.z = + static_cast(static_cast(RawPosZ) * PosUnpackFactor + Settings.position_minimum); + } else { // 16-bit Position + 8-bit Scale Extraction const uint32_t RawPosX = (packed_transform[1] >> 9) & 0xFFFF; const uint32_t RawPosY = ((packed_transform[1] >> 25) & 0x7F) | ((packed_transform[2] & 0x1FF) << 7); @@ -220,15 +211,21 @@ namespace VTX const uint32_t RawScaleY = (packed_transform[3] >> 1) & 0xFF; const uint32_t RawScaleZ = (packed_transform[3] >> 9) & 0xFF; - OutPosition.x = static_cast(static_cast(RawPosX) * PosUnpackFactor + Settings.position_minimum); - OutPosition.y = static_cast(static_cast(RawPosY) * PosUnpackFactor + Settings.position_minimum); - OutPosition.z = static_cast(static_cast(RawPosZ) * PosUnpackFactor + Settings.position_minimum); + OutPosition.x = + static_cast(static_cast(RawPosX) * PosUnpackFactor + Settings.position_minimum); + OutPosition.y = + static_cast(static_cast(RawPosY) * PosUnpackFactor + Settings.position_minimum); + OutPosition.z = + static_cast(static_cast(RawPosZ) * PosUnpackFactor + Settings.position_minimum); const float ScaleUnpackFactor = Settings.GetScaleRange() * 0.00392157f; - OutScale.x = static_cast(static_cast(RawScaleX) * ScaleUnpackFactor + Settings.scale_minimum); - OutScale.y = static_cast(static_cast(RawScaleY) * ScaleUnpackFactor + Settings.scale_minimum); - OutScale.z = static_cast(static_cast(RawScaleZ) * ScaleUnpackFactor + Settings.scale_minimum); + OutScale.x = + static_cast(static_cast(RawScaleX) * ScaleUnpackFactor + Settings.scale_minimum); + OutScale.y = + static_cast(static_cast(RawScaleY) * ScaleUnpackFactor + Settings.scale_minimum); + OutScale.z = + static_cast(static_cast(RawScaleZ) * ScaleUnpackFactor + Settings.scale_minimum); } OutTransform.translation = OutPosition; @@ -236,14 +233,12 @@ namespace VTX } // C++ Standard I/O replacement for Unreal's FArchive - friend std::ostream& operator<<(std::ostream& os, const OptimizedBoneData& data) - { + friend std::ostream& operator<<(std::ostream& os, const OptimizedBoneData& data) { os.write(reinterpret_cast(data.packed_transform), sizeof(data.packed_transform)); return os; } - friend std::istream& operator>>(std::istream& is, OptimizedBoneData& data) - { + friend std::istream& operator>>(std::istream& is, OptimizedBoneData& data) { is.read(reinterpret_cast(data.packed_transform), sizeof(data.packed_transform)); return is; } @@ -251,4 +246,4 @@ namespace VTX // Ensure it remains exactly 16 bytes for hardware alignment guarantees static_assert(sizeof(OptimizedBoneData) == 16, "OptimizedBoneData must be exactly 16 bytes"); -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/common/vtx_property_cache.h b/sdk/include/vtx/common/vtx_property_cache.h index 932fa78..5a400b0 100644 --- a/sdk/include/vtx/common/vtx_property_cache.h +++ b/sdk/include/vtx/common/vtx_property_cache.h @@ -5,14 +5,14 @@ #include #include #include "vtx/common/vtx_types.h" -#include "vtx/common/readers/schema_reader/game_schema_types.h" -namespace VTX -{ +#include "vtx/common/readers/schema_reader/game_schema_types.h" +namespace VTX { - constexpr uint64_t MakePropertyLookupKey(int32_t index, VTX::FieldType type_id, VTX::FieldContainerType container_type) { + constexpr uint64_t MakePropertyLookupKey(int32_t index, VTX::FieldType type_id, + VTX::FieldContainerType container_type) { return (static_cast(static_cast(index)) << 32) | - (static_cast(static_cast(type_id)) << 16) | - static_cast(static_cast(container_type)); + (static_cast(static_cast(type_id)) << 16) | + static_cast(static_cast(container_type)); } /** @@ -21,18 +21,29 @@ namespace VTX */ template constexpr VTX::FieldType GetExpectedFieldType() { - if constexpr (std::same_as) return VTX::FieldType::Bool; - else if constexpr (std::same_as) return VTX::FieldType::Int32; - else if constexpr (std::same_as) return VTX::FieldType::Int64; - else if constexpr (std::same_as) return VTX::FieldType::Float; - else if constexpr (std::same_as) return VTX::FieldType::Double; - else if constexpr (std::same_as) return VTX::FieldType::String; - else if constexpr (std::same_as) return VTX::FieldType::Vector; - else if constexpr (std::same_as) return VTX::FieldType::Quat; - else if constexpr (std::same_as) return VTX::FieldType::Transform; - else if constexpr (std::same_as) return VTX::FieldType::FloatRange; + if constexpr (std::same_as) + return VTX::FieldType::Bool; + else if constexpr (std::same_as) + return VTX::FieldType::Int32; + else if constexpr (std::same_as) + return VTX::FieldType::Int64; + else if constexpr (std::same_as) + return VTX::FieldType::Float; + else if constexpr (std::same_as) + return VTX::FieldType::Double; + else if constexpr (std::same_as) + return VTX::FieldType::String; + else if constexpr (std::same_as) + return VTX::FieldType::Vector; + else if constexpr (std::same_as) + return VTX::FieldType::Quat; + else if constexpr (std::same_as) + return VTX::FieldType::Transform; + else if constexpr (std::same_as) + return VTX::FieldType::FloatRange; // else if constexpr (std::same_as) return VTX::FieldType::Struct; - else return VTX::FieldType::None; + else + return VTX::FieldType::None; } /** @@ -43,11 +54,9 @@ namespace VTX struct PropertyKey { int32_t index = -1; bool IsValid() const { return index > -1; } - bool operator==(const PropertyKey& other) const { - return index == other.index; - } + bool operator==(const PropertyKey& other) const { return index == other.index; } }; - + /** * @brief Represents the metadata of a property in the schema. * Now includes the type (and optionally array flag) for safety validation. @@ -55,7 +64,8 @@ namespace VTX struct PropertyAddress { int32_t index = -1; ///< Index in the SoA vector or FlatArray offset list. VTX::FieldType type_id = VTX::FieldType::None; ///< Stored to validate the type at runtime. - VTX::FieldContainerType container_type = VTX::FieldContainerType::None; ///< Full container metadata from schema. + VTX::FieldContainerType container_type = + VTX::FieldContainerType::None; ///< Full container metadata from schema. std::string child_type_name; bool IsValid() const { return index > -1; } }; @@ -82,7 +92,7 @@ namespace VTX for (const auto& property_name : property_order) { const auto it = properties.find(property_name); if (it != properties.end()) { - ordered_properties.push_back(OrderedPropertyView{ + ordered_properties.push_back(OrderedPropertyView { .name = it->first, .address = &it->second, }); @@ -93,7 +103,7 @@ namespace VTX ordered_properties.reserve(properties.size()); for (const auto& [property_name, property_address] : properties) { - ordered_properties.push_back(OrderedPropertyView{ + ordered_properties.push_back(OrderedPropertyView { .name = property_name, .address = &property_address, }); @@ -111,20 +121,16 @@ namespace VTX // Value: The schema cache containing its specific properties std::unordered_map structs; std::unordered_map name_to_id; - void Clear() - { + void Clear() { structs.clear(); name_to_id.clear(); } }; -} +} // namespace VTX -namespace std -{ +namespace std { template struct hash> { - size_t operator()(const VTX::PropertyKey& key) const noexcept { - return std::hash()(key.index); - } + size_t operator()(const VTX::PropertyKey& key) const noexcept { return std::hash()(key.index); } }; -} +} // namespace std diff --git a/sdk/include/vtx/common/vtx_reflection.h b/sdk/include/vtx/common/vtx_reflection.h index 070250f..9e615c0 100644 --- a/sdk/include/vtx/common/vtx_reflection.h +++ b/sdk/include/vtx/common/vtx_reflection.h @@ -12,26 +12,26 @@ #include "readers/schema_reader/game_schema_types.h" namespace VTX { - + struct FieldMapping { std::string name; std::string json_name; FieldType type; - int32_t index; // index in PropertyContainer (SoA) + int32_t index; // index in PropertyContainer (SoA) std::string sub_type; // for complexObject / struct (struct name) }; - #define VTX_BEGIN_STRUCT(struct_name) \ - inline void Register_##struct_name(SchemaRegistry& reg) { \ - SchemaStruct s; s.name = #struct_name; +#define VTX_BEGIN_STRUCT(struct_name) \ + inline void Register_##struct_name(SchemaRegistry& reg) { \ + SchemaStruct s; \ + s.name = #struct_name; - #define VTX_FIELD(field_name, json_key, type_id, idx) \ - s.fields.push_back({#field_name, json_key, type_id, idx, ""}); +#define VTX_FIELD(field_name, json_key, type_id, idx) s.fields.push_back({#field_name, json_key, type_id, idx, ""}); - #define VTX_COMPLEX_FIELD(field_name, json_key, idx, sub_struct) \ +#define VTX_COMPLEX_FIELD(field_name, json_key, idx, sub_struct) \ s.fields.push_back({#field_name, json_key, FieldType::Struct, idx, #sub_struct}); - #define VTX_END_STRUCT() \ - reg.AddStruct(std::move(s)); \ +#define VTX_END_STRUCT() \ + reg.AddStruct(std::move(s)); \ } -} +} // namespace VTX diff --git a/sdk/include/vtx/common/vtx_types.h b/sdk/include/vtx/common/vtx_types.h index d5fb4ad..c705be7 100644 --- a/sdk/include/vtx/common/vtx_types.h +++ b/sdk/include/vtx/common/vtx_types.h @@ -23,16 +23,11 @@ #include #include -namespace VTX -{ +namespace VTX { /** * @brief Identifies the binary serialization format of a VTX file. */ - enum class VtxFormat : uint8_t { - Unknown = 0, - FlatBuffers, - Protobuf - }; + enum class VtxFormat : uint8_t { Unknown = 0, FlatBuffers, Protobuf }; /** * @brief Represents a double-precision 3D vector. @@ -63,7 +58,7 @@ namespace VTX struct Transform { Vector translation; Quat rotation; - Vector scale = { 1.0, 1.0, 1.0 }; + Vector scale = {1.0, 1.0, 1.0}; bool operator==(const Transform&) const = default; }; @@ -88,7 +83,7 @@ namespace VTX */ template struct FlatArray { - std::vector data; ///< Contiguous block of data values. + std::vector data; ///< Contiguous block of data values. std::vector offsets; ///< Indices or offsets defining boundaries/mapping. /** @@ -106,40 +101,35 @@ namespace VTX size_t TotalElementCount() const { return data.size(); } /** Returns a read-only span for subarray at Index (empty on out-of-bounds). */ - std::span GetSubArray(size_t Index) const - { - if (Index >= offsets.size()) return {}; + std::span GetSubArray(size_t Index) const { + if (Index >= offsets.size()) + return {}; const size_t Start = offsets[Index]; - const size_t End = (Index + 1 < offsets.size()) ? offsets[Index + 1] : data.size(); + const size_t End = (Index + 1 < offsets.size()) ? offsets[Index + 1] : data.size(); return std::span(&data[Start], End - Start); } /** Returns a mutable span for subarray at Index (empty on out-of-bounds). */ - std::span GetMutableSubArray(size_t Index) - { - if (Index >= offsets.size()) return {}; + std::span GetMutableSubArray(size_t Index) { + if (Index >= offsets.size()) + return {}; const size_t Start = offsets[Index]; - const size_t End = (Index + 1 < offsets.size()) ? offsets[Index + 1] : data.size(); + const size_t End = (Index + 1 < offsets.size()) ? offsets[Index + 1] : data.size(); return std::span(&data[Start], End - Start); } /** Creates an empty subarray at the end (i.e., a new marker pointing to current Bucket.size()). */ - void CreateEmptySubArray() - { - offsets.push_back(data.size()); - } + void CreateEmptySubArray() { offsets.push_back(data.size()); } /** Appends a subarray with given elements at the end. */ - void AppendSubArray(std::span Items) - { + void AppendSubArray(std::span Items) { offsets.push_back(data.size()); data.insert(data.end(), Items.begin(), Items.end()); } /** Appends a subarray with an initializer_list. */ - void AppendSubArray(std::initializer_list Items) - { + void AppendSubArray(std::initializer_list Items) { AppendSubArray(std::span(Items.begin(), Items.size())); } @@ -147,9 +137,9 @@ namespace VTX * Inserts a subarray BEFORE SubIndex. Returns false if SubIndex is out of range (SubIndex > offsets.size()). * SubIndex == offsets.size() is equivalent to AppendSubArray. */ - bool InsertSubArray(size_t SubIndex, std::span Items) - { - if (SubIndex > offsets.size()) return false; + bool InsertSubArray(size_t SubIndex, std::span Items) { + if (SubIndex > offsets.size()) + return false; // Compute insertion position in Bucket const size_t InsertPos = (SubIndex < offsets.size()) ? offsets[SubIndex] : data.size(); @@ -168,8 +158,7 @@ namespace VTX } /** Inserts a subarray from initializer_list BEFORE SubIndex. */ - bool InsertSubArray(size_t SubIndex, std::initializer_list Items) - { + bool InsertSubArray(size_t SubIndex, std::initializer_list Items) { return InsertSubArray(SubIndex, std::span(Items.begin(), Items.size())); } @@ -177,12 +166,12 @@ namespace VTX * Erases the entire subarray at SubIndex. Returns false on out-of-bounds. * Elements are removed from Bucket and offsets are rebased. */ - bool EraseSubArray(size_t SubIndex) - { - if (SubIndex >= offsets.size()) return false; + bool EraseSubArray(size_t SubIndex) { + if (SubIndex >= offsets.size()) + return false; const size_t Start = offsets[SubIndex]; - const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); + const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); const size_t Count = End - Start; // Remove range from Bucket @@ -204,8 +193,7 @@ namespace VTX * Pushes Value at the end of subarray SubIndex. * Returns false on out-of-bounds. */ - bool PushBack(size_t SubIndex, const T& Value) - { + bool PushBack(size_t SubIndex, const T& Value) { if (SubIndex >= offsets.size()) { size_t old_size = offsets.size(); offsets.resize(SubIndex + 1); @@ -230,14 +218,15 @@ namespace VTX * Inserts Value at position ItemIndex INSIDE subarray SubIndex. * ItemIndex is 0..SubArraySize; ItemIndex==SubArraySize inserts at the end. */ - bool Insert(size_t SubIndex, size_t ItemIndex, const T& Value) - { - if (SubIndex >= offsets.size()) return false; + bool Insert(size_t SubIndex, size_t ItemIndex, const T& Value) { + if (SubIndex >= offsets.size()) + return false; const size_t Start = offsets[SubIndex]; - const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); - const size_t Sz = End - Start; - if (ItemIndex > Sz) return false; + const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); + const size_t Sz = End - Start; + if (ItemIndex > Sz) + return false; const size_t InsertPos = Start + ItemIndex; data.insert(data.begin() + InsertPos, Value); @@ -252,14 +241,15 @@ namespace VTX /** * Replaces the element at ItemIndex INSIDE subarray SubIndex with Value. */ - bool Replace(size_t SubIndex, size_t ItemIndex, const T& Value) - { - if (SubIndex >= offsets.size()) return false; + bool Replace(size_t SubIndex, size_t ItemIndex, const T& Value) { + if (SubIndex >= offsets.size()) + return false; const size_t Start = offsets[SubIndex]; - const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); - const size_t Sz = End - Start; - if (ItemIndex >= Sz) return false; + const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); + const size_t Sz = End - Start; + if (ItemIndex >= Sz) + return false; data[Start + ItemIndex] = Value; return true; @@ -269,14 +259,15 @@ namespace VTX * Erases the element at ItemIndex INSIDE subarray SubIndex. * Returns false on out-of-bounds. */ - bool Erase(size_t SubIndex, size_t ItemIndex) - { - if (SubIndex >= offsets.size()) return false; + bool Erase(size_t SubIndex, size_t ItemIndex) { + if (SubIndex >= offsets.size()) + return false; const size_t Start = offsets[SubIndex]; - const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); - const size_t Sz = End - Start; - if (ItemIndex >= Sz) return false; + const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); + const size_t Sz = End - Start; + if (ItemIndex >= Sz) + return false; data.erase(data.begin() + (Start + ItemIndex)); @@ -291,20 +282,22 @@ namespace VTX * Removes a range [First, Last) INSIDE subarray SubIndex. * Returns false if the range is invalid. */ - bool EraseRange(size_t SubIndex, size_t First, size_t Last) - { - if (SubIndex >= offsets.size()) return false; + bool EraseRange(size_t SubIndex, size_t First, size_t Last) { + if (SubIndex >= offsets.size()) + return false; const size_t Start = offsets[SubIndex]; - const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); - const size_t Sz = End - Start; + const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); + const size_t Sz = End - Start; - if (First > Last || Last > Sz) return false; + if (First > Last || Last > Sz) + return false; const size_t GlobalFirst = Start + First; - const size_t GlobalLast = Start + Last; + const size_t GlobalLast = Start + Last; const size_t Count = GlobalLast - GlobalFirst; - if (Count == 0) return true; + if (Count == 0) + return true; data.erase(data.begin() + GlobalFirst, data.begin() + GlobalLast); @@ -318,12 +311,12 @@ namespace VTX * Replaces the entire subarray SubIndex with Items. * Equivalent to EraseSubArray + InsertSubArray at the same position. */ - bool ReplaceSubArray(size_t SubIndex, std::span Items) - { - if (SubIndex >= offsets.size()) return false; + bool ReplaceSubArray(size_t SubIndex, std::span Items) { + if (SubIndex >= offsets.size()) + return false; const size_t Start = offsets[SubIndex]; - const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); + const size_t End = (SubIndex + 1 < offsets.size()) ? offsets[SubIndex + 1] : data.size(); const size_t OldCount = End - Start; const size_t NewCount = Items.size(); @@ -333,8 +326,7 @@ namespace VTX // Rebase subsequent offsets by (NewCount - OldCount) const ptrdiff_t Delta = static_cast(NewCount) - static_cast(OldCount); - if (Delta != 0) - { + if (Delta != 0) { for (size_t i = SubIndex + 1; i < offsets.size(); ++i) offsets[i] = static_cast(static_cast(offsets[i]) + Delta); } @@ -342,8 +334,7 @@ namespace VTX return true; } - bool ReplaceSubArray(size_t SubIndex, std::initializer_list Items) - { + bool ReplaceSubArray(size_t SubIndex, std::initializer_list Items) { return ReplaceSubArray(SubIndex, std::span(Items.begin(), Items.size())); } }; @@ -385,7 +376,6 @@ namespace VTX using FlatMapArray = FlatArray; - /** * @brief The core dynamic container for all entity properties. * @details This "Mega-Struct" acts as a variant-like container capable of holding @@ -393,7 +383,6 @@ namespace VTX * vectors for every supported type. */ struct PropertyContainer { - /** * @brief Type identifier for this container (maps to the autogenerated EntityType enum). * If -1, the type is unknown or anonymous. @@ -408,56 +397,56 @@ namespace VTX // These vectors hold the values for properties mapped by index in the Schema. - std::vector bool_properties; ///< List of boolean property values. - std::vector int32_properties; ///< List of int32_t property values. - std::vector int64_properties; ///< List of int64_t property values. - std::vector float_properties; ///< List of float property values. - std::vector double_properties; ///< List of double property values. + std::vector bool_properties; ///< List of boolean property values. + std::vector int32_properties; ///< List of int32_t property values. + std::vector int64_properties; ///< List of int64_t property values. + std::vector float_properties; ///< List of float property values. + std::vector double_properties; ///< List of double property values. std::vector string_properties; ///< List of string property values. - std::vector transform_properties; ///< List of Transform values. - std::vector vector_properties; ///< List of Vector values. - std::vector quat_properties; ///< List of Quaternion values. - std::vector range_properties; ///< List of FloatRange values. + std::vector transform_properties; ///< List of Transform values. + std::vector vector_properties; ///< List of Vector values. + std::vector quat_properties; ///< List of Quaternion values. + std::vector range_properties; ///< List of FloatRange values. // --- Optimized Flat Arrays (SoA) --- // These store arrays of arrays (e.g., an inventory list per entity). - FlatBytesArray byte_array_properties; ///< Arrays of bytes. - FlatIntArray int32_arrays; ///< Arrays of int32_ts. - Flatint64_tArray int64_arrays; ///< Arrays of int64_ts. - FlatFloatArray float_arrays; ///< Arrays of floats. - FlatDoubleArray double_arrays; ///< Arrays of doubles. + FlatBytesArray byte_array_properties; ///< Arrays of bytes. + FlatIntArray int32_arrays; ///< Arrays of int32_ts. + Flatint64_tArray int64_arrays; ///< Arrays of int64_ts. + FlatFloatArray float_arrays; ///< Arrays of floats. + FlatDoubleArray double_arrays; ///< Arrays of doubles. - FlatVectorArray vector_arrays; ///< Arrays of Vectors. - FlatQuatArray quat_arrays; ///< Arrays of Quaternions. - FlatTransformArray transform_arrays; ///< Arrays of Transforms. - FlatRangeArray range_arrays; ///< Arrays of FloatRanges. + FlatVectorArray vector_arrays; ///< Arrays of Vectors. + FlatQuatArray quat_arrays; ///< Arrays of Quaternions. + FlatTransformArray transform_arrays; ///< Arrays of Transforms. + FlatRangeArray range_arrays; ///< Arrays of FloatRanges. - FlatBoolArray bool_arrays; ///< Arrays of booleans. - FlatStringArray string_arrays; ///< Arrays of strings. + FlatBoolArray bool_arrays; ///< Arrays of booleans. + FlatStringArray string_arrays; ///< Arrays of strings. /** * @brief Nested structures. * @details Recursively holds other PropertyContainers. */ - std::vector any_struct_properties; + std::vector any_struct_properties; /** * @brief Arrays of nested structures. */ - FlatAnyStructArray any_struct_arrays; + FlatAnyStructArray any_struct_arrays; /** * @brief Map properties (Key-Value pairs). */ - std::vector map_properties; + std::vector map_properties; /** * @brief Arrays of Maps. */ - FlatMapArray map_arrays; + FlatMapArray map_arrays; }; /** @@ -465,8 +454,8 @@ namespace VTX * @details Maps string keys to PropertyContainer values. */ struct MapContainer { - std::vector keys; ///< List of keys. - std::vector values; ///< Corresponding list of values. + std::vector keys; ///< List of keys. + std::vector values; ///< Corresponding list of values. }; @@ -479,9 +468,10 @@ namespace VTX * @brief Represents the data payload for a specific grouping (e.g., a Team or Actor list). */ struct Bucket { - std::vector unique_ids; ///< Unique identifiers for the entities in this data block. + std::vector unique_ids; ///< Unique identifiers for the entities in this data block. std::vector entities; ///< The actual properties for each entity/ID. - std::vector type_ranges; //< Ranges to know were a type starts and ends, assumes entites are ordered by type + std::vector + type_ranges; //< Ranges to know were a type starts and ends, assumes entites are ordered by type std::span GetEntitiesOfType(int32_t typeId) const { if (typeId >= 0 && typeId < type_ranges.size()) { @@ -503,15 +493,11 @@ namespace VTX * @brief Represents a single simulation frame or tick. */ struct Frame { - Frame() - { - } + Frame() {} - Bucket& CreateBucket(const std::string& name) - { + Bucket& CreateBucket(const std::string& name) { auto it = bucket_map.find(name); - if (it != bucket_map.end()) - { + if (it != bucket_map.end()) { return buckets[it->second]; } @@ -520,23 +506,15 @@ namespace VTX return buckets.back(); } - Bucket& GetBucket(const std::string& name) - { - return CreateBucket(name); - } + Bucket& GetBucket(const std::string& name) { return CreateBucket(name); } - Bucket& GetBucket(int32_t bucket_index) - { - return buckets.at(bucket_index); - } + Bucket& GetBucket(int32_t bucket_index) { return buckets.at(bucket_index); } - const Bucket& GetBucket(const std::string& name) const - { - static const Bucket Dummy{}; + const Bucket& GetBucket(const std::string& name) const { + static const Bucket Dummy {}; auto it = bucket_map.find(name); - if (it != bucket_map.end()) - { + if (it != bucket_map.end()) { return buckets[it->second]; } return Dummy; @@ -554,9 +532,9 @@ namespace VTX * @brief Versioning information for the replay file. */ struct VersionInfo { - uint32_t format_major = 1; ///< Major binary format version. - uint32_t format_minor = 2; ///< Minor binary format version. - uint32_t schema_version = 3; ///< Logic schema version (gameplay data structure). + uint32_t format_major = 1; ///< Major binary format version. + uint32_t format_minor = 2; ///< Minor binary format version. + uint32_t schema_version = 3; ///< Logic schema version (gameplay data structure). }; /** @@ -565,12 +543,12 @@ namespace VTX * `PropertyContainer` vectors (e.g., int32_tProperties[5]). */ struct PropertySchema { - std::unordered_map bool_mapping; ///< Name -> Index for Bool. - std::unordered_map int_mapping; ///< Name -> Index for int32_t. - std::unordered_map int64_mapping; ///< Name -> Index for int64_t. - std::unordered_map float_mapping; ///< Name -> Index for Float. - std::unordered_map double_mapping; ///< Name -> Index for Double. - std::unordered_map string_mapping; ///< Name -> Index for String. + std::unordered_map bool_mapping; ///< Name -> Index for Bool. + std::unordered_map int_mapping; ///< Name -> Index for int32_t. + std::unordered_map int64_mapping; ///< Name -> Index for int64_t. + std::unordered_map float_mapping; ///< Name -> Index for Float. + std::unordered_map double_mapping; ///< Name -> Index for Double. + std::unordered_map string_mapping; ///< Name -> Index for String. std::unordered_map vector_mapping; ///< Name -> Index for Vector. std::unordered_map quat_mapping; ///< Name -> Index for Quat. @@ -578,12 +556,11 @@ namespace VTX std::unordered_map range_mapping; ///< Name -> Index for FloatRange. }; - struct ContextualSchema - { + struct ContextualSchema { std::string data_identifier; - int32_t data_version; // 0 + int32_t data_version; // 0 std::string data_version_string; //data_version 0 = 15.23 prod - std::string property_mapping; //This is the full schema in json format + std::string property_mapping; //This is the full schema in json format }; /** @@ -591,11 +568,11 @@ namespace VTX * @details Used for UI navigation and scrubbing. */ struct TimelineEvent { - float game_time = 0.0f; ///< Time in seconds when the event occurred. - std::string event_type; ///< type_ category (e.g., "Kill"). - std::string label; ///< Display label (e.g., "Player A killed Player B"). - Vector location; ///< 3D location of the event. - std::string entity_unique_id; ///< ID of the primary entity involved. + float game_time = 0.0f; ///< Time in seconds when the event occurred. + std::string event_type; ///< type_ category (e.g., "Kill"). + std::string label; ///< Display label (e.g., "Player A killed Player B"). + Vector location; ///< 3D location of the event. + std::string entity_unique_id; ///< ID of the primary entity involved. }; @@ -603,11 +580,11 @@ namespace VTX * @brief Header structure located at the beginning of a VTX file. */ struct FileHeader { - VersionInfo version; ///< Versioning info. - PropertySchema prop_schema; ///< Schema for property lookup. + VersionInfo version; ///< Versioning info. + PropertySchema prop_schema; ///< Schema for property lookup. ContextualSchema contextual_schema; - std::string replay_uuid; ///< Unique ID for the replay session. - std::string replay_name; ///< Display name of the replay. + std::string replay_uuid; ///< Unique ID for the replay session. + std::string replay_name; ///< Display name of the replay. int64_t recorded_utc_timestamp = 0; ///< UTC Timestamp of recording start. std::string custom_json_metadata; ///< Arbitrary JSON string for extra metadata. @@ -618,10 +595,10 @@ namespace VTX * @details Frames are grouped into chunks to allow for compression and partial loading. */ struct Chunk { - int32_t chunk_index = 0; ///< Sequential index of the chunk. - bool is_compressed = false; ///< Flag indicating if `compressed_frames` contains valid data. + int32_t chunk_index = 0; ///< Sequential index of the chunk. + bool is_compressed = false; ///< Flag indicating if `compressed_frames` contains valid data. - std::vector frames; ///< Raw frames (if not compressed / after decompression). + std::vector frames; ///< Raw frames (if not compressed / after decompression). std::vector compressed_frames; ///< Compressed binary blob of frames. }; @@ -629,12 +606,12 @@ namespace VTX * @brief Entry in the seek table for random access. */ struct ChunkIndexEntry { - int32_t chunk_index = 0; ///< Index of the chunk. - int32_t start_frame = 0; ///< First frame number in this chunk. - int32_t end_frame = 0; ///< Last frame number in this chunk. + int32_t chunk_index = 0; ///< Index of the chunk. + int32_t start_frame = 0; ///< First frame number in this chunk. + int32_t end_frame = 0; ///< Last frame number in this chunk. - uint64_t file_offset = 0; ///< Byte offset in the file where the chunk begins. - uint32_t chunk_size_bytes = 0;///< Size of the chunk in bytes. + uint64_t file_offset = 0; ///< Byte offset in the file where the chunk begins. + uint32_t chunk_size_bytes = 0; ///< Size of the chunk in bytes. }; /** @@ -652,24 +629,18 @@ namespace VTX * @details Contains the index and summary data needed to navigate the file efficiently. */ struct FileFooter { - int32_t total_frames = 0; ///< Total number of frames in the replay. - float duration_seconds = 0.0f; ///< Total duration in seconds. + int32_t total_frames = 0; ///< Total number of frames in the replay. + float duration_seconds = 0.0f; ///< Total duration in seconds. - ReplayTimeData times; ///< Detailed time data. + ReplayTimeData times; ///< Detailed time data. std::vector chunk_index; ///< Seek table. std::vector events; ///< List of significant events. - uint64_t payload_checksum = 0; ///< Integrity checksum of the payload. + uint64_t payload_checksum = 0; ///< Integrity checksum of the payload. }; - namespace GameTime - { - enum class EGameTimeType : uint8_t { - None, - OnlyGameTime, - OnlyCreatedUtc, - Both - }; + namespace GameTime { + enum class EGameTimeType : uint8_t { None, OnlyGameTime, OnlyCreatedUtc, Both }; enum class EFilterType : uint8_t { None, @@ -677,8 +648,7 @@ namespace VTX OnlyDecreasing, }; - struct GameTimeRegister - { + struct GameTimeRegister { std::optional game_time = std::nullopt; std::optional created_utc_time = std::nullopt; EFilterType FrameFilterType = EFilterType::None; @@ -688,16 +658,9 @@ namespace VTX using int32 = std::int32_t; constexpr int64 TICKS_PER_SECOND = 10'000'000; - enum class GameTimeType - { - None, - OnlyGameTime, - OnlyCreatedUtc, - Both - }; + enum class GameTimeType { None, OnlyGameTime, OnlyCreatedUtc, Both }; - struct VTXGameTimes - { + struct VTXGameTimes { struct StateSnapshot { size_t game_time_size = 0; size_t created_utc_size = 0; @@ -712,16 +675,14 @@ namespace VTX StateSnapshot current_snapshot; void CreateSnapshot() { - current_snapshot = { - game_time_.size(), - created_utc_.size(), - timeline_gaps_.size(), - game_segments_.size(), - offset_, - type_, - add_used_, - resolve_success_ - }; + current_snapshot = {game_time_.size(), + created_utc_.size(), + timeline_gaps_.size(), + game_segments_.size(), + offset_, + type_, + add_used_, + resolve_success_}; } void Rollback() { @@ -744,37 +705,38 @@ namespace VTX // Initialising it to system_clock::now() here used to reject any // replay whose timestamps predate the writer process. VTXGameTimes() - : fps_(0), fps_inverse_(0), is_increasing_(true), start_utc_(0), offset_(0), - type_(EGameTimeType::None), add_used_(false), resolve_success_(false), chunk_start_index_(0) - { + : fps_(0) + , fps_inverse_(0) + , is_increasing_(true) + , start_utc_(0) + , offset_(0) + , type_(EGameTimeType::None) + , add_used_(false) + , resolve_success_(false) + , chunk_start_index_(0) { SetFPS(30); } - static int64 GetUtcNowTicks() - { - + static int64 GetUtcNowTicks() { auto now = std::chrono::system_clock::now(); auto duration = now.time_since_epoch(); return std::chrono::duration_cast(duration).count() / 100; } - static int64 SecondsToTicks(float Seconds) - { + static int64 SecondsToTicks(float Seconds) { // Simulates FTimespan::FromSeconds(Seconds).GetTicks() return static_cast(Seconds * TICKS_PER_SECOND); } - static std::string TicksToString(int64 Ticks) - { + static std::string TicksToString(int64 Ticks) { // Simulates FTimespan(...).ToString() double seconds = static_cast(Ticks) / TICKS_PER_SECOND; return std::to_string(seconds) + "s"; } - void CopyFrom(const VTXGameTimes& GameTimes) - { + void CopyFrom(const VTXGameTimes& GameTimes) { game_time_ = GameTimes.GetLastChunkGameTime(); created_utc_ = GameTimes.GetLastChunkCreatedUtc(); receive_utc_ = GameTimes.GetLastChunkReceiveUtc(); @@ -784,8 +746,7 @@ namespace VTX game_segments_ = GameTimes.GetLastChunkGameSegments(); } - void Clear() - { + void Clear() { game_time_.clear(); created_utc_.clear(); receive_utc_.clear(); @@ -793,7 +754,7 @@ namespace VTX sorted_game_time_indexes_.clear(); timeline_gaps_.clear(); game_segments_.clear(); - start_utc_ = 0; // set when the first real frame arrives + start_utc_ = 0; // set when the first real frame arrives offset_ = 0; type_ = EGameTimeType::None; SetFPS(30); @@ -803,35 +764,38 @@ namespace VTX resolve_success_ = false; } - void Setup(const float InFPS, const bool InIsIncreasing, const int64 InStartUtc = 0) - { + void Setup(const float InFPS, const bool InIsIncreasing, const int64 InStartUtc = 0) { SetFPS(InFPS); is_increasing_ = InIsIncreasing; - if (InStartUtc == 0) - { + if (InStartUtc == 0) { start_utc_ = GetUtcNowTicks(); - } - else - { + } else { start_utc_ = InStartUtc; } } - void InsertLiveChunkTimes(const VTXGameTimes& NewChunkGameTime) - { - game_time_.insert(game_time_.end(), NewChunkGameTime.game_time_.begin(), NewChunkGameTime.game_time_.end()); - created_utc_.insert(created_utc_.end(), NewChunkGameTime.created_utc_.begin(), NewChunkGameTime.created_utc_.end()); - receive_utc_.insert(receive_utc_.end(), NewChunkGameTime.receive_utc_.begin(), NewChunkGameTime.receive_utc_.end()); - game_times_sorted_as_ticks_.insert(game_times_sorted_as_ticks_.end(), NewChunkGameTime.game_times_sorted_as_ticks_.begin(), NewChunkGameTime.game_times_sorted_as_ticks_.end()); - sorted_game_time_indexes_.insert(sorted_game_time_indexes_.end(), NewChunkGameTime.sorted_game_time_indexes_.begin(), NewChunkGameTime.sorted_game_time_indexes_.end()); - timeline_gaps_.insert(timeline_gaps_.end(), NewChunkGameTime.timeline_gaps_.begin(), NewChunkGameTime.timeline_gaps_.end()); - game_segments_.insert(game_segments_.end(), NewChunkGameTime.game_segments_.begin(), NewChunkGameTime.game_segments_.end()); + void InsertLiveChunkTimes(const VTXGameTimes& NewChunkGameTime) { + game_time_.insert(game_time_.end(), NewChunkGameTime.game_time_.begin(), + NewChunkGameTime.game_time_.end()); + created_utc_.insert(created_utc_.end(), NewChunkGameTime.created_utc_.begin(), + NewChunkGameTime.created_utc_.end()); + receive_utc_.insert(receive_utc_.end(), NewChunkGameTime.receive_utc_.begin(), + NewChunkGameTime.receive_utc_.end()); + game_times_sorted_as_ticks_.insert(game_times_sorted_as_ticks_.end(), + NewChunkGameTime.game_times_sorted_as_ticks_.begin(), + NewChunkGameTime.game_times_sorted_as_ticks_.end()); + sorted_game_time_indexes_.insert(sorted_game_time_indexes_.end(), + NewChunkGameTime.sorted_game_time_indexes_.begin(), + NewChunkGameTime.sorted_game_time_indexes_.end()); + timeline_gaps_.insert(timeline_gaps_.end(), NewChunkGameTime.timeline_gaps_.begin(), + NewChunkGameTime.timeline_gaps_.end()); + game_segments_.insert(game_segments_.end(), NewChunkGameTime.game_segments_.begin(), + NewChunkGameTime.game_segments_.end()); } - bool AddTimeRegistry(const GameTimeRegister& time_registry) - { + bool AddTimeRegistry(const GameTimeRegister& time_registry) { // UTC regression check only makes sense once we already have a // prior frame. Comparing against start_utc_ (system_clock::now // at construction time) was wrong -- it rejected valid @@ -840,13 +804,12 @@ namespace VTX // fixes. if (time_registry.created_utc_time.has_value() && !created_utc_.empty() && *time_registry.created_utc_time <= created_utc_.back()) { - VTX_WARN("CreatedUTC {} (new) is <= {} (last)", - *time_registry.created_utc_time, created_utc_.back()); + VTX_WARN("CreatedUTC {} (new) is <= {} (last)", *time_registry.created_utc_time, + created_utc_.back()); return false; } - if (time_registry.game_time.has_value()){ - + if (time_registry.game_time.has_value()) { // Monotonicity filters only apply once we already have a // prior frame -- the first frame is always accepted, // otherwise a replay that legitimately starts at t=0 would @@ -856,13 +819,15 @@ namespace VTX switch (time_registry.FrameFilterType) { case EFilterType::OnlyIncreasing: if (has_prior_game_time && time_registry.game_time <= GetLastGameTimeSeconds()) { - VTX_WARN("OnlyIncreasing filter rejected game_time {} (new) <= {} (last)", *time_registry.game_time, GetLastGameTimeSeconds()); + VTX_WARN("OnlyIncreasing filter rejected game_time {} (new) <= {} (last)", + *time_registry.game_time, GetLastGameTimeSeconds()); return false; } break; case EFilterType::OnlyDecreasing: if (has_prior_game_time && time_registry.game_time >= GetLastGameTimeSeconds()) { - VTX_WARN("OnlyDecreasing filter rejected game_time {} (new) >= {} (last)", *time_registry.game_time, GetLastGameTimeSeconds()); + VTX_WARN("OnlyDecreasing filter rejected game_time {} (new) >= {} (last)", + *time_registry.game_time, GetLastGameTimeSeconds()); return false; } break; @@ -874,20 +839,16 @@ namespace VTX if (time_registry.game_time.has_value() && time_registry.created_utc_time.has_value()) { return AddBoth(*time_registry.game_time, *time_registry.created_utc_time); - } - else if (time_registry.created_utc_time.has_value()) { + } else if (time_registry.created_utc_time.has_value()) { return AddCreatedUtc(*time_registry.created_utc_time); - } - else if (time_registry.game_time.has_value()) { + } else if (time_registry.game_time.has_value()) { return AddGameTime(*time_registry.game_time); } return true; } - bool AddAll(const float InGameTime, const int64 InCreatedUtc, const int64 InReceiveUtc) - { - if (add_used_) - { + bool AddAll(const float InGameTime, const int64 InCreatedUtc, const int64 InReceiveUtc) { + if (add_used_) { VTX_ERROR("Another Add method has already been used for this frame!"); return false; } @@ -901,10 +862,8 @@ namespace VTX return true; } - bool AddBoth(const float InGameTime, const int64 InCreatedUtc) - { - if (add_used_) - { + bool AddBoth(const float InGameTime, const int64 InCreatedUtc) { + if (add_used_) { VTX_ERROR("Another Add method has already been used for this frame!"); return false; } @@ -917,10 +876,8 @@ namespace VTX return true; } - bool AddGameTime(const float InGameTime) - { - if (add_used_) - { + bool AddGameTime(const float InGameTime) { + if (add_used_) { VTX_ERROR("Another Add method has already been used for this frame!"); return false; } @@ -932,10 +889,8 @@ namespace VTX return true; } - bool AddCreatedUtc(const int64 InCreatedUtc) - { - if (add_used_) - { + bool AddCreatedUtc(const int64 InCreatedUtc) { + if (add_used_) { VTX_ERROR("Another Add method has already been used for this frame!"); return false; } @@ -947,126 +902,106 @@ namespace VTX return true; } - bool ResolveGameTimes(const int32 FrameAmountSoFar) - { + bool ResolveGameTimes(const int32 FrameAmountSoFar) { add_used_ = false; resolve_success_ = Resolve(FrameAmountSoFar); return resolve_success_; } - void ManuallyMarkGameSegmentStart(const int32 FrameNumber) - { - game_segments_.push_back(FrameNumber); - } + void ManuallyMarkGameSegmentStart(const int32 FrameNumber) { game_segments_.push_back(FrameNumber); } - void ManuallyMarkGameSegmentStart() - { - game_segments_.push_back(GetFrameNumber()); - } + void ManuallyMarkGameSegmentStart() { game_segments_.push_back(GetFrameNumber()); } - bool IsEmpty() const - { - return game_time_.empty() && created_utc_.empty() && receive_utc_.empty(); - } + bool IsEmpty() const { return game_time_.empty() && created_utc_.empty() && receive_utc_.empty(); } - int64 LastGameTime() const - { - if (!game_time_.empty()) - { + int64 LastGameTime() const { + if (!game_time_.empty()) { return game_time_.back(); } return 0; } - double GetLastGameTimeSeconds() const - { + double GetLastGameTimeSeconds() const { const int64 Time = LastGameTime(); return static_cast(Time) / static_cast(TICKS_PER_SECOND); } - int64 FirstGameTime() const - { - if (!game_time_.empty()) - { + int64 FirstGameTime() const { + if (!game_time_.empty()) { return game_time_.front(); } return 0; } - double FirstGameTimeSeconds() const - { + double FirstGameTimeSeconds() const { const int64 Time = FirstGameTime(); return static_cast(Time) / static_cast(TICKS_PER_SECOND); } - int64 LastCreatedUtc() const - { - if (!created_utc_.empty()) - { + int64 LastCreatedUtc() const { + if (!created_utc_.empty()) { return created_utc_.back(); } return start_utc_; } - int64 StartUtc() const - { - return start_utc_; - } + int64 StartUtc() const { return start_utc_; } - int64 FirstCreatedUtc() const - { - if (!created_utc_.empty()) - { + int64 FirstCreatedUtc() const { + if (!created_utc_.empty()) { return created_utc_.front(); } return start_utc_; } - float GetDuration() const - { + float GetDuration() const { return static_cast(LastCreatedUtc() - FirstCreatedUtc()) / static_cast(TICKS_PER_SECOND); } - void DebugPrintGameTimes() - { - for (size_t i = 0; i < game_time_.size(); ++i) - { - VTX_DEBUG("Frame {}: game_time_ = {}, CreatedUTC = {}", i, TicksToString(game_time_[i]), TicksToString(created_utc_[i])); + void DebugPrintGameTimes() { + for (size_t i = 0; i < game_time_.size(); ++i) { + VTX_DEBUG("Frame {}: game_time_ = {}, CreatedUTC = {}", i, TicksToString(game_time_[i]), + TicksToString(created_utc_[i])); } } - void UpdateChunkStartIndex() - { - chunk_start_index_ = GetFrameNumber(); - } + void UpdateChunkStartIndex() { chunk_start_index_ = GetFrameNumber(); } // Getters - const std::vector& GetGameTime() const { return game_time_;} - const std::vector& GetCreatedUtc() const { return created_utc_;} - const std::vector& GetReceiveUtc() const { return receive_utc_;} - const std::vector& GetSortedGameTimeIndexes() const { return sorted_game_time_indexes_;} - const std::vector& GetGameTimesSortedAsTicks() const { return game_times_sorted_as_ticks_;} - const std::vector& GetTimelineGaps() const { return timeline_gaps_;} - const std::vector& GetGameSegments() const { return game_segments_;} + const std::vector& GetGameTime() const { return game_time_; } + const std::vector& GetCreatedUtc() const { return created_utc_; } + const std::vector& GetReceiveUtc() const { return receive_utc_; } + const std::vector& GetSortedGameTimeIndexes() const { return sorted_game_time_indexes_; } + const std::vector& GetGameTimesSortedAsTicks() const { return game_times_sorted_as_ticks_; } + const std::vector& GetTimelineGaps() const { return timeline_gaps_; } + const std::vector& GetGameSegments() const { return game_segments_; } // Chunk Getters std::vector GetLastChunkGameTime() const { return GetLastChunk(game_time_); } std::vector GetLastChunkCreatedUtc() const { return GetLastChunk(created_utc_); } std::vector GetLastChunkReceiveUtc() const { return GetLastChunk(receive_utc_); } - std::vector GetLastChunkSortedGameTimeIndexes() const { return GetLastChunk(sorted_game_time_indexes_); } - std::vector GetLastChunkGameTimesSortedAsTicks() const { return GetLastChunk(game_times_sorted_as_ticks_); } + std::vector GetLastChunkSortedGameTimeIndexes() const { + return GetLastChunk(sorted_game_time_indexes_); + } + std::vector GetLastChunkGameTimesSortedAsTicks() const { + return GetLastChunk(game_times_sorted_as_ticks_); + } std::vector GetLastChunkTimelineGaps() const { return GetLastChunkPartial(timeline_gaps_); } std::vector GetLastChunkGameSegments() const { return GetLastChunkPartial(game_segments_); } void SetGameTime(std::vector times) { game_time_ = std::move(times); } void SetCreatedUtc(std::vector utc) { created_utc_ = std::move(utc); } void SetReceiveUtc(std::vector utc) { receive_utc_ = std::move(utc); } - void SetGameTimesSortedAsTicks(std::vector ticks) { game_times_sorted_as_ticks_ = std::move(ticks); } - void SetSortedGameTimeIndexes(std::vector indexes) { sorted_game_time_indexes_ = std::move(indexes); } + void SetGameTimesSortedAsTicks(std::vector ticks) { + game_times_sorted_as_ticks_ = std::move(ticks); + } + void SetSortedGameTimeIndexes(std::vector indexes) { + sorted_game_time_indexes_ = std::move(indexes); + } void SetTimelineGaps(std::vector gaps) { timeline_gaps_ = std::move(gaps); } void SetGameSegments(std::vector segments) { game_segments_ = std::move(segments); } - private: + private: std::vector game_time_; std::vector created_utc_; std::vector receive_utc_; @@ -1085,21 +1020,17 @@ namespace VTX bool resolve_success_; int32 chunk_start_index_; - void SetFPS(const float InFPS) - { + void SetFPS(const float InFPS) { fps_ = InFPS; fps_inverse_ = static_cast((1.0f / fps_) * TICKS_PER_SECOND); } - int32 GetFrameNumber() const - { + int32 GetFrameNumber() const { return static_cast(std::max({game_time_.size(), created_utc_.size(), receive_utc_.size()})); } - bool Resolve(const int32 FrameAmountSoFar) - { - if(FrameAmountSoFar <= 0) - { + bool Resolve(const int32 FrameAmountSoFar) { + if (FrameAmountSoFar <= 0) { VTX_ERROR("Quitting because there are 0 frames while trying to resolve GameTimes!"); return false; } @@ -1116,13 +1047,12 @@ namespace VTX break; case EGameTimeType::Both: // Dont do anything. - break; + break; } - if (game_time_.size() == created_utc_.size() && game_time_.size() == static_cast(FrameAmountSoFar)) - { - if (!game_time_.empty()) - { + if (game_time_.size() == created_utc_.size() && + game_time_.size() == static_cast(FrameAmountSoFar)) { + if (!game_time_.empty()) { SortGameTime(); DetectGap(); DetectGameSegment(); @@ -1133,52 +1063,36 @@ namespace VTX return false; } - void FakeBothTimesFromFPS() - { - if (created_utc_.empty()) - { + void FakeBothTimesFromFPS() { + if (created_utc_.empty()) { created_utc_.push_back(start_utc_); game_time_.push_back(0); - } - else - { + } else { game_time_.push_back(game_time_.back() + fps_inverse_); created_utc_.push_back(created_utc_.back() + fps_inverse_); } } - void FakeGameTimeFromUtc() - { - game_time_.push_back(created_utc_.back() - created_utc_[0]); - } + void FakeGameTimeFromUtc() { game_time_.push_back(created_utc_.back() - created_utc_[0]); } - void FakeUtcFromGameTime() - { + void FakeUtcFromGameTime() { int64 FakedUtc; const int64 RawTicks = start_utc_ + game_time_.back(); - if (created_utc_.empty()) - { + if (created_utc_.empty()) { FakedUtc = RawTicks; - } - else - { + } else { const int64 LastFakeUtc = created_utc_.back(); FakedUtc = RawTicks + offset_; - if (is_increasing_) - { - if (FakedUtc <= LastFakeUtc) - { + if (is_increasing_) { + if (FakedUtc <= LastFakeUtc) { const int64 Bump = LastFakeUtc + fps_inverse_ - FakedUtc; offset_ += Bump; FakedUtc = RawTicks + offset_; } - } - else - { - if (FakedUtc >= LastFakeUtc) - { + } else { + if (FakedUtc >= LastFakeUtc) { const int64 Bump = LastFakeUtc - fps_inverse_ - FakedUtc; offset_ += Bump; FakedUtc = RawTicks + offset_; @@ -1189,82 +1103,75 @@ namespace VTX created_utc_.push_back(FakedUtc); } - void SortGameTime() - { + void SortGameTime() { const int32 NewIdx = static_cast(game_time_.size()) - 1; const int64 NewTime = game_time_.back(); - auto it = std::lower_bound(game_times_sorted_as_ticks_.begin(), game_times_sorted_as_ticks_.end(), NewTime); + auto it = + std::lower_bound(game_times_sorted_as_ticks_.begin(), game_times_sorted_as_ticks_.end(), NewTime); auto insertPos = std::distance(game_times_sorted_as_ticks_.begin(), it); sorted_game_time_indexes_.insert(sorted_game_time_indexes_.begin() + insertPos, NewIdx); game_times_sorted_as_ticks_.insert(it, NewTime); } - void DetectGap() - { - if (fps_ > 0 && created_utc_.size() > 1) - { + void DetectGap() { + if (fps_ > 0 && created_utc_.size() > 1) { const int64 Threshold = 3 * fps_inverse_; int64 last = created_utc_.back(); int64 prev = created_utc_[created_utc_.size() - 2]; - if ((last - prev) > Threshold) - { + if ((last - prev) > Threshold) { timeline_gaps_.push_back(GetFrameNumber()); } } } - void DetectGameSegment() - { + void DetectGameSegment() { int32 currentFrame = GetFrameNumber(); bool bContains = false; - for(int32 val : game_segments_) { if(val == currentFrame) { bContains = true; break; } } + for (int32 val : game_segments_) { + if (val == currentFrame) { + bContains = true; + break; + } + } - if (bContains) - { + if (bContains) { return; } - if(game_time_.size() > 1) - { + if (game_time_.size() > 1) { const int64 Delta = game_time_.back() - game_time_[game_time_.size() - 2]; - if ((is_increasing_ && Delta < 0) || (!is_increasing_ && Delta > 0)) - { + if ((is_increasing_ && Delta < 0) || (!is_increasing_ && Delta > 0)) { game_segments_.push_back(GetFrameNumber()); } } } - std::vector GetLastChunkPartial(const std::vector& InArray) const - { + std::vector GetLastChunkPartial(const std::vector& InArray) const { std::vector Result; - for (const int32 Elem : InArray) - { - if (Elem > chunk_start_index_) - { + for (const int32 Elem : InArray) { + if (Elem > chunk_start_index_) { Result.push_back(Elem); } } return Result; } - template - std::vector GetLastChunk(const std::vector& InArray) const - { + template + std::vector GetLastChunk(const std::vector& InArray) const { const int32 Count = static_cast(InArray.size()) - chunk_start_index_; std::vector Result; - if (Count > 0) - { + if (Count > 0) { Result.reserve(Count); Result.insert(Result.end(), InArray.begin() + chunk_start_index_, InArray.end()); } return Result; } }; - } + } // namespace GameTime struct SessionConfig { std::string replay_name; @@ -1274,9 +1181,9 @@ namespace VTX }; struct SessionFooter { - SessionFooter() : total_frames(0), duration_seconds(0) - { - } + SessionFooter() + : total_frames(0) + , duration_seconds(0) {} int32_t total_frames; double duration_seconds; @@ -1288,9 +1195,12 @@ namespace VTX }; struct ChunkIndexData { - ChunkIndexData() : chunk_index(0), file_offset(0), chunk_size_bytes(0), start_frame(0), end_frame(0) - { - } + ChunkIndexData() + : chunk_index(0) + , file_offset(0) + , chunk_size_bytes(0) + , start_frame(0) + , end_frame(0) {} int32_t chunk_index; int64_t file_offset; @@ -1298,4 +1208,4 @@ namespace VTX int32_t start_frame; int32_t end_frame; }; -} +} // namespace VTX diff --git a/sdk/include/vtx/common/vtx_types_helpers.h b/sdk/include/vtx/common/vtx_types_helpers.h index c3cc0e8..ee90450 100644 --- a/sdk/include/vtx/common/vtx_types_helpers.h +++ b/sdk/include/vtx/common/vtx_types_helpers.h @@ -26,91 +26,104 @@ namespace VTX { * @param schema The schema definition for the structure to be held. */ inline void PreparePropertyContainer(PropertyContainer& container, const SchemaStruct& schema) { - auto GetNeeded = [&](FieldType type) -> int32_t { size_t typeIdx = static_cast(type); return (typeIdx < schema.type_max_indices.size()) ? schema.type_max_indices[typeIdx] : 0; }; - - if (int32_t n = GetNeeded(FieldType::Bool)) container.bool_properties.resize(n); - if (int32_t n = GetNeeded(FieldType::Int32)) container.int32_properties.resize(n); - if (int32_t n = GetNeeded(FieldType::Int64)) container.int64_properties.resize(n); - if (int32_t n = GetNeeded(FieldType::Float)) container.float_properties.resize(n); - if (int32_t n = GetNeeded(FieldType::Double)) container.double_properties.resize(n); - if (int32_t n = GetNeeded(FieldType::String)) container.string_properties.resize(n); - - if (int32_t n = GetNeeded(FieldType::Vector)) container.vector_properties.resize(n); - if (int32_t n = GetNeeded(FieldType::Quat)) container.quat_properties.resize(n); - if (int32_t n = GetNeeded(FieldType::Transform)) container.transform_properties.resize(n); - if (int32_t n = GetNeeded(FieldType::FloatRange)) container.range_properties.resize(n); - - if (int32_t n = GetNeeded(FieldType::Struct)) container.any_struct_properties.resize(n); + + if (int32_t n = GetNeeded(FieldType::Bool)) + container.bool_properties.resize(n); + if (int32_t n = GetNeeded(FieldType::Int32)) + container.int32_properties.resize(n); + if (int32_t n = GetNeeded(FieldType::Int64)) + container.int64_properties.resize(n); + if (int32_t n = GetNeeded(FieldType::Float)) + container.float_properties.resize(n); + if (int32_t n = GetNeeded(FieldType::Double)) + container.double_properties.resize(n); + if (int32_t n = GetNeeded(FieldType::String)) + container.string_properties.resize(n); + + if (int32_t n = GetNeeded(FieldType::Vector)) + container.vector_properties.resize(n); + if (int32_t n = GetNeeded(FieldType::Quat)) + container.quat_properties.resize(n); + if (int32_t n = GetNeeded(FieldType::Transform)) + container.transform_properties.resize(n); + if (int32_t n = GetNeeded(FieldType::FloatRange)) + container.range_properties.resize(n); + + if (int32_t n = GetNeeded(FieldType::Struct)) + container.any_struct_properties.resize(n); } - - inline uint64_t CalculateContainerHash(const PropertyContainer& container) - { + + inline uint64_t CalculateContainerHash(const PropertyContainer& container) { thread_local XXH_INLINE_XXH3_state_t* state = XXH3_createState(); XXH3_64bits_reset(state); - - auto hash_update = [&](const void*data, size_t size) - { - if (size > 0 && data != nullptr ) - { + + auto hash_update = [&](const void* data, size_t size) { + if (size > 0 && data != nullptr) { XXH3_64bits_update(state, data, size); } - }; - - hash_update(&container.entity_type_id,sizeof(container.entity_type_id)); - + + hash_update(&container.entity_type_id, sizeof(container.entity_type_id)); + //special case, std::vector does not have .data(), fast loop instead for (const bool& b : container.bool_properties) { uint8_t val = b ? 1 : 0; hash_update(&val, sizeof(val)); } - + hash_update(container.int32_properties.data(), container.int32_properties.size() * sizeof(int32_t)); hash_update(container.int64_properties.data(), container.int64_properties.size() * sizeof(int64_t)); hash_update(container.float_properties.data(), container.float_properties.size() * sizeof(float)); hash_update(container.double_properties.data(), container.double_properties.size() * sizeof(double)); - + for (const auto& str : container.string_properties) { hash_update(str.data(), str.size()); } - - hash_update(container.transform_properties.data(), container.transform_properties.size() * sizeof(Transform)); + + hash_update(container.transform_properties.data(), + container.transform_properties.size() * sizeof(Transform)); hash_update(container.vector_properties.data(), container.vector_properties.size() * sizeof(Vector)); hash_update(container.quat_properties.data(), container.quat_properties.size() * sizeof(Quat)); hash_update(container.range_properties.data(), container.range_properties.size() * sizeof(FloatRange)); - - + + //arrays - + if (!container.byte_array_properties.data.empty()) { - hash_update(container.byte_array_properties.data.data(), container.byte_array_properties.data.size() * sizeof(uint8_t)); - hash_update(container.byte_array_properties.offsets.data(), container.byte_array_properties.offsets.size() * sizeof(uint32_t)); + hash_update(container.byte_array_properties.data.data(), + container.byte_array_properties.data.size() * sizeof(uint8_t)); + hash_update(container.byte_array_properties.offsets.data(), + container.byte_array_properties.offsets.size() * sizeof(uint32_t)); } - + if (!container.int32_arrays.data.empty()) { hash_update(container.int32_arrays.data.data(), container.int32_arrays.data.size() * sizeof(int32_t)); - hash_update(container.int32_arrays.offsets.data(), container.int32_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.int32_arrays.offsets.data(), + container.int32_arrays.offsets.size() * sizeof(uint32_t)); } - + if (!container.int64_arrays.data.empty()) { hash_update(container.int64_arrays.data.data(), container.int64_arrays.data.size() * sizeof(int64_t)); - hash_update(container.int64_arrays.offsets.data(), container.int64_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.int64_arrays.offsets.data(), + container.int64_arrays.offsets.size() * sizeof(uint32_t)); } - + if (!container.float_arrays.data.empty()) { hash_update(container.float_arrays.data.data(), container.float_arrays.data.size() * sizeof(float)); - hash_update(container.float_arrays.offsets.data(), container.float_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.float_arrays.offsets.data(), + container.float_arrays.offsets.size() * sizeof(uint32_t)); } - + if (!container.double_arrays.data.empty()) { hash_update(container.double_arrays.data.data(), container.double_arrays.data.size() * sizeof(double)); - hash_update(container.double_arrays.offsets.data(), container.double_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.double_arrays.offsets.data(), + container.double_arrays.offsets.size() * sizeof(uint32_t)); } - + // Nested structs (scalar) for (const auto& sc : container.any_struct_properties) { uint64_t sub = CalculateContainerHash(sc); @@ -123,7 +136,8 @@ namespace VTX { uint64_t sub = CalculateContainerHash(sc); hash_update(&sub, sizeof(sub)); } - hash_update(container.any_struct_arrays.offsets.data(), container.any_struct_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.any_struct_arrays.offsets.data(), + container.any_struct_arrays.offsets.size() * sizeof(uint32_t)); } // Maps (scalar) @@ -148,45 +162,55 @@ namespace VTX { hash_update(&sub, sizeof(sub)); } } - hash_update(container.map_arrays.offsets.data(), container.map_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.map_arrays.offsets.data(), + container.map_arrays.offsets.size() * sizeof(uint32_t)); } if (!container.vector_arrays.data.empty()) { - hash_update(container.vector_arrays.data.data(), container.vector_arrays.data.size() * sizeof(VTX::Vector)); - hash_update(container.vector_arrays.offsets.data(), container.vector_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.vector_arrays.data.data(), + container.vector_arrays.data.size() * sizeof(VTX::Vector)); + hash_update(container.vector_arrays.offsets.data(), + container.vector_arrays.offsets.size() * sizeof(uint32_t)); } - + if (!container.quat_arrays.data.empty()) { hash_update(container.quat_arrays.data.data(), container.quat_arrays.data.size() * sizeof(VTX::Quat)); - hash_update(container.quat_arrays.offsets.data(), container.quat_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.quat_arrays.offsets.data(), + container.quat_arrays.offsets.size() * sizeof(uint32_t)); } - + if (!container.transform_arrays.data.empty()) { - hash_update(container.transform_arrays.data.data(), container.transform_arrays.data.size() * sizeof(VTX::Transform)); - hash_update(container.transform_arrays.offsets.data(), container.transform_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.transform_arrays.data.data(), + container.transform_arrays.data.size() * sizeof(VTX::Transform)); + hash_update(container.transform_arrays.offsets.data(), + container.transform_arrays.offsets.size() * sizeof(uint32_t)); } - + if (!container.range_arrays.data.empty()) { - hash_update(container.range_arrays.data.data(), container.range_arrays.data.size() * sizeof(VTX::FloatRange)); - hash_update(container.range_arrays.offsets.data(), container.range_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.range_arrays.data.data(), + container.range_arrays.data.size() * sizeof(VTX::FloatRange)); + hash_update(container.range_arrays.offsets.data(), + container.range_arrays.offsets.size() * sizeof(uint32_t)); } - + if (!container.bool_arrays.data.empty()) { hash_update(container.bool_arrays.data.data(), container.bool_arrays.data.size() * sizeof(uint8_t)); - hash_update(container.bool_arrays.offsets.data(), container.bool_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.bool_arrays.offsets.data(), + container.bool_arrays.offsets.size() * sizeof(uint32_t)); } - + if (!container.string_arrays.data.empty()) { for (const auto& str : container.string_arrays.data) { hash_update(str.data(), str.size()); } - hash_update(container.string_arrays.offsets.data(), container.string_arrays.offsets.size() * sizeof(uint32_t)); + hash_update(container.string_arrays.offsets.data(), + container.string_arrays.offsets.size() * sizeof(uint32_t)); } - - + + return XXH3_64bits_digest(state); } - } + } // namespace Helpers namespace TimeUtils { constexpr int64 TICKS_PER_SECOND = 10'000'000; @@ -195,17 +219,15 @@ namespace VTX { // (Days between 0001 and 1970) * SecondsPerDay * TicksPerSecond constexpr int64 TICKS_AT_UNIX_EPOCH = 621'355'968'000'000'000; - enum class TimeFormat - { - UeUTC, // Ticks (since 0001) - UnixUTC, // Secs since 1970 - UnixMsUTC, // Ms since 1970 - ISO8601, // String date + enum class TimeFormat { + UeUTC, // Ticks (since 0001) + UnixUTC, // Secs since 1970 + UnixMsUTC, // Ms since 1970 + ISO8601, // String date Seconds }; - - inline int64 ConvertToUeTicks(TimeFormat in_time_format, int64 value) - { + + inline int64 ConvertToUeTicks(TimeFormat in_time_format, int64 value) { switch (in_time_format) { case TimeFormat::UeUTC: return value; @@ -216,36 +238,36 @@ namespace VTX { case TimeFormat::ISO8601: case TimeFormat::Seconds: return 0; - default: ; + default:; } return 0; } - inline int64 ConvertToUeTicks(TimeFormat in_time_format,float seconds) - { - if (in_time_format != TimeFormat::Seconds) - { + inline int64 ConvertToUeTicks(TimeFormat in_time_format, float seconds) { + if (in_time_format != TimeFormat::Seconds) { return 0; } - + return seconds * TICKS_PER_SECOND; } - - inline int64 ConvertToUeTicks(TimeFormat in_time_format, const std::string& ISO8601UTC) - { - if (in_time_format != TimeFormat::ISO8601) return 0; - if (ISO8601UTC.empty()) return 0; + + inline int64 ConvertToUeTicks(TimeFormat in_time_format, const std::string& ISO8601UTC) { + if (in_time_format != TimeFormat::ISO8601) + return 0; + if (ISO8601UTC.empty()) + return 0; std::tm tm = {}; std::istringstream ss(ISO8601UTC); ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); - if (ss.fail()) return 0; - - - auto time_point = std::chrono::sys_days{std::chrono::year(tm.tm_year + 1900) / (tm.tm_mon + 1) / tm.tm_mday}; - auto tp_seconds = std::chrono::time_point_cast(time_point) + - std::chrono::hours(tm.tm_hour) + - std::chrono::minutes(tm.tm_min) + + if (ss.fail()) + return 0; + + + auto time_point = + std::chrono::sys_days {std::chrono::year(tm.tm_year + 1900) / (tm.tm_mon + 1) / tm.tm_mday}; + auto tp_seconds = std::chrono::time_point_cast(time_point) + + std::chrono::hours(tm.tm_hour) + std::chrono::minutes(tm.tm_min) + std::chrono::seconds(tm.tm_sec); int64 unix_seconds = tp_seconds.time_since_epoch().count(); @@ -262,53 +284,50 @@ namespace VTX { int32_t milliseconds = 0; }; - inline Duration TicksToDuration(int64 ticks) - { + inline Duration TicksToDuration(int64 ticks) { const int64 total_ms = (ticks + (TICKS_PER_MILLISECOND / 2)) / TICKS_PER_MILLISECOND; Duration d; - d.hours = static_cast(total_ms / 3'600'000); - d.minutes = static_cast((total_ms % 3'600'000) / 60'000); - d.seconds = static_cast((total_ms % 60'000) / 1'000); + d.hours = static_cast(total_ms / 3'600'000); + d.minutes = static_cast((total_ms % 3'600'000) / 60'000); + d.seconds = static_cast((total_ms % 60'000) / 1'000); d.milliseconds = static_cast(total_ms % 1'000); return d; } - inline std::string FormatDuration(int64 ticks) - { + inline std::string FormatDuration(int64 ticks) { auto d = TicksToDuration(ticks); std::ostringstream s; - if (d.hours > 0) s << d.hours << "h "; - if (d.minutes > 0 || d.hours > 0) s << d.minutes << "m "; + if (d.hours > 0) + s << d.hours << "h "; + if (d.minutes > 0 || d.hours > 0) + s << d.minutes << "m "; s << d.seconds << "s " << d.milliseconds << "ms"; return s.str(); } - inline double TicksToSeconds(int64 ticks) - { + inline double TicksToSeconds(int64 ticks) { return static_cast(ticks) / static_cast(TICKS_PER_SECOND); } - inline std::string FormatUtcTicks(int64 ticks) - { + inline std::string FormatUtcTicks(int64 ticks) { // Normalize: if ticks are in UE epoch (since 0001), convert to unix-relative - const int64 unix_ticks = (ticks >= TICKS_AT_UNIX_EPOCH) - ? ticks - TICKS_AT_UNIX_EPOCH - : ticks; + const int64 unix_ticks = (ticks >= TICKS_AT_UNIX_EPOCH) ? ticks - TICKS_AT_UNIX_EPOCH : ticks; const int64 unix_seconds = unix_ticks / TICKS_PER_SECOND; const int32_t ms = static_cast((unix_ticks % TICKS_PER_SECOND) / TICKS_PER_MILLISECOND); const std::time_t time_val = static_cast(unix_seconds); - std::tm utc_time{}; + std::tm utc_time {}; #if defined(_WIN32) - if (gmtime_s(&utc_time, &time_val) != 0) return "Invalid UTC"; + if (gmtime_s(&utc_time, &time_val) != 0) + return "Invalid UTC"; #else - if (gmtime_r(&time_val, &utc_time) == nullptr) return "Invalid UTC"; + if (gmtime_r(&time_val, &utc_time) == nullptr) + return "Invalid UTC"; #endif std::ostringstream s; - s << std::put_time(&utc_time, "%d %b %Y %H:%M:%S") - << "." << std::setw(3) << std::setfill('0') << (ms >= 0 ? ms : ms + 1000) - << " UTC"; + s << std::put_time(&utc_time, "%d %b %Y %H:%M:%S") << "." << std::setw(3) << std::setfill('0') + << (ms >= 0 ? ms : ms + 1000) << " UTC"; return s.str(); } - } -} \ No newline at end of file + } // namespace TimeUtils +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/differ/adapters/flatbuffers/vtx_flatbuffer_view_adapter.h b/sdk/include/vtx/differ/adapters/flatbuffers/vtx_flatbuffer_view_adapter.h index ed32117..d7099cf 100644 --- a/sdk/include/vtx/differ/adapters/flatbuffers/vtx_flatbuffer_view_adapter.h +++ b/sdk/include/vtx/differ/adapters/flatbuffers/vtx_flatbuffer_view_adapter.h @@ -18,11 +18,11 @@ namespace VtxDiff::Flatbuffers { struct CachedField { const reflection::Field* FbField = nullptr; EVTXContainerType VtxType = EVTXContainerType::Unknown; - + reflection::BaseType BaseType = reflection::None; reflection::BaseType ElementType = reflection::None; size_t ByteSize = 0; - size_t ElementByteSize = 0; + size_t ElementByteSize = 0; const reflection::Object* NestedObj = nullptr; const reflection::Field* SoaDataField = nullptr; @@ -37,7 +37,7 @@ namespace VtxDiff::Flatbuffers { std::vector EnumerableFields; std::unordered_map FieldsByName; }; - + class FbSchemaCache { public: void Build(const reflection::Schema* Schema); @@ -45,26 +45,28 @@ namespace VtxDiff::Flatbuffers { const std::vector& GetEnumerableFields(const reflection::Object* Obj) const; const reflection::Schema* RawSchema = nullptr; + private: std::unordered_map ObjectCache; }; - + class FlatbufferViewAdapter { public: FlatbufferViewAdapter() = default; - FlatbufferViewAdapter(const FbSchemaCache* Cache, const reflection::Object* Object, const flatbuffers::Table* TablePtr); - + FlatbufferViewAdapter(const FbSchemaCache* Cache, const reflection::Object* Object, + const flatbuffers::Table* TablePtr); + void Clear() {} void Reset(); bool IsValid() const; - + std::span EnumerateFields() const; std::span GetFieldBytes(const FieldDesc& Fd) const; - + size_t GetArraySize(const FieldDesc& Fd) const; std::span GetArrayElementBytes(const FieldDesc& Fd, size_t Index) const; std::span GetSubArrayBytes(const FieldDesc& Fd, size_t SubIndex) const; - + size_t GetMapSize(const FieldDesc& Fd) const; std::string GetMapKey(const FieldDesc& Fd, size_t I) const; std::string GetScalarFieldString(const std::string& FieldName) const; @@ -75,24 +77,25 @@ namespace VtxDiff::Flatbuffers { FlatbufferViewAdapter GetMapValueAsStruct(const FieldDesc& Fd, size_t I) const; FlatbufferViewAdapter GetFieldByName(const std::string& FieldName) const; - static std::optional CreateRoot( - const FbSchemaCache* Cache, - const char* RootObjectName, const uint8_t* Buffer, size_t BufferSize); + static std::optional CreateRoot(const FbSchemaCache* Cache, const char* RootObjectName, + const uint8_t* Buffer, size_t BufferSize); private: const reflection::Field* FindField(std::string_view FieldName) const; - const uint8_t* GetVectorData(const flatbuffers::Table* Tbl, const reflection::Field* F, size_t& Count, size_t& ElemSize) const; + const uint8_t* GetVectorData(const flatbuffers::Table* Tbl, const reflection::Field* F, size_t& Count, + size_t& ElemSize) const; bool IsFlatArrayType(EVTXContainerType Type) const; private: - const FbSchemaCache* Cache{nullptr}; - const reflection::Object* Object{nullptr}; - const flatbuffers::Table* TablePtr{nullptr}; + const FbSchemaCache* Cache {nullptr}; + const reflection::Object* Object {nullptr}; + const flatbuffers::Table* TablePtr {nullptr}; }; - static_assert(CBinaryNodeView, "FlatbufferViewAdapter does not follow CBinaryNodeView concept!"); - - class FbViewFactory{ + static_assert(CBinaryNodeView, + "FlatbufferViewAdapter does not follow CBinaryNodeView concept!"); + + class FbViewFactory { public: FbViewFactory() = default; @@ -100,13 +103,14 @@ namespace VtxDiff::Flatbuffers { bool InitFromFile(const std::string& Path, const std::string& RootType); bool InitFromMemory(const uint8_t* Data, size_t Size, const std::string& RootType); + private: - const reflection::Schema* Schema{}; + const reflection::Schema* Schema {}; std::string RootName; std::unordered_map SubNames; std::vector SchemaBytes; FbSchemaCache GlobalCache; }; - -} + +} // namespace VtxDiff::Flatbuffers diff --git a/sdk/include/vtx/differ/adapters/protobuff/vtx_proto_verify.h b/sdk/include/vtx/differ/adapters/protobuff/vtx_proto_verify.h index 85051ff..ebcf1a6 100644 --- a/sdk/include/vtx/differ/adapters/protobuff/vtx_proto_verify.h +++ b/sdk/include/vtx/differ/adapters/protobuff/vtx_proto_verify.h @@ -12,19 +12,20 @@ #include "google/protobuf/util/message_differencer.h" -namespace VtxDiff::Protobuf -{ - static bool ProtoEquals(const google::protobuf::Message& A,const google::protobuf::Message& B,std::string* OutDiff) - { - using google::protobuf::util::MessageDifferencer; - MessageDifferencer Diff; - Diff.set_message_field_comparison(MessageDifferencer::EQUIVALENT); - - std::string report; - if (OutDiff) Diff.ReportDifferencesToString(&report); - - const bool Ok = Diff.Compare(A, B); - if (!Ok && OutDiff) *OutDiff = report; - return Ok; - } -} \ No newline at end of file +namespace VtxDiff::Protobuf { + static bool ProtoEquals(const google::protobuf::Message& A, const google::protobuf::Message& B, + std::string* OutDiff) { + using google::protobuf::util::MessageDifferencer; + MessageDifferencer Diff; + Diff.set_message_field_comparison(MessageDifferencer::EQUIVALENT); + + std::string report; + if (OutDiff) + Diff.ReportDifferencesToString(&report); + + const bool Ok = Diff.Compare(A, B); + if (!Ok && OutDiff) + *OutDiff = report; + return Ok; + } +} // namespace VtxDiff::Protobuf \ No newline at end of file diff --git a/sdk/include/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.h b/sdk/include/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.h index 1190e11..27459fa 100644 --- a/sdk/include/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.h +++ b/sdk/include/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.h @@ -21,14 +21,13 @@ namespace VtxDiff::Protobuf { std::string FlatDataField; std::string OffsetsField; }; - - class FProtobufViewAdapter - { + + class FProtobufViewAdapter { public: FProtobufViewAdapter() = default; FProtobufViewAdapter(const google::protobuf::Message* InMsg, bool bOwnsMessage); - + ~FProtobufViewAdapter(); FProtobufViewAdapter(FProtobufViewAdapter&&) noexcept; @@ -37,17 +36,14 @@ namespace VtxDiff::Protobuf { FProtobufViewAdapter(const FProtobufViewAdapter&) = delete; FProtobufViewAdapter& operator=(const FProtobufViewAdapter&) = delete; - static std::optional CreateRoot( - const google::protobuf::DescriptorPool* Pool, - const std::string& RootType, - const uint8_t* Buffer, - size_t Size); + static std::optional CreateRoot(const google::protobuf::DescriptorPool* Pool, + const std::string& RootType, const uint8_t* Buffer, + size_t Size); - static std::optional FromMessage( - const google::protobuf::Message& Message); + static std::optional FromMessage(const google::protobuf::Message& Message); void SetSubArrayNames(std::unordered_map Map); - + void Reset(); bool IsValid() const; @@ -59,7 +55,7 @@ namespace VtxDiff::Protobuf { size_t GetArraySize(const VtxDiff::FieldDesc& Field) const; std::span GetArrayElementBytes(const VtxDiff::FieldDesc& Field, size_t Index) const; std::span GetSubArrayBytes(const VtxDiff::FieldDesc& Field, size_t SubIndex) const; - + size_t GetMapSize(const VtxDiff::FieldDesc& Field) const; std::string GetMapKey(const VtxDiff::FieldDesc& Field, size_t Index) const; @@ -84,7 +80,7 @@ namespace VtxDiff::Protobuf { const google::protobuf::FieldDescriptor* FindField(std::string_view Name) const; bool IsScalar(const google::protobuf::FieldDescriptor* Field) const; EVTXContainerType InferContainerType(const google::protobuf::FieldDescriptor* Field) const; - + std::span Publish(const void* Data, size_t Size) const; std::span Publish(const std::vector& Buffer) const; std::span Publish(std::vector&& Buffer) const; @@ -93,9 +89,7 @@ namespace VtxDiff::Protobuf { static std::unique_ptr CloneMessage(const google::protobuf::Message& Src); public: - inline const google::protobuf::Message* GetMsg() const noexcept { - return Owned ? Owned.get() : External; - } + inline const google::protobuf::Message* GetMsg() const noexcept { return Owned ? Owned.get() : External; } inline const google::protobuf::Descriptor* GetDesc() const noexcept { auto* m = GetMsg(); return m ? m->GetDescriptor() : nullptr; @@ -106,8 +100,9 @@ namespace VtxDiff::Protobuf { } }; - static_assert(CBinaryNodeView, "FProtobufViewAdapter does not follow CBinaryNodeView concept!"); - + static_assert(CBinaryNodeView, + "FProtobufViewAdapter does not follow CBinaryNodeView concept!"); + class PbViewFactory { public: PbViewFactory() = default; diff --git a/sdk/include/vtx/differ/core/interfaces/vtx_binary_view_node.h b/sdk/include/vtx/differ/core/interfaces/vtx_binary_view_node.h index 7cfdbb9..fb65faa 100644 --- a/sdk/include/vtx/differ/core/interfaces/vtx_binary_view_node.h +++ b/sdk/include/vtx/differ/core/interfaces/vtx_binary_view_node.h @@ -2,26 +2,25 @@ namespace VtxDiff { - template - concept CBinaryNodeView = requires(const T& View, const FieldDesc& Fd, size_t Index, const std::string& Str) - { - { View.IsValid() } -> std::same_as; - //{ View.Reset() } -> std::same_as; - - { View.EnumerateFields() } -> std::same_as>; - { View.GetFieldBytes(Fd) } -> std::same_as>; - { View.GetScalarFieldString(Str) } -> std::same_as; - - { View.GetArraySize(Fd) } -> std::same_as; - { View.GetArrayElementBytes(Fd, Index) } -> std::same_as>; - { View.GetSubArrayBytes(Fd, Index) } -> std::same_as>; - - { View.GetMapSize(Fd) } -> std::same_as; - { View.GetMapKey(Fd, Index) } -> std::same_as; - - { View.GetNestedStruct(Fd) } -> std::same_as; - { View.GetArrayElementAsStruct(Fd, Index) } -> std::same_as; - { View.GetMapValueAsStruct(Fd, Index) } -> std::same_as; - { View.GetFieldByName(Str) } -> std::same_as; - }; + template + concept CBinaryNodeView = requires(const T& View, const FieldDesc& Fd, size_t Index, const std::string& Str) { + { View.IsValid() } -> std::same_as; + //{ View.Reset() } -> std::same_as; + + { View.EnumerateFields() } -> std::same_as>; + { View.GetFieldBytes(Fd) } -> std::same_as>; + { View.GetScalarFieldString(Str) } -> std::same_as; + + { View.GetArraySize(Fd) } -> std::same_as; + { View.GetArrayElementBytes(Fd, Index) } -> std::same_as>; + { View.GetSubArrayBytes(Fd, Index) } -> std::same_as>; + + { View.GetMapSize(Fd) } -> std::same_as; + { View.GetMapKey(Fd, Index) } -> std::same_as; + + { View.GetNestedStruct(Fd) } -> std::same_as; + { View.GetArrayElementAsStruct(Fd, Index) } -> std::same_as; + { View.GetMapValueAsStruct(Fd, Index) } -> std::same_as; + { View.GetFieldByName(Str) } -> std::same_as; + }; } // namespace VtxDiff diff --git a/sdk/include/vtx/differ/core/vtx_default_tree_diff.h b/sdk/include/vtx/differ/core/vtx_default_tree_diff.h index b5f2aa9..579a10f 100644 --- a/sdk/include/vtx/differ/core/vtx_default_tree_diff.h +++ b/sdk/include/vtx/differ/core/vtx_default_tree_diff.h @@ -17,708 +17,865 @@ namespace VtxDiff { -inline std::span SafeSubspan(std::span Data, size_t Start, size_t End) { - const size_t Max = Data.size(); - Start = std::min(Start, Max); End = std::min(End, Max); End = std::max(End, Start); - return Data.subspan(Start, End - Start); -} - -inline bool SpanEqual(std::span A, std::span B) { - if (A.size() != B.size()) return false; - if (A.empty()) return true; - return std::memcmp(A.data(), B.data(), A.size()) == 0; -} - -inline bool ReadOffsetU32(std::span Raw, size_t& Out) { - if (Raw.size() != sizeof(uint32_t)) return false; - uint32_t v = 0; std::memcpy(&v, Raw.data(), sizeof(uint32_t)); Out = (size_t)v; return true; -} - -inline bool ReadOffsetAny(std::span Raw, size_t& Out) { - if (Raw.size() == sizeof(uint32_t)) { uint32_t v; std::memcpy(&v, Raw.data(), sizeof(v)); Out = v; return true; } - if (Raw.size() == sizeof(uint64_t)) { uint64_t v; std::memcpy(&v, Raw.data(), sizeof(v)); Out = static_cast(v); return true; } - return false; -} - -inline uint64_t StableHash64(std::string_view s) { - uint64_t idHash64 = XXH3_64bits(s.data(), s.size()); - return idHash64; -} - -inline bool IsAncestorPath(const VtxDiff::DiffIndexPath& A, const VtxDiff::DiffIndexPath& B) { - if (A.size() >= B.size()) return false; - for (size_t i = 0; i < A.size(); ++i) if (A[i] != B[i]) return false; - return true; -} - -template -class DefaultTreeDiff -{ -private: - struct ActorEntry { uint64_t Hash; size_t Index; }; - mutable std::vector ScratchActorsB; - mutable std::vector ScratchMatchedB; - - struct MapEntry { uint64_t Hash; std::string Key; size_t Index; }; - mutable std::vector ScratchMapA; - mutable std::vector ScratchMapB; - - static int32_t HashToPathKey(uint64_t Hash) { - return static_cast(Hash & 0x7FFFFFFF); + inline std::span SafeSubspan(std::span Data, size_t Start, size_t End) { + const size_t Max = Data.size(); + Start = std::min(Start, Max); + End = std::min(End, Max); + End = std::max(End, Start); + return Data.subspan(Start, End - Start); } - std::optional TryGetContentHash(const TNodeView& Node) const - { - const std::string hash_string = Node.GetScalarFieldString("content_hash"); - if (hash_string.empty()) { - return std::nullopt; - } - return Node.GetUint64Field("content_hash"); + inline bool SpanEqual(std::span A, std::span B) { + if (A.size() != B.size()) + return false; + if (A.empty()) + return true; + return std::memcmp(A.data(), B.data(), A.size()) == 0; } - bool HaveMatchingContentHash(const TNodeView& NodeA, const TNodeView& NodeB) const - { - const auto hash_a = TryGetContentHash(NodeA); - const auto hash_b = TryGetContentHash(NodeB); - return hash_a.has_value() && hash_b.has_value() && (*hash_a == *hash_b); + inline bool ReadOffsetU32(std::span Raw, size_t& Out) { + if (Raw.size() != sizeof(uint32_t)) + return false; + uint32_t v = 0; + std::memcpy(&v, Raw.data(), sizeof(uint32_t)); + Out = (size_t)v; + return true; } -public: - DefaultTreeDiff() = default; - ~DefaultTreeDiff() = default; - - PatchIndex ComputeDiff(const TNodeView& NodeA, const TNodeView& NodeB, const DiffOptions& Opt) const - { - PatchIndex Out; - Out.operations.reserve(512); - DiffIndexPath root; + inline bool ReadOffsetAny(std::span Raw, size_t& Out) { + if (Raw.size() == sizeof(uint32_t)) { + uint32_t v; + std::memcpy(&v, Raw.data(), sizeof(v)); + Out = v; + return true; + } + if (Raw.size() == sizeof(uint64_t)) { + uint64_t v; + std::memcpy(&v, Raw.data(), sizeof(v)); + Out = static_cast(v); + return true; + } + return false; + } - if (!NodeA.IsValid() && !NodeB.IsValid()) return Out; - if (!NodeA.IsValid() && NodeB.IsValid()) { Out.operations.push_back({ DiffOperation::Add, EVTXContainerType::Unknown, root }); return Out; } - if (NodeA.IsValid() && !NodeB.IsValid()) { Out.operations.push_back({ DiffOperation::Remove, EVTXContainerType::Unknown, root }); return Out; } + inline uint64_t StableHash64(std::string_view s) { + uint64_t idHash64 = XXH3_64bits(s.data(), s.size()); + return idHash64; + } - std::span fields_a = NodeA.EnumerateFields(); - std::span fields_b = NodeB.EnumerateFields(); + inline bool IsAncestorPath(const VtxDiff::DiffIndexPath& A, const VtxDiff::DiffIndexPath& B) { + if (A.size() >= B.size()) + return false; + for (size_t i = 0; i < A.size(); ++i) + if (A[i] != B[i]) + return false; + return true; + } - const FieldDesc* entities_a = nullptr; - const FieldDesc* entities_b = nullptr; + template + class DefaultTreeDiff { + private: + struct ActorEntry { + uint64_t Hash; + size_t Index; + }; + mutable std::vector ScratchActorsB; + mutable std::vector ScratchMatchedB; - for (const auto& f : fields_a) { if (f.name == "entities") { entities_a = &f; break; } } - for (const auto& f : fields_b) { if (f.name == "entities") { entities_b = &f; break; } } + struct MapEntry { + uint64_t Hash; + std::string Key; + size_t Index; + }; + mutable std::vector ScratchMapA; + mutable std::vector ScratchMapB; - if (entities_a && entities_b) - { - root.push_back(static_cast(entities_a->type)); - DiffActorsArray(NodeA, NodeB, *entities_a, *entities_b, Opt, Out, root); - return Out; - } + static int32_t HashToPathKey(uint64_t Hash) { return static_cast(Hash & 0x7FFFFFFF); } - Diff(NodeA, NodeB, Opt, root, Out); - return Out; - } - - PatchIndex ComputeEntityDiff(const TNodeView& EntityA, const TNodeView& EntityB,const DiffOptions& Opt, DiffIndexPath BasePath = {}) const - { - PatchIndex Out; - Out.operations.reserve(512); - if (!EntityA.IsValid() && !EntityB.IsValid()) return Out; - if (!EntityA.IsValid() && EntityB.IsValid()) { Out.operations.push_back({ DiffOperation::Add, EVTXContainerType::Unknown, BasePath }); return Out; } - if (EntityA.IsValid() && !EntityB.IsValid()) { Out.operations.push_back({ DiffOperation::Remove, EVTXContainerType::Unknown, BasePath }); return Out; } - - if (HaveMatchingContentHash(EntityA, EntityB)) { - return Out; - } - - Diff(EntityA, EntityB, Opt, BasePath, Out); - return Out; - } - - -private: - void Diff(const TNodeView& NodeA, const TNodeView& NodeB, const DiffOptions& Opt, DiffIndexPath& Path, PatchIndex& Out) const - { - const bool validA = NodeA.IsValid(); - const bool validB = NodeB.IsValid(); - - if (!validA && !validB) return; - if (!validA && validB) { Out.operations.push_back({ DiffOperation::Add, EVTXContainerType::Unknown, Path }); return; } - if (validA && !validB) { Out.operations.push_back({ DiffOperation::Remove, EVTXContainerType::Unknown, Path }); return; } - - auto FieldsA = NodeA.EnumerateFields(); - auto FieldsB = NodeB.EnumerateFields(); - - constexpr uint32_t MAX_ENUM_TYPES = 64; - const FieldDesc* ArrA[MAX_ENUM_TYPES] = { nullptr }; - const FieldDesc* ArrB[MAX_ENUM_TYPES] = { nullptr }; - uint32_t maxTypeSeen = 0; - - for (const auto& f : FieldsA) { - if (f.is_actors_field || f.type == EVTXContainerType::Unknown) continue; - uint32_t t = static_cast(f.type); - if (t < MAX_ENUM_TYPES) { ArrA[t] = &f; if (t > maxTypeSeen) maxTypeSeen = t; } - } - for (const auto& f : FieldsB) { - if (f.is_actors_field || f.type == EVTXContainerType::Unknown) continue; - uint32_t t = static_cast(f.type); - if (t < MAX_ENUM_TYPES) { ArrB[t] = &f; if (t > maxTypeSeen) maxTypeSeen = t; } + std::optional TryGetContentHash(const TNodeView& Node) const { + const std::string hash_string = Node.GetScalarFieldString("content_hash"); + if (hash_string.empty()) { + return std::nullopt; + } + return Node.GetUint64Field("content_hash"); } - for (uint32_t t = 0; t <= maxTypeSeen; ++t) - { - const FieldDesc* Fda = ArrA[t]; - const FieldDesc* Fdb = ArrB[t]; - if (!Fda && !Fdb) continue; - - EVTXContainerType T = static_cast(t); - Path.push_back(t); - - if (!Fda && Fdb) { - Out.operations.push_back({ DiffOperation::Add, T, Path }); - } else if (Fda && !Fdb) { - Out.operations.push_back({ DiffOperation::Remove, T, Path }); - } else if (IsArraysType(T)) { - DiffEncapsulatedArray(NodeA, NodeB, *Fda, *Fdb, Opt, Out, Path); - } else { - switch (T) { - case EVTXContainerType::BoolProperties: - case EVTXContainerType::Int32Properties: - case EVTXContainerType::Int64Properties: - case EVTXContainerType::FloatProperties: - case EVTXContainerType::DoubleProperties: - case EVTXContainerType::StringProperties: - case EVTXContainerType::TransformProperties: - case EVTXContainerType::VectorProperties: - case EVTXContainerType::QuatProperties: - case EVTXContainerType::RangeProperties: - DiffScalarArray(NodeA, NodeB, *Fda, *Fdb, Opt, Out, Path); break; - case EVTXContainerType::AnyStructProperties: - DiffStructArray(NodeA, NodeB, *Fda, *Fdb, Opt, Out, Path); break; - case EVTXContainerType::MapProperties: - DiffMapContainers(NodeA, NodeB, *Fda, *Fdb, Opt, Out, Path); break; - default: break; - } - } - Path.pop_back(); + bool HaveMatchingContentHash(const TNodeView& NodeA, const TNodeView& NodeB) const { + const auto hash_a = TryGetContentHash(NodeA); + const auto hash_b = TryGetContentHash(NodeB); + return hash_a.has_value() && hash_b.has_value() && (*hash_a == *hash_b); } - } - - void DiffActorsArray(const TNodeView& NodeA, const TNodeView& NodeB, const FieldDesc& Fda, const FieldDesc& Fdb, const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const - { - const size_t CountA = NodeA.GetArraySize(Fda); - const size_t CountB = NodeB.GetArraySize(Fdb); - if (CountA == 0 && CountB == 0) return; - FieldDesc IdFieldDesc; IdFieldDesc.name = "unique_ids"; + public: + DefaultTreeDiff() = default; + ~DefaultTreeDiff() = default; - ScratchActorsB.clear(); - ScratchMatchedB.assign(CountB, 0); + PatchIndex ComputeDiff(const TNodeView& NodeA, const TNodeView& NodeB, const DiffOptions& Opt) const { + PatchIndex Out; + Out.operations.reserve(512); + DiffIndexPath root; - //check if actors order has changed, actor order is different in frames a and b - for (size_t i = 0; i < CountB; ++i) { - auto spanId = NodeB.GetArrayElementBytes(IdFieldDesc, i); - if (!spanId.empty()) { - std::string_view IdView(reinterpret_cast(spanId.data()), spanId.size()); - ScratchActorsB.push_back({StableHash64(IdView), i}); + if (!NodeA.IsValid() && !NodeB.IsValid()) + return Out; + if (!NodeA.IsValid() && NodeB.IsValid()) { + Out.operations.push_back({DiffOperation::Add, EVTXContainerType::Unknown, root}); + return Out; + } + if (NodeA.IsValid() && !NodeB.IsValid()) { + Out.operations.push_back({DiffOperation::Remove, EVTXContainerType::Unknown, root}); + return Out; } - } - std::sort(ScratchActorsB.begin(), ScratchActorsB.end(), [](const ActorEntry& a, const ActorEntry& b) { - return a.Hash < b.Hash; - }); - - for (size_t i = 0; i < CountA; ++i) { - auto spanIdA = NodeA.GetArrayElementBytes(IdFieldDesc, i); - if (spanIdA.empty()) continue; - - std::string_view IdViewA(reinterpret_cast(spanIdA.data()), spanIdA.size()); - uint64_t TargetHash = StableHash64(IdViewA); - - size_t foundIndexB = static_cast(-1); - - //fast-path, direct position, 99% of the cases entity 'i' is in the same postion 'i' - if (i < CountB) { - auto spanIdB_Direct = NodeB.GetArrayElementBytes(IdFieldDesc, i); - std::string_view IdViewB_Direct(reinterpret_cast(spanIdB_Direct.data()), spanIdB_Direct.size()); - if (IdViewA == IdViewB_Direct) { - foundIndexB = i; //found we dont search + std::span fields_a = NodeA.EnumerateFields(); + std::span fields_b = NodeB.EnumerateFields(); + + const FieldDesc* entities_a = nullptr; + const FieldDesc* entities_b = nullptr; + + for (const auto& f : fields_a) { + if (f.name == "entities") { + entities_a = &f; + break; } } - - //entities are out of order, do a binary search - if (foundIndexB == static_cast(-1)) { - auto It = std::lower_bound(ScratchActorsB.begin(), ScratchActorsB.end(), TargetHash, - [](const ActorEntry& elem, uint64_t val) { return elem.Hash < val; }); - - if (It != ScratchActorsB.end() && It->Hash == TargetHash) { - auto spanIdB = NodeB.GetArrayElementBytes(IdFieldDesc, It->Index); - std::string_view IdB(reinterpret_cast(spanIdB.data()), spanIdB.size()); - if (IdViewA == IdB) { - foundIndexB = It->Index; - } + for (const auto& f : fields_b) { + if (f.name == "entities") { + entities_b = &f; + break; } } - - //not found in binary search, removed or destroyed - if (foundIndexB == static_cast(-1)) - { - std::string IdStr(IdViewA); - Path.push_back(HashToPathKey(TargetHash)); - Out.operations.push_back({ DiffOperation::Remove, EVTXContainerType::AnyStructProperties, Path, IdStr }); - Path.pop_back(); - continue; + + if (entities_a && entities_b) { + root.push_back(static_cast(entities_a->type)); + DiffActorsArray(NodeA, NodeB, *entities_a, *entities_b, Opt, Out, root); + return Out; } - //mark entity B as processed - ScratchMatchedB[foundIndexB] = 1; + Diff(NodeA, NodeB, Opt, root, Out); + return Out; + } - auto ActorA = NodeA.GetArrayElementAsStruct(Fda, i); - auto ActorB = NodeB.GetArrayElementAsStruct(Fdb, foundIndexB); - if (!ActorA.IsValid() || !ActorB.IsValid()) continue; - - //check now content hash + PatchIndex ComputeEntityDiff(const TNodeView& EntityA, const TNodeView& EntityB, const DiffOptions& Opt, + DiffIndexPath BasePath = {}) const { + PatchIndex Out; + Out.operations.reserve(512); + if (!EntityA.IsValid() && !EntityB.IsValid()) + return Out; + if (!EntityA.IsValid() && EntityB.IsValid()) { + Out.operations.push_back({DiffOperation::Add, EVTXContainerType::Unknown, BasePath}); + return Out; + } + if (EntityA.IsValid() && !EntityB.IsValid()) { + Out.operations.push_back({DiffOperation::Remove, EVTXContainerType::Unknown, BasePath}); + return Out; + } - if (HaveMatchingContentHash(ActorA, ActorB)) { - continue; //entity is edentical, no need to process all vectors. + if (HaveMatchingContentHash(EntityA, EntityB)) { + return Out; } - - const int32_t Key = HashToPathKey(TargetHash); - Path.push_back(Key); - size_t OpCountBefore = Out.operations.size(); - Diff(ActorA, ActorB, Opt, Path, Out); + Diff(EntityA, EntityB, Opt, BasePath, Out); + return Out; + } + + + private: + void Diff(const TNodeView& NodeA, const TNodeView& NodeB, const DiffOptions& Opt, DiffIndexPath& Path, + PatchIndex& Out) const { + const bool validA = NodeA.IsValid(); + const bool validB = NodeB.IsValid(); + + if (!validA && !validB) + return; + if (!validA && validB) { + Out.operations.push_back({DiffOperation::Add, EVTXContainerType::Unknown, Path}); + return; + } + if (validA && !validB) { + Out.operations.push_back({DiffOperation::Remove, EVTXContainerType::Unknown, Path}); + return; + } - if (Out.operations.size() > OpCountBefore) { - std::string IdStr(IdViewA); - Out.actor_id_by_key.try_emplace(Key, IdStr); - for (size_t opIdx = OpCountBefore; opIdx < Out.operations.size(); ++opIdx) { - Out.operations[opIdx].ActorId = IdStr; + auto FieldsA = NodeA.EnumerateFields(); + auto FieldsB = NodeB.EnumerateFields(); + + constexpr uint32_t MAX_ENUM_TYPES = 64; + const FieldDesc* ArrA[MAX_ENUM_TYPES] = {nullptr}; + const FieldDesc* ArrB[MAX_ENUM_TYPES] = {nullptr}; + uint32_t maxTypeSeen = 0; + + for (const auto& f : FieldsA) { + if (f.is_actors_field || f.type == EVTXContainerType::Unknown) + continue; + uint32_t t = static_cast(f.type); + if (t < MAX_ENUM_TYPES) { + ArrA[t] = &f; + if (t > maxTypeSeen) + maxTypeSeen = t; + } + } + for (const auto& f : FieldsB) { + if (f.is_actors_field || f.type == EVTXContainerType::Unknown) + continue; + uint32_t t = static_cast(f.type); + if (t < MAX_ENUM_TYPES) { + ArrB[t] = &f; + if (t > maxTypeSeen) + maxTypeSeen = t; } } - Path.pop_back(); - } - //process entities added in frame B - for (size_t i = 0; i < CountB; ++i) { - if (ScratchMatchedB[i] == 0) { - auto spanIdB = NodeB.GetArrayElementBytes(IdFieldDesc, i); - if (spanIdB.empty()) continue; - std::string_view IdView(reinterpret_cast(spanIdB.data()), spanIdB.size()); - std::string IdStr(IdView); - - Path.push_back(HashToPathKey(StableHash64(IdView))); - Out.operations.push_back({ DiffOperation::Add, EVTXContainerType::AnyStructProperties, Path, IdStr }); + for (uint32_t t = 0; t <= maxTypeSeen; ++t) { + const FieldDesc* Fda = ArrA[t]; + const FieldDesc* Fdb = ArrB[t]; + if (!Fda && !Fdb) + continue; + + EVTXContainerType T = static_cast(t); + Path.push_back(t); + + if (!Fda && Fdb) { + Out.operations.push_back({DiffOperation::Add, T, Path}); + } else if (Fda && !Fdb) { + Out.operations.push_back({DiffOperation::Remove, T, Path}); + } else if (IsArraysType(T)) { + DiffEncapsulatedArray(NodeA, NodeB, *Fda, *Fdb, Opt, Out, Path); + } else { + switch (T) { + case EVTXContainerType::BoolProperties: + case EVTXContainerType::Int32Properties: + case EVTXContainerType::Int64Properties: + case EVTXContainerType::FloatProperties: + case EVTXContainerType::DoubleProperties: + case EVTXContainerType::StringProperties: + case EVTXContainerType::TransformProperties: + case EVTXContainerType::VectorProperties: + case EVTXContainerType::QuatProperties: + case EVTXContainerType::RangeProperties: + DiffScalarArray(NodeA, NodeB, *Fda, *Fdb, Opt, Out, Path); + break; + case EVTXContainerType::AnyStructProperties: + DiffStructArray(NodeA, NodeB, *Fda, *Fdb, Opt, Out, Path); + break; + case EVTXContainerType::MapProperties: + DiffMapContainers(NodeA, NodeB, *Fda, *Fdb, Opt, Out, Path); + break; + default: + break; + } + } Path.pop_back(); } } - } - inline bool FastSpanEqual(std::span A, std::span B) const { - if (A.size() != B.size()) return false; - const size_t sz = A.size(); - if (sz == 0) return true; + void DiffActorsArray(const TNodeView& NodeA, const TNodeView& NodeB, const FieldDesc& Fda, const FieldDesc& Fdb, + const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const { + const size_t CountA = NodeA.GetArraySize(Fda); + const size_t CountB = NodeB.GetArraySize(Fdb); + if (CountA == 0 && CountB == 0) + return; + + FieldDesc IdFieldDesc; + IdFieldDesc.name = "unique_ids"; + + ScratchActorsB.clear(); + ScratchMatchedB.assign(CountB, 0); + + //check if actors order has changed, actor order is different in frames a and b + for (size_t i = 0; i < CountB; ++i) { + auto spanId = NodeB.GetArrayElementBytes(IdFieldDesc, i); + if (!spanId.empty()) { + std::string_view IdView(reinterpret_cast(spanId.data()), spanId.size()); + ScratchActorsB.push_back({StableHash64(IdView), i}); + } + } - const void* pa = A.data(); - const void* pb = B.data(); + std::sort(ScratchActorsB.begin(), ScratchActorsB.end(), + [](const ActorEntry& a, const ActorEntry& b) { return a.Hash < b.Hash; }); - // 4 bytes (Int32, Float) - if (sz == 4) { - uint32_t va, vb; - std::memcpy(&va, pa, 4); std::memcpy(&vb, pb, 4); - return va == vb; - } - // 8 bytes (Int64, Double, 2x Float) - if (sz == 8) { - uint64_t va, vb; - std::memcpy(&va, pa, 8); std::memcpy(&vb, pb, 8); - return va == vb; - } - // 12 bytes (Vector: 3x Float) - 64 bits + 32 bits - if (sz == 12) { - uint64_t va8, vb8; uint32_t va4, vb4; - std::memcpy(&va8, pa, 8); std::memcpy(&vb8, pb, 8); - std::memcpy(&va4, static_cast(pa) + 8, 4); - std::memcpy(&vb4, static_cast(pb) + 8, 4); - return (va8 == vb8) && (va4 == vb4); - } - // 16 bytes (Quat, Range, 4x Float) - 128 bits (2x 64 bits) - if (sz == 16) { - uint64_t va1, vb1, va2, vb2; - std::memcpy(&va1, pa, 8); std::memcpy(&vb1, pb, 8); - std::memcpy(&va2, static_cast(pa) + 8, 8); - std::memcpy(&vb2, static_cast(pb) + 8, 8); - return (va1 == vb1) && (va2 == vb2); - } + for (size_t i = 0; i < CountA; ++i) { + auto spanIdA = NodeA.GetArrayElementBytes(IdFieldDesc, i); + if (spanIdA.empty()) + continue; - // Fallback para strings o arrays gigantes - return std::memcmp(pa, pb, sz) == 0; - } - - void DiffScalarArray(const TNodeView& A, const TNodeView& B, const FieldDesc& Fda, const FieldDesc& Fdb, const DiffOptions& Opt, PatchIndex& Out, const DiffIndexPath& Path) const - { - auto full_bytes_a = A.GetFieldBytes(Fda); - auto full_bytes_b = B.GetFieldBytes(Fdb); - - // Fast-path global (Idénticos al 100%) - if (!full_bytes_a.empty() && full_bytes_a.size() == full_bytes_b.size()) { - if (std::memcmp(full_bytes_a.data(), full_bytes_b.data(), full_bytes_a.size()) == 0) return; - } + std::string_view IdViewA(reinterpret_cast(spanIdA.data()), spanIdA.size()); + uint64_t TargetHash = StableHash64(IdViewA); - const size_t size_a = A.GetArraySize(Fda); - const size_t size_b = B.GetArraySize(Fdb); - const size_t size = std::min(size_a, size_b); - - int32_t current_range_start = -1; - int32_t current_range_count = 0; - - auto FlushRange = [&]() { - if (current_range_count > 0) { - if (current_range_count == 1) { - Out.operations.push_back({DiffOperation::Replace, Fda.type, Path.Append(current_range_start)}); - } else { - DiffIndexOp op = {DiffOperation::ReplaceRange, Fda.type, Path.Append(current_range_start)}; - op.ReplaceRangeCount = current_range_count; - Out.operations.push_back(op); - } - current_range_start = -1; - current_range_count = 0; - } - }; - - bool is_contiguous = !full_bytes_a.empty() && !full_bytes_b.empty() && size_a > 0 && size_b > 0; - - if (is_contiguous) { - const size_t stride_a = full_bytes_a.size() / size_a; - const size_t stride_b = full_bytes_b.size() / size_b; - - if (stride_a == stride_b && stride_a > 0) { - const uint8_t* ptr_a = reinterpret_cast(full_bytes_a.data()); - const uint8_t* ptr_b = reinterpret_cast(full_bytes_b.data()); - - for (size_t i = 0; i < size; ++i) { - bool bDifferent = false; - - if (Opt.compare_floats_with_epsilon) { - if (stride_a == sizeof(float)) { - float fa, fb; - std::memcpy(&fa, ptr_a + (i * stride_a), sizeof(float)); - std::memcpy(&fb, ptr_b + (i * stride_b), sizeof(float)); - bDifferent = std::abs(fa - fb) > Opt.float_epsilon; - } else if (stride_a == sizeof(double)) { - double da, db; - std::memcpy(&da, ptr_a + (i * stride_a), sizeof(double)); - std::memcpy(&db, ptr_b + (i * stride_b), sizeof(double)); - bDifferent = std::abs(da - db) > static_cast(Opt.float_epsilon); - } else { - bDifferent = (std::memcmp(ptr_a + (i * stride_a), ptr_b + (i * stride_b), stride_a) != 0); - } - } else { - auto span_a = std::span(reinterpret_cast(ptr_a + (i * stride_a)), stride_a); - auto span_b = std::span(reinterpret_cast(ptr_b + (i * stride_b)), stride_a); - bDifferent = !FastSpanEqual(span_a, span_b); + size_t foundIndexB = static_cast(-1); + + //fast-path, direct position, 99% of the cases entity 'i' is in the same postion 'i' + if (i < CountB) { + auto spanIdB_Direct = NodeB.GetArrayElementBytes(IdFieldDesc, i); + std::string_view IdViewB_Direct(reinterpret_cast(spanIdB_Direct.data()), + spanIdB_Direct.size()); + if (IdViewA == IdViewB_Direct) { + foundIndexB = i; //found we dont search } + } - if (bDifferent) { - if (current_range_start == -1) current_range_start = static_cast(i); - ++current_range_count; - } else { - FlushRange(); + //entities are out of order, do a binary search + if (foundIndexB == static_cast(-1)) { + auto It = std::lower_bound(ScratchActorsB.begin(), ScratchActorsB.end(), TargetHash, + [](const ActorEntry& elem, uint64_t val) { return elem.Hash < val; }); + + if (It != ScratchActorsB.end() && It->Hash == TargetHash) { + auto spanIdB = NodeB.GetArrayElementBytes(IdFieldDesc, It->Index); + std::string_view IdB(reinterpret_cast(spanIdB.data()), spanIdB.size()); + if (IdViewA == IdB) { + foundIndexB = It->Index; + } } } - FlushRange(); - goto EMIT_RESIZES; // Saltamos el bucle lento - } - } - - for (size_t i = 0; i < size; ++i) { - auto bytes_a = A.GetArrayElementBytes(Fda, i); - auto bytes_b = B.GetArrayElementBytes(Fdb, i); - - if (bytes_a.empty() && bytes_b.empty()) { FlushRange(); continue; } - - bool bDifferent = false; - if (Opt.compare_floats_with_epsilon && bytes_a.size() == bytes_b.size()) { - if (bytes_a.size() == sizeof(float)) { - float fa, fb; std::memcpy(&fa, bytes_a.data(), sizeof(float)); std::memcpy(&fb, bytes_b.data(), sizeof(float)); - bDifferent = std::abs(fa - fb) > Opt.float_epsilon; - } else if (bytes_a.size() == sizeof(double)) { - double da, db; std::memcpy(&da, bytes_a.data(), sizeof(double)); std::memcpy(&db, bytes_b.data(), sizeof(double)); - bDifferent = std::abs(da - db) > static_cast(Opt.float_epsilon); - } else { - bDifferent = !FastSpanEqual(bytes_a, bytes_b); - } - } else { - bDifferent = !FastSpanEqual(bytes_a, bytes_b); - } - - if (bDifferent) { - if (current_range_start == -1) current_range_start = static_cast(i); - ++current_range_count; - } else { - FlushRange(); - } - } - FlushRange(); -EMIT_RESIZES: - for (size_t i = size; i < size_b; ++i) { - Out.operations.push_back({ DiffOperation::Add, Fda.type, Path.Append(static_cast(i)) }); - } - - for (size_t i = size; i < size_a; ++i) { - Out.operations.push_back({ DiffOperation::Remove, Fda.type, Path.Append(static_cast(i)) }); - } - } + //not found in binary search, removed or destroyed + if (foundIndexB == static_cast(-1)) { + std::string IdStr(IdViewA); + Path.push_back(HashToPathKey(TargetHash)); + Out.operations.push_back( + {DiffOperation::Remove, EVTXContainerType::AnyStructProperties, Path, IdStr}); + Path.pop_back(); + continue; + } - void DiffStructArray(const TNodeView& A, const TNodeView& B, const FieldDesc& Fda, const FieldDesc& Fdb, const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const - { - auto full_bytes_a = A.GetFieldBytes(Fda); - auto full_bytes_b = B.GetFieldBytes(Fdb); - - if (!full_bytes_a.empty() && full_bytes_a.size() == full_bytes_b.size()) { - if (std::memcmp(full_bytes_a.data(), full_bytes_b.data(), full_bytes_a.size()) == 0) return; - } + //mark entity B as processed + ScratchMatchedB[foundIndexB] = 1; - const size_t size_a = A.GetArraySize(Fda); - const size_t size_b = B.GetArraySize(Fdb); + auto ActorA = NodeA.GetArrayElementAsStruct(Fda, i); + auto ActorB = NodeB.GetArrayElementAsStruct(Fdb, foundIndexB); + if (!ActorA.IsValid() || !ActorB.IsValid()) + continue; - if (size_a != size_b) { - Out.operations.push_back({ DiffOperation::Replace, Fda.type, Path }); - return; - } + //check now content hash - for (size_t i = 0; i < size_a; ++i) { - auto node_a = A.GetArrayElementAsStruct(Fda, i); - auto node_b = B.GetArrayElementAsStruct(Fdb, i); - Path.push_back(static_cast(i)); - - if (!node_a.IsValid() || !node_b.IsValid()) { - Out.operations.push_back({ DiffOperation::Replace, Fda.type, Path }); - } else - { - //use fast-path with content hash to omit identical structs - if (HaveMatchingContentHash(node_a, node_b)) { - //structs are identical, omit - } else { - Diff(node_a, node_b, Opt, Path, Out); + if (HaveMatchingContentHash(ActorA, ActorB)) { + continue; //entity is edentical, no need to process all vectors. } - } - Path.pop_back(); - } - } - void DiffFlatRepeatedByOffsets(const TNodeView& A, const TNodeView& B, const FieldDesc& FlatFda, const FieldDesc& FlatFdb, const FieldDesc& OffFda, const FieldDesc& OffFdb, const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const - { - const size_t OffCountA = A.GetArraySize(OffFda); - const size_t OffCountB = B.GetArraySize(OffFdb); - const size_t N = std::min(OffCountA, OffCountB); - - for (size_t i = 0; i < N; ++i) { - size_t StartA = 0, StartB = 0; - bool bValidOffsets = true; - bValidOffsets &= ReadOffsetU32(A.GetArrayElementBytes(OffFda, i), StartA); - bValidOffsets &= ReadOffsetU32(B.GetArrayElementBytes(OffFdb, i), StartB); - - if (!bValidOffsets) { - Out.operations.push_back({ DiffOperation::Replace, FlatFda.type, Path.Append(static_cast(i)) }); - continue; - } - - size_t EndA = A.GetArraySize(FlatFda); - size_t EndB = B.GetArraySize(FlatFdb); - if (i + 1 < OffCountA) { size_t Next = 0; if (ReadOffsetU32(A.GetArrayElementBytes(OffFda, i + 1), Next)) EndA = Next; } - if (i + 1 < OffCountB) { size_t Next = 0; if (ReadOffsetU32(B.GetArrayElementBytes(OffFdb, i + 1), Next)) EndB = Next; } - - if (EndA < StartA) EndA = StartA; - if (EndB < StartB) EndB = StartB; - - const size_t LenA = EndA - StartA; - const size_t LenB = EndB - StartB; - - if (LenA != LenB) { - Out.operations.push_back({ DiffOperation::Replace, FlatFda.type, Path.Append(static_cast(i)) }); - continue; - } - - bool bSubArrayChanged = false; - if (FlatFda.type == EVTXContainerType::AnyStructArrays) { - for (size_t j = 0; j < LenA; ++j) { - auto NodeElemA = A.GetArrayElementAsStruct(FlatFda, StartA + j); - auto NodeElemB = B.GetArrayElementAsStruct(FlatFdb, StartB + j); - if (!NodeElemA.IsValid() || !NodeElemB.IsValid()) { bSubArrayChanged = true; break; } - - size_t before = Out.operations.size(); - Diff(NodeElemA, NodeElemB, Opt, Path, Out); - if (Out.operations.size() > before) { - Out.operations.resize(before); - bSubArrayChanged = true; break; + const int32_t Key = HashToPathKey(TargetHash); + Path.push_back(Key); + + size_t OpCountBefore = Out.operations.size(); + Diff(ActorA, ActorB, Opt, Path, Out); + + if (Out.operations.size() > OpCountBefore) { + std::string IdStr(IdViewA); + Out.actor_id_by_key.try_emplace(Key, IdStr); + for (size_t opIdx = OpCountBefore; opIdx < Out.operations.size(); ++opIdx) { + Out.operations[opIdx].ActorId = IdStr; } } - } else { - for (size_t j = 0; j < LenA; ++j) { - auto EA = A.GetArrayElementBytes(FlatFda, StartA + j); - auto EB = B.GetArrayElementBytes(FlatFdb, StartB + j); - if (!AreScalarsEqual(FlatFda.type, EA, EB, Opt)) { bSubArrayChanged = true; break; } + Path.pop_back(); + } + + //process entities added in frame B + for (size_t i = 0; i < CountB; ++i) { + if (ScratchMatchedB[i] == 0) { + auto spanIdB = NodeB.GetArrayElementBytes(IdFieldDesc, i); + if (spanIdB.empty()) + continue; + std::string_view IdView(reinterpret_cast(spanIdB.data()), spanIdB.size()); + std::string IdStr(IdView); + + Path.push_back(HashToPathKey(StableHash64(IdView))); + Out.operations.push_back({DiffOperation::Add, EVTXContainerType::AnyStructProperties, Path, IdStr}); + Path.pop_back(); } } + } - if (bSubArrayChanged) { - Out.operations.push_back({ DiffOperation::Replace, FlatFda.type, Path.Append(static_cast(i)) }); + inline bool FastSpanEqual(std::span A, std::span B) const { + if (A.size() != B.size()) + return false; + const size_t sz = A.size(); + if (sz == 0) + return true; + + const void* pa = A.data(); + const void* pb = B.data(); + + // 4 bytes (Int32, Float) + if (sz == 4) { + uint32_t va, vb; + std::memcpy(&va, pa, 4); + std::memcpy(&vb, pb, 4); + return va == vb; + } + // 8 bytes (Int64, Double, 2x Float) + if (sz == 8) { + uint64_t va, vb; + std::memcpy(&va, pa, 8); + std::memcpy(&vb, pb, 8); + return va == vb; } + // 12 bytes (Vector: 3x Float) - 64 bits + 32 bits + if (sz == 12) { + uint64_t va8, vb8; + uint32_t va4, vb4; + std::memcpy(&va8, pa, 8); + std::memcpy(&vb8, pb, 8); + std::memcpy(&va4, static_cast(pa) + 8, 4); + std::memcpy(&vb4, static_cast(pb) + 8, 4); + return (va8 == vb8) && (va4 == vb4); + } + // 16 bytes (Quat, Range, 4x Float) - 128 bits (2x 64 bits) + if (sz == 16) { + uint64_t va1, vb1, va2, vb2; + std::memcpy(&va1, pa, 8); + std::memcpy(&vb1, pb, 8); + std::memcpy(&va2, static_cast(pa) + 8, 8); + std::memcpy(&vb2, static_cast(pb) + 8, 8); + return (va1 == vb1) && (va2 == vb2); + } + + // Fallback para strings o arrays gigantes + return std::memcmp(pa, pb, sz) == 0; } - for (size_t i = N; i < OffCountA; ++i) { Out.operations.push_back({ DiffOperation::Remove, FlatFda.type, Path.Append(static_cast(i)) }); } - for (size_t i = N; i < OffCountB; ++i) { Out.operations.push_back({ DiffOperation::Replace, FlatFda.type, Path.Append(static_cast(i)) }); } - } + void DiffScalarArray(const TNodeView& A, const TNodeView& B, const FieldDesc& Fda, const FieldDesc& Fdb, + const DiffOptions& Opt, PatchIndex& Out, const DiffIndexPath& Path) const { + auto full_bytes_a = A.GetFieldBytes(Fda); + auto full_bytes_b = B.GetFieldBytes(Fdb); - void DiffEncapsulatedArray(const TNodeView& ParentA, const TNodeView& ParentB, const FieldDesc& WrapperFda, const FieldDesc& WrapperFdb, const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const - { - auto wrap_a = ParentA.GetNestedStruct(WrapperFda); - auto wrap_b = ParentB.GetNestedStruct(WrapperFdb); + // Fast-path global (Idénticos al 100%) + if (!full_bytes_a.empty() && full_bytes_a.size() == full_bytes_b.size()) { + if (std::memcmp(full_bytes_a.data(), full_bytes_b.data(), full_bytes_a.size()) == 0) + return; + } - if (!wrap_a.IsValid() && !wrap_b.IsValid()) return; - if (!wrap_a.IsValid() && wrap_b.IsValid()) { Out.operations.push_back({ DiffOperation::Add, WrapperFda.type, Path }); return; } - if (wrap_a.IsValid() && !wrap_b.IsValid()) { Out.operations.push_back({ DiffOperation::Remove, WrapperFda.type, Path }); return; } + const size_t size_a = A.GetArraySize(Fda); + const size_t size_b = B.GetArraySize(Fdb); + const size_t size = std::min(size_a, size_b); - FieldDesc field_desc; field_desc.name = "offsets"; - FieldDesc data_desc; data_desc.name = "data"; + int32_t current_range_start = -1; + int32_t current_range_count = 0; - bool b_is_recursive = (WrapperFda.type == EVTXContainerType::AnyStructArrays); + auto FlushRange = [&]() { + if (current_range_count > 0) { + if (current_range_count == 1) { + Out.operations.push_back({DiffOperation::Replace, Fda.type, Path.Append(current_range_start)}); + } else { + DiffIndexOp op = {DiffOperation::ReplaceRange, Fda.type, Path.Append(current_range_start)}; + op.ReplaceRangeCount = current_range_count; + Out.operations.push_back(op); + } + current_range_start = -1; + current_range_count = 0; + } + }; + + bool is_contiguous = !full_bytes_a.empty() && !full_bytes_b.empty() && size_a > 0 && size_b > 0; + + if (is_contiguous) { + const size_t stride_a = full_bytes_a.size() / size_a; + const size_t stride_b = full_bytes_b.size() / size_b; + + if (stride_a == stride_b && stride_a > 0) { + const uint8_t* ptr_a = reinterpret_cast(full_bytes_a.data()); + const uint8_t* ptr_b = reinterpret_cast(full_bytes_b.data()); + + for (size_t i = 0; i < size; ++i) { + bool bDifferent = false; + + if (Opt.compare_floats_with_epsilon) { + if (stride_a == sizeof(float)) { + float fa, fb; + std::memcpy(&fa, ptr_a + (i * stride_a), sizeof(float)); + std::memcpy(&fb, ptr_b + (i * stride_b), sizeof(float)); + bDifferent = std::abs(fa - fb) > Opt.float_epsilon; + } else if (stride_a == sizeof(double)) { + double da, db; + std::memcpy(&da, ptr_a + (i * stride_a), sizeof(double)); + std::memcpy(&db, ptr_b + (i * stride_b), sizeof(double)); + bDifferent = std::abs(da - db) > static_cast(Opt.float_epsilon); + } else { + bDifferent = + (std::memcmp(ptr_a + (i * stride_a), ptr_b + (i * stride_b), stride_a) != 0); + } + } else { + auto span_a = std::span( + reinterpret_cast(ptr_a + (i * stride_a)), stride_a); + auto span_b = std::span( + reinterpret_cast(ptr_b + (i * stride_b)), stride_a); + bDifferent = !FastSpanEqual(span_a, span_b); + } + + if (bDifferent) { + if (current_range_start == -1) + current_range_start = static_cast(i); + ++current_range_count; + } else { + FlushRange(); + } + } + FlushRange(); + goto EMIT_RESIZES; // Saltamos el bucle lento + } + } + + for (size_t i = 0; i < size; ++i) { + auto bytes_a = A.GetArrayElementBytes(Fda, i); + auto bytes_b = B.GetArrayElementBytes(Fdb, i); - const size_t off_count_a = wrap_a.GetArraySize(field_desc); - const size_t off_count_b = wrap_b.GetArraySize(field_desc); - const size_t size = std::min(off_count_a, off_count_b); + if (bytes_a.empty() && bytes_b.empty()) { + FlushRange(); + continue; + } + + bool bDifferent = false; + if (Opt.compare_floats_with_epsilon && bytes_a.size() == bytes_b.size()) { + if (bytes_a.size() == sizeof(float)) { + float fa, fb; + std::memcpy(&fa, bytes_a.data(), sizeof(float)); + std::memcpy(&fb, bytes_b.data(), sizeof(float)); + bDifferent = std::abs(fa - fb) > Opt.float_epsilon; + } else if (bytes_a.size() == sizeof(double)) { + double da, db; + std::memcpy(&da, bytes_a.data(), sizeof(double)); + std::memcpy(&db, bytes_b.data(), sizeof(double)); + bDifferent = std::abs(da - db) > static_cast(Opt.float_epsilon); + } else { + bDifferent = !FastSpanEqual(bytes_a, bytes_b); + } + } else { + bDifferent = !FastSpanEqual(bytes_a, bytes_b); + } - for (size_t i = 0; i < size; ++i) { - size_t start_a = 0, start_b = 0; - bool b_valid = true; - b_valid &= ReadOffsetAny(wrap_a.GetArrayElementBytes(field_desc, i), start_a); - b_valid &= ReadOffsetAny(wrap_b.GetArrayElementBytes(field_desc, i), start_b); + if (bDifferent) { + if (current_range_start == -1) + current_range_start = static_cast(i); + ++current_range_count; + } else { + FlushRange(); + } + } + FlushRange(); - if (!b_valid) { - Out.operations.push_back({ DiffOperation::Replace, WrapperFda.type, Path.Append(static_cast(i)) }); - continue; + EMIT_RESIZES: + for (size_t i = size; i < size_b; ++i) { + Out.operations.push_back({DiffOperation::Add, Fda.type, Path.Append(static_cast(i))}); } - size_t end_a = wrap_a.GetArraySize(data_desc); - size_t end_b = wrap_b.GetArraySize(data_desc); + for (size_t i = size; i < size_a; ++i) { + Out.operations.push_back({DiffOperation::Remove, Fda.type, Path.Append(static_cast(i))}); + } + } - if (i + 1 < off_count_a) { size_t Next = 0; if (ReadOffsetAny(wrap_a.GetArrayElementBytes(field_desc, i + 1), Next)) end_a = Next; } - if (i + 1 < off_count_b) { size_t Next = 0; if (ReadOffsetAny(wrap_b.GetArrayElementBytes(field_desc, i + 1), Next)) end_b = Next; } + void DiffStructArray(const TNodeView& A, const TNodeView& B, const FieldDesc& Fda, const FieldDesc& Fdb, + const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const { + auto full_bytes_a = A.GetFieldBytes(Fda); + auto full_bytes_b = B.GetFieldBytes(Fdb); - if (end_a < start_a) end_a = start_a; - if (end_b < start_b) end_b = start_b; + if (!full_bytes_a.empty() && full_bytes_a.size() == full_bytes_b.size()) { + if (std::memcmp(full_bytes_a.data(), full_bytes_b.data(), full_bytes_a.size()) == 0) + return; + } - const size_t len_a = end_a - start_a; - const size_t len_b = end_b - start_b; + const size_t size_a = A.GetArraySize(Fda); + const size_t size_b = B.GetArraySize(Fdb); - if (len_a != len_b) { - Out.operations.push_back({ DiffOperation::Replace, WrapperFda.type, Path.Append(static_cast(i)) }); - continue; + if (size_a != size_b) { + Out.operations.push_back({DiffOperation::Replace, Fda.type, Path}); + return; } - bool b_changed = false; - if (b_is_recursive) { - for (size_t j = 0; j < len_a; ++j) { - auto node_a = wrap_a.GetArrayElementAsStruct(data_desc, start_a + j); - auto node_b = wrap_b.GetArrayElementAsStruct(data_desc, start_b + j); - if (!node_a.IsValid() || !node_b.IsValid()) { b_changed = true; break; } + for (size_t i = 0; i < size_a; ++i) { + auto node_a = A.GetArrayElementAsStruct(Fda, i); + auto node_b = B.GetArrayElementAsStruct(Fdb, i); + Path.push_back(static_cast(i)); + if (!node_a.IsValid() || !node_b.IsValid()) { + Out.operations.push_back({DiffOperation::Replace, Fda.type, Path}); + } else { //use fast-path with content hash to omit identical structs if (HaveMatchingContentHash(node_a, node_b)) { //structs are identical, omit } else { - //we surely know something has changed, we can omit the full diff - b_changed = true; - break; + Diff(node_a, node_b, Opt, Path, Out); } } - } else { - auto block_a = wrap_a.GetSubArrayBytes(WrapperFda, i); - auto block_b = wrap_b.GetSubArrayBytes(WrapperFdb, i); + Path.pop_back(); + } + } + + void DiffFlatRepeatedByOffsets(const TNodeView& A, const TNodeView& B, const FieldDesc& FlatFda, + const FieldDesc& FlatFdb, const FieldDesc& OffFda, const FieldDesc& OffFdb, + const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const { + const size_t OffCountA = A.GetArraySize(OffFda); + const size_t OffCountB = B.GetArraySize(OffFdb); + const size_t N = std::min(OffCountA, OffCountB); + + for (size_t i = 0; i < N; ++i) { + size_t StartA = 0, StartB = 0; + bool bValidOffsets = true; + bValidOffsets &= ReadOffsetU32(A.GetArrayElementBytes(OffFda, i), StartA); + bValidOffsets &= ReadOffsetU32(B.GetArrayElementBytes(OffFdb, i), StartB); + + if (!bValidOffsets) { + Out.operations.push_back( + {DiffOperation::Replace, FlatFda.type, Path.Append(static_cast(i))}); + continue; + } + + size_t EndA = A.GetArraySize(FlatFda); + size_t EndB = B.GetArraySize(FlatFdb); + if (i + 1 < OffCountA) { + size_t Next = 0; + if (ReadOffsetU32(A.GetArrayElementBytes(OffFda, i + 1), Next)) + EndA = Next; + } + if (i + 1 < OffCountB) { + size_t Next = 0; + if (ReadOffsetU32(B.GetArrayElementBytes(OffFdb, i + 1), Next)) + EndB = Next; + } + + if (EndA < StartA) + EndA = StartA; + if (EndB < StartB) + EndB = StartB; + + const size_t LenA = EndA - StartA; + const size_t LenB = EndB - StartB; + + if (LenA != LenB) { + Out.operations.push_back( + {DiffOperation::Replace, FlatFda.type, Path.Append(static_cast(i))}); + continue; + } + + bool bSubArrayChanged = false; + if (FlatFda.type == EVTXContainerType::AnyStructArrays) { + for (size_t j = 0; j < LenA; ++j) { + auto NodeElemA = A.GetArrayElementAsStruct(FlatFda, StartA + j); + auto NodeElemB = B.GetArrayElementAsStruct(FlatFdb, StartB + j); + if (!NodeElemA.IsValid() || !NodeElemB.IsValid()) { + bSubArrayChanged = true; + break; + } - if (!block_a.empty() && block_a.size() == block_b.size()) { - if (std::memcmp(block_a.data(), block_b.data(), block_a.size()) == 0) continue; + size_t before = Out.operations.size(); + Diff(NodeElemA, NodeElemB, Opt, Path, Out); + if (Out.operations.size() > before) { + Out.operations.resize(before); + bSubArrayChanged = true; + break; + } + } + } else { + for (size_t j = 0; j < LenA; ++j) { + auto EA = A.GetArrayElementBytes(FlatFda, StartA + j); + auto EB = B.GetArrayElementBytes(FlatFdb, StartB + j); + if (!AreScalarsEqual(FlatFda.type, EA, EB, Opt)) { + bSubArrayChanged = true; + break; + } + } } - for (size_t j = 0; j < len_a; ++j) { - auto bytes_a = wrap_a.GetArrayElementBytes(data_desc, start_a + j); - auto bytes_b = wrap_b.GetArrayElementBytes(data_desc, start_b + j); - if (!AreScalarsEqual(WrapperFda.type, bytes_a, bytes_b, Opt)) { b_changed = true; break; } + if (bSubArrayChanged) { + Out.operations.push_back( + {DiffOperation::Replace, FlatFda.type, Path.Append(static_cast(i))}); } } - if (b_changed) { - Out.operations.push_back({ DiffOperation::Replace, WrapperFda.type, Path.Append(static_cast(i)) }); + for (size_t i = N; i < OffCountA; ++i) { + Out.operations.push_back({DiffOperation::Remove, FlatFda.type, Path.Append(static_cast(i))}); + } + for (size_t i = N; i < OffCountB; ++i) { + Out.operations.push_back({DiffOperation::Replace, FlatFda.type, Path.Append(static_cast(i))}); } } - for (size_t i = size; i < off_count_a; ++i) { Out.operations.push_back({ DiffOperation::Remove, WrapperFda.type, Path.Append(static_cast(i)) }); } - for (size_t i = size; i < off_count_b; ++i) { Out.operations.push_back({ DiffOperation::Replace, WrapperFda.type, Path.Append(static_cast(i)) }); } - } + void DiffEncapsulatedArray(const TNodeView& ParentA, const TNodeView& ParentB, const FieldDesc& WrapperFda, + const FieldDesc& WrapperFdb, const DiffOptions& Opt, PatchIndex& Out, + DiffIndexPath& Path) const { + auto wrap_a = ParentA.GetNestedStruct(WrapperFda); + auto wrap_b = ParentB.GetNestedStruct(WrapperFdb); + + if (!wrap_a.IsValid() && !wrap_b.IsValid()) + return; + if (!wrap_a.IsValid() && wrap_b.IsValid()) { + Out.operations.push_back({DiffOperation::Add, WrapperFda.type, Path}); + return; + } + if (wrap_a.IsValid() && !wrap_b.IsValid()) { + Out.operations.push_back({DiffOperation::Remove, WrapperFda.type, Path}); + return; + } - void DiffMapContainers(const TNodeView& A, const TNodeView& B, const FieldDesc& Fda, const FieldDesc& Fdb, const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const - { - const size_t size_a = A.GetMapSize(Fda); - const size_t size_b = B.GetMapSize(Fdb); - if (size_a == 0 && size_b == 0) return; - - ScratchMapA.clear(); ScratchMapB.clear(); - for (size_t i = 0; i < size_a; ++i) { - std::string k = A.GetMapKey(Fda, i); - ScratchMapA.push_back({StableHash64(k), std::move(k), i}); - } - for (size_t i = 0; i < size_b; ++i) { - std::string k = B.GetMapKey(Fdb, i); - ScratchMapB.push_back({StableHash64(k), std::move(k), i}); - } + FieldDesc field_desc; + field_desc.name = "offsets"; + FieldDesc data_desc; + data_desc.name = "data"; - std::sort(ScratchMapA.begin(), ScratchMapA.end(), [](const auto& a, const auto& b){ return a.Hash < b.Hash; }); - std::sort(ScratchMapB.begin(), ScratchMapB.end(), [](const auto& a, const auto& b){ return a.Hash < b.Hash; }); + bool b_is_recursive = (WrapperFda.type == EVTXContainerType::AnyStructArrays); - size_t idx_a = 0, idx_b = 0; - while(idx_a < ScratchMapA.size() || idx_b < ScratchMapB.size()) { - if (idx_a < ScratchMapA.size() && (idx_b == ScratchMapB.size() || ScratchMapA[idx_a].Hash < ScratchMapB[idx_b].Hash)) { - Path.push_back(HashToPathKey(ScratchMapA[idx_a].Hash)); - Out.operations.push_back({ DiffOperation::Remove, Fda.type, Path, {}, ScratchMapA[idx_a].Key }); - Path.pop_back(); - idx_a++; - } - else if (idx_b < ScratchMapB.size() && (idx_a == ScratchMapA.size() || ScratchMapB[idx_b].Hash < ScratchMapA[idx_a].Hash)) { - Path.push_back(HashToPathKey(ScratchMapB[idx_b].Hash)); - Out.operations.push_back({ DiffOperation::Add, Fda.type, Path, {}, ScratchMapB[idx_b].Key }); - Path.pop_back(); - idx_b++; - } - else { - if (ScratchMapA[idx_a].Key == ScratchMapB[idx_b].Key) { - auto node_a = A.GetMapValueAsStruct(Fda, ScratchMapA[idx_a].Index); - auto node_b = B.GetMapValueAsStruct(Fdb, ScratchMapB[idx_b].Index); - if (node_a.IsValid() && node_b.IsValid()) { + const size_t off_count_a = wrap_a.GetArraySize(field_desc); + const size_t off_count_b = wrap_b.GetArraySize(field_desc); + const size_t size = std::min(off_count_a, off_count_b); + + for (size_t i = 0; i < size; ++i) { + size_t start_a = 0, start_b = 0; + bool b_valid = true; + b_valid &= ReadOffsetAny(wrap_a.GetArrayElementBytes(field_desc, i), start_a); + b_valid &= ReadOffsetAny(wrap_b.GetArrayElementBytes(field_desc, i), start_b); + + if (!b_valid) { + Out.operations.push_back( + {DiffOperation::Replace, WrapperFda.type, Path.Append(static_cast(i))}); + continue; + } + + size_t end_a = wrap_a.GetArraySize(data_desc); + size_t end_b = wrap_b.GetArraySize(data_desc); + + if (i + 1 < off_count_a) { + size_t Next = 0; + if (ReadOffsetAny(wrap_a.GetArrayElementBytes(field_desc, i + 1), Next)) + end_a = Next; + } + if (i + 1 < off_count_b) { + size_t Next = 0; + if (ReadOffsetAny(wrap_b.GetArrayElementBytes(field_desc, i + 1), Next)) + end_b = Next; + } + + if (end_a < start_a) + end_a = start_a; + if (end_b < start_b) + end_b = start_b; + + const size_t len_a = end_a - start_a; + const size_t len_b = end_b - start_b; + + if (len_a != len_b) { + Out.operations.push_back( + {DiffOperation::Replace, WrapperFda.type, Path.Append(static_cast(i))}); + continue; + } + + bool b_changed = false; + if (b_is_recursive) { + for (size_t j = 0; j < len_a; ++j) { + auto node_a = wrap_a.GetArrayElementAsStruct(data_desc, start_a + j); + auto node_b = wrap_b.GetArrayElementAsStruct(data_desc, start_b + j); + if (!node_a.IsValid() || !node_b.IsValid()) { + b_changed = true; + break; + } + + //use fast-path with content hash to omit identical structs if (HaveMatchingContentHash(node_a, node_b)) { //structs are identical, omit } else { - Path.push_back(HashToPathKey(ScratchMapA[idx_a].Hash)); - const size_t OpCountBefore = Out.operations.size(); - Diff(node_a, node_b, Opt, Path, Out); - for (size_t OpIdx = OpCountBefore; OpIdx < Out.operations.size(); ++OpIdx) { - Out.operations[OpIdx].MapKey = ScratchMapA[idx_a].Key; - } - Path.pop_back(); + //we surely know something has changed, we can omit the full diff + b_changed = true; + break; } } } else { + auto block_a = wrap_a.GetSubArrayBytes(WrapperFda, i); + auto block_b = wrap_b.GetSubArrayBytes(WrapperFdb, i); + + if (!block_a.empty() && block_a.size() == block_b.size()) { + if (std::memcmp(block_a.data(), block_b.data(), block_a.size()) == 0) + continue; + } + + for (size_t j = 0; j < len_a; ++j) { + auto bytes_a = wrap_a.GetArrayElementBytes(data_desc, start_a + j); + auto bytes_b = wrap_b.GetArrayElementBytes(data_desc, start_b + j); + if (!AreScalarsEqual(WrapperFda.type, bytes_a, bytes_b, Opt)) { + b_changed = true; + break; + } + } + } + + if (b_changed) { + Out.operations.push_back( + {DiffOperation::Replace, WrapperFda.type, Path.Append(static_cast(i))}); + } + } + + for (size_t i = size; i < off_count_a; ++i) { + Out.operations.push_back( + {DiffOperation::Remove, WrapperFda.type, Path.Append(static_cast(i))}); + } + for (size_t i = size; i < off_count_b; ++i) { + Out.operations.push_back( + {DiffOperation::Replace, WrapperFda.type, Path.Append(static_cast(i))}); + } + } + + void DiffMapContainers(const TNodeView& A, const TNodeView& B, const FieldDesc& Fda, const FieldDesc& Fdb, + const DiffOptions& Opt, PatchIndex& Out, DiffIndexPath& Path) const { + const size_t size_a = A.GetMapSize(Fda); + const size_t size_b = B.GetMapSize(Fdb); + if (size_a == 0 && size_b == 0) + return; + + ScratchMapA.clear(); + ScratchMapB.clear(); + for (size_t i = 0; i < size_a; ++i) { + std::string k = A.GetMapKey(Fda, i); + ScratchMapA.push_back({StableHash64(k), std::move(k), i}); + } + for (size_t i = 0; i < size_b; ++i) { + std::string k = B.GetMapKey(Fdb, i); + ScratchMapB.push_back({StableHash64(k), std::move(k), i}); + } + + std::sort(ScratchMapA.begin(), ScratchMapA.end(), + [](const auto& a, const auto& b) { return a.Hash < b.Hash; }); + std::sort(ScratchMapB.begin(), ScratchMapB.end(), + [](const auto& a, const auto& b) { return a.Hash < b.Hash; }); + + size_t idx_a = 0, idx_b = 0; + while (idx_a < ScratchMapA.size() || idx_b < ScratchMapB.size()) { + if (idx_a < ScratchMapA.size() && + (idx_b == ScratchMapB.size() || ScratchMapA[idx_a].Hash < ScratchMapB[idx_b].Hash)) { Path.push_back(HashToPathKey(ScratchMapA[idx_a].Hash)); - Out.operations.push_back({ DiffOperation::Remove, Fda.type, Path, {}, ScratchMapA[idx_a].Key }); + Out.operations.push_back({DiffOperation::Remove, Fda.type, Path, {}, ScratchMapA[idx_a].Key}); Path.pop_back(); - + idx_a++; + } else if (idx_b < ScratchMapB.size() && + (idx_a == ScratchMapA.size() || ScratchMapB[idx_b].Hash < ScratchMapA[idx_a].Hash)) { Path.push_back(HashToPathKey(ScratchMapB[idx_b].Hash)); - Out.operations.push_back({ DiffOperation::Add, Fda.type, Path, {}, ScratchMapB[idx_b].Key }); + Out.operations.push_back({DiffOperation::Add, Fda.type, Path, {}, ScratchMapB[idx_b].Key}); Path.pop_back(); + idx_b++; + } else { + if (ScratchMapA[idx_a].Key == ScratchMapB[idx_b].Key) { + auto node_a = A.GetMapValueAsStruct(Fda, ScratchMapA[idx_a].Index); + auto node_b = B.GetMapValueAsStruct(Fdb, ScratchMapB[idx_b].Index); + if (node_a.IsValid() && node_b.IsValid()) { + if (HaveMatchingContentHash(node_a, node_b)) { + //structs are identical, omit + } else { + Path.push_back(HashToPathKey(ScratchMapA[idx_a].Hash)); + const size_t OpCountBefore = Out.operations.size(); + Diff(node_a, node_b, Opt, Path, Out); + for (size_t OpIdx = OpCountBefore; OpIdx < Out.operations.size(); ++OpIdx) { + Out.operations[OpIdx].MapKey = ScratchMapA[idx_a].Key; + } + Path.pop_back(); + } + } + } else { + Path.push_back(HashToPathKey(ScratchMapA[idx_a].Hash)); + Out.operations.push_back({DiffOperation::Remove, Fda.type, Path, {}, ScratchMapA[idx_a].Key}); + Path.pop_back(); + + Path.push_back(HashToPathKey(ScratchMapB[idx_b].Hash)); + Out.operations.push_back({DiffOperation::Add, Fda.type, Path, {}, ScratchMapB[idx_b].Key}); + Path.pop_back(); + } + idx_a++; + idx_b++; } - idx_a++; idx_b++; } } - } -}; + }; } // namespace VtxDiff diff --git a/sdk/include/vtx/differ/core/vtx_diff_types.h b/sdk/include/vtx/differ/core/vtx_diff_types.h index c9b8f88..6bd48eb 100644 --- a/sdk/include/vtx/differ/core/vtx_diff_types.h +++ b/sdk/include/vtx/differ/core/vtx_diff_types.h @@ -19,14 +19,7 @@ namespace VtxDiff { // Represents the binary serialization technology used for // reading/writing structured binary data (Protobuf, FlatBuffers, etc.). // ========================================================== - enum class WireFormat : uint8_t - { - None = 0, - Archive, - Flatbuffers, - Protobuf, - MAX - }; + enum class WireFormat : uint8_t { None = 0, Archive, Flatbuffers, Protobuf, MAX }; // ========================================================== // Enum: DiffAlgorithmType @@ -34,8 +27,8 @@ namespace VtxDiff { // Identifies which diff algorithm implementation to use. // ========================================================== enum class DiffAlgorithmType : uint8_t { - Default = 0, // Tree-based recursive diff - Experimental, // Placeholder for testing new ones + Default = 0, // Tree-based recursive diff + Experimental, // Placeholder for testing new ones MAX }; @@ -46,104 +39,130 @@ namespace VtxDiff { enum class EVTXContainerType : uint8_t { Unknown = 0, - BoolProperties, // 1 - Int32Properties, // 2 - Int64Properties, // 3 - FloatProperties, // 4 - DoubleProperties, // 5 - StringProperties, // 6 - TransformProperties, // 7 - VectorProperties, // 8 - QuatProperties, // 9 - RangeProperties, // 10 - - ByteArrayProperties, // 11 (reaplce UInt8Arrays/Int8Arrays) - Int32Arrays, // 12 - Int64Arrays, // 13 - FloatArrays, // 14 - DoubleArrays, // 15 - StringArrays, // 16 - TransformArrays, // 17 - VectorArrays, // 18 - QuatArrays, // 19 - RangeArrays, // 20 - BoolArrays, // 21 - - AnyStructProperties, // 22 - AnyStructArrays, // 23 - MapProperties, // 24 - MapArrays // 25 + BoolProperties, // 1 + Int32Properties, // 2 + Int64Properties, // 3 + FloatProperties, // 4 + DoubleProperties, // 5 + StringProperties, // 6 + TransformProperties, // 7 + VectorProperties, // 8 + QuatProperties, // 9 + RangeProperties, // 10 + + ByteArrayProperties, // 11 (reaplce UInt8Arrays/Int8Arrays) + Int32Arrays, // 12 + Int64Arrays, // 13 + FloatArrays, // 14 + DoubleArrays, // 15 + StringArrays, // 16 + TransformArrays, // 17 + VectorArrays, // 18 + QuatArrays, // 19 + RangeArrays, // 20 + BoolArrays, // 21 + + AnyStructProperties, // 22 + AnyStructArrays, // 23 + MapProperties, // 24 + MapArrays // 25 }; - + static std::string TypeToFieldName(VtxDiff::EVTXContainerType Type) { - switch (Type) { - case VtxDiff::EVTXContainerType::Unknown: return "Unknown"; - - case VtxDiff::EVTXContainerType::BoolProperties: return "bool_properties"; - case VtxDiff::EVTXContainerType::Int32Properties: return "int32_properties"; - case VtxDiff::EVTXContainerType::Int64Properties: return "int64_properties"; - case VtxDiff::EVTXContainerType::FloatProperties: return "float_properties"; - case VtxDiff::EVTXContainerType::DoubleProperties: return "double_properties"; - case VtxDiff::EVTXContainerType::StringProperties: return "string_properties"; - case VtxDiff::EVTXContainerType::TransformProperties: return "transform_properties"; - case VtxDiff::EVTXContainerType::VectorProperties: return "vector_properties"; - case VtxDiff::EVTXContainerType::QuatProperties: return "quat_properties"; - case VtxDiff::EVTXContainerType::RangeProperties: return "range_properties"; - - case VtxDiff::EVTXContainerType::ByteArrayProperties: return "byte_array_properties"; - case VtxDiff::EVTXContainerType::Int32Arrays: return "int32_arrays"; - case VtxDiff::EVTXContainerType::Int64Arrays: return "int64_arrays"; - case VtxDiff::EVTXContainerType::FloatArrays: return "float_arrays"; - case VtxDiff::EVTXContainerType::DoubleArrays: return "double_arrays"; - case VtxDiff::EVTXContainerType::StringArrays: return "string_arrays"; - case VtxDiff::EVTXContainerType::TransformArrays: return "transform_arrays"; - case VtxDiff::EVTXContainerType::VectorArrays: return "vector_arrays"; - case VtxDiff::EVTXContainerType::QuatArrays: return "quat_arrays"; - case VtxDiff::EVTXContainerType::RangeArrays: return "range_arrays"; - case VtxDiff::EVTXContainerType::BoolArrays: return "bool_arrays"; - - case VtxDiff::EVTXContainerType::AnyStructProperties: return "any_struct_properties"; - case VtxDiff::EVTXContainerType::AnyStructArrays: return "any_struct_arrays"; - case VtxDiff::EVTXContainerType::MapProperties: return "map_properties"; - case VtxDiff::EVTXContainerType::MapArrays: return "map_arrays"; - - default: return ""; + switch (Type) { + case VtxDiff::EVTXContainerType::Unknown: + return "Unknown"; + + case VtxDiff::EVTXContainerType::BoolProperties: + return "bool_properties"; + case VtxDiff::EVTXContainerType::Int32Properties: + return "int32_properties"; + case VtxDiff::EVTXContainerType::Int64Properties: + return "int64_properties"; + case VtxDiff::EVTXContainerType::FloatProperties: + return "float_properties"; + case VtxDiff::EVTXContainerType::DoubleProperties: + return "double_properties"; + case VtxDiff::EVTXContainerType::StringProperties: + return "string_properties"; + case VtxDiff::EVTXContainerType::TransformProperties: + return "transform_properties"; + case VtxDiff::EVTXContainerType::VectorProperties: + return "vector_properties"; + case VtxDiff::EVTXContainerType::QuatProperties: + return "quat_properties"; + case VtxDiff::EVTXContainerType::RangeProperties: + return "range_properties"; + + case VtxDiff::EVTXContainerType::ByteArrayProperties: + return "byte_array_properties"; + case VtxDiff::EVTXContainerType::Int32Arrays: + return "int32_arrays"; + case VtxDiff::EVTXContainerType::Int64Arrays: + return "int64_arrays"; + case VtxDiff::EVTXContainerType::FloatArrays: + return "float_arrays"; + case VtxDiff::EVTXContainerType::DoubleArrays: + return "double_arrays"; + case VtxDiff::EVTXContainerType::StringArrays: + return "string_arrays"; + case VtxDiff::EVTXContainerType::TransformArrays: + return "transform_arrays"; + case VtxDiff::EVTXContainerType::VectorArrays: + return "vector_arrays"; + case VtxDiff::EVTXContainerType::QuatArrays: + return "quat_arrays"; + case VtxDiff::EVTXContainerType::RangeArrays: + return "range_arrays"; + case VtxDiff::EVTXContainerType::BoolArrays: + return "bool_arrays"; + + case VtxDiff::EVTXContainerType::AnyStructProperties: + return "any_struct_properties"; + case VtxDiff::EVTXContainerType::AnyStructArrays: + return "any_struct_arrays"; + case VtxDiff::EVTXContainerType::MapProperties: + return "map_properties"; + case VtxDiff::EVTXContainerType::MapArrays: + return "map_arrays"; + + default: + return ""; + } } -} // ========================================================== // Enum: DiffOperation // ---------------------------------------------------------- // Represents one kind of change in a diff result. // ========================================================== - enum class DiffOperation : uint8_t { - Add, - Remove, - Replace, - ReplaceRange - }; + enum class DiffOperation : uint8_t { Add, Remove, Replace, ReplaceRange }; - - struct EnumHash - { + + struct EnumHash { template - size_t operator()(T v) const noexcept { return static_cast(v); } + size_t operator()(T v) const noexcept { + return static_cast(v); + } }; // =========================================================== // Binary path (no strings) // =========================================================== - struct DiffIndexPath - { + struct DiffIndexPath { int32_t indices[16]; uint8_t count = 0; void push_back(int32_t v) { - if (count < 16) { indices[count++] = v; } + if (count < 16) { + indices[count++] = v; + } } void pop_back() { - if (count > 0) { count--; } + if (count > 0) { + count--; + } } DiffIndexPath Append(int32_t Index) const { DiffIndexPath p = *this; @@ -151,17 +170,17 @@ namespace VtxDiff { return p; } bool IsEmpty() const { return count == 0; } - + size_t size() const { return count; } int32_t operator[](size_t i) const { return indices[i]; } bool operator==(const DiffIndexPath& o) const { - if (count != o.count) return false; + if (count != o.count) + return false; return std::memcmp(indices, o.indices, count * sizeof(int32_t)) == 0; } }; - struct DiffIndexOp - { + struct DiffIndexOp { DiffOperation Operation; EVTXContainerType ContainerType; DiffIndexPath Path; @@ -173,8 +192,7 @@ namespace VtxDiff { // =========================================================== // Patch container // =========================================================== - struct PatchIndex - { + struct PatchIndex { std::vector operations; std::unordered_map actor_id_by_key; @@ -188,11 +206,11 @@ namespace VtxDiff { // ========================================================== struct FieldDesc { std::string name; - EVTXContainerType type{ EVTXContainerType::Int32Properties }; - bool is_array_like{ false }; + EVTXContainerType type {EVTXContainerType::Int32Properties}; + bool is_array_like {false}; bool is_actors_field = false; - bool is_map_like{ false }; - uint32_t array_size{0}; + bool is_map_like {false}; + uint32_t array_size {0}; auto operator<=>(const FieldDesc&) const = default; }; @@ -202,12 +220,12 @@ namespace VtxDiff { // Represents a single patch operation (add/remove/replace). // ========================================================== struct PatchOp { - DiffOperation Operation{ DiffOperation::Replace }; + DiffOperation Operation {DiffOperation::Replace}; std::string Path; - EVTXContainerType FieldType{ EVTXContainerType::Int32Properties }; + EVTXContainerType FieldType {EVTXContainerType::Int32Properties}; std::vector Data; }; - + // ========================================================== // Struct: DiffOptions // ---------------------------------------------------------- @@ -215,14 +233,12 @@ namespace VtxDiff { // and comparison behavior. // ========================================================== struct DiffOptions { - bool compare_floats_with_epsilon{ true }; - float float_epsilon{ 1e-5f }; - bool verbose{ false }; + bool compare_floats_with_epsilon {true}; + float float_epsilon {1e-5f}; + bool verbose {false}; }; - - // ========================================================== // Struct: PathS // ---------------------------------------------------------- @@ -268,16 +284,17 @@ namespace VtxDiff { return OSS.str(); } }; - + // ========================================================== // Utility helpers for path building and byte copying. // ========================================================== inline std::vector CopyBytes(std::span data) { - return { data.begin(), data.end() }; + return {data.begin(), data.end()}; } inline std::string MakeFieldPath(const std::string& base, const std::string& field_name) { - if (base.empty()) return field_name; + if (base.empty()) + return field_name; return base + "." + field_name; } @@ -289,10 +306,8 @@ namespace VtxDiff { return base + "{" + key + "}"; } - inline constexpr bool IsArraysType(EVTXContainerType type) - { - switch (type) - { + inline constexpr bool IsArraysType(EVTXContainerType type) { + switch (type) { case EVTXContainerType::ByteArrayProperties: case EVTXContainerType::Int32Arrays: case EVTXContainerType::Int64Arrays: @@ -313,66 +328,68 @@ namespace VtxDiff { } - static bool EndsWith(const std::string& s, const char* suffix) - { + static bool EndsWith(const std::string& s, const char* suffix) { const size_t n = s.size(); const size_t m = std::strlen(suffix); - if (n < m) return false; + if (n < m) + return false; return std::memcmp(s.data() + (n - m), suffix, m) == 0; } - - static bool AreScalarsEqual(VtxDiff::EVTXContainerType type, std::span A, std::span B, const VtxDiff::DiffOptions& Opt) - { + static bool AreScalarsEqual(VtxDiff::EVTXContainerType type, std::span A, + std::span B, const VtxDiff::DiffOptions& Opt) { if (!Opt.compare_floats_with_epsilon) return std::memcmp(A.data(), B.data(), A.size()) == 0; - auto IsNear = [&](float a, float b) { return std::abs(a - b) <= Opt.float_epsilon; }; + auto IsNear = [&](float a, float b) { + return std::abs(a - b) <= Opt.float_epsilon; + }; switch (type) { - case VtxDiff::EVTXContainerType::FloatArrays: - case VtxDiff::EVTXContainerType::FloatProperties: - { - float fa, fb; - std::memcpy(&fa, A.data(), sizeof(float)); - std::memcpy(&fb, B.data(), sizeof(float)); - return IsNear(fa, fb); - } + case VtxDiff::EVTXContainerType::FloatArrays: + case VtxDiff::EVTXContainerType::FloatProperties: { + float fa, fb; + std::memcpy(&fa, A.data(), sizeof(float)); + std::memcpy(&fb, B.data(), sizeof(float)); + return IsNear(fa, fb); + } - case VtxDiff::EVTXContainerType::VectorArrays: - case VtxDiff::EVTXContainerType::VectorProperties: - { - if (A.size() < sizeof(float) * 3) return false; - const float* va = reinterpret_cast(A.data()); - const float* vb = reinterpret_cast(B.data()); - return IsNear(va[0], vb[0]) && IsNear(va[1], vb[1]) && IsNear(va[2], vb[2]); - } + case VtxDiff::EVTXContainerType::VectorArrays: + case VtxDiff::EVTXContainerType::VectorProperties: { + if (A.size() < sizeof(float) * 3) + return false; + const float* va = reinterpret_cast(A.data()); + const float* vb = reinterpret_cast(B.data()); + return IsNear(va[0], vb[0]) && IsNear(va[1], vb[1]) && IsNear(va[2], vb[2]); + } - case VtxDiff::EVTXContainerType::QuatArrays: - case VtxDiff::EVTXContainerType::QuatProperties: - { - if (A.size() < sizeof(float) * 4) return false; - const float* qa = reinterpret_cast(A.data()); - const float* qb = reinterpret_cast(B.data()); - return IsNear(qa[0], qb[0]) && IsNear(qa[1], qb[1]) && IsNear(qa[2], qb[2]) && IsNear(qa[3], qb[3]); - } + case VtxDiff::EVTXContainerType::QuatArrays: + case VtxDiff::EVTXContainerType::QuatProperties: { + if (A.size() < sizeof(float) * 4) + return false; + const float* qa = reinterpret_cast(A.data()); + const float* qb = reinterpret_cast(B.data()); + return IsNear(qa[0], qb[0]) && IsNear(qa[1], qb[1]) && IsNear(qa[2], qb[2]) && IsNear(qa[3], qb[3]); + } - case VtxDiff::EVTXContainerType::TransformArrays: - case VtxDiff::EVTXContainerType::TransformProperties: - { - if (A.size() < sizeof(float) * 10) return false; - const float* ta = reinterpret_cast(A.data()); - const float* tb = reinterpret_cast(B.data()); - for (int k = 0; k < 10; ++k) if (!IsNear(ta[k], tb[k])) return false; - return true; - } - default: ; + case VtxDiff::EVTXContainerType::TransformArrays: + case VtxDiff::EVTXContainerType::TransformProperties: { + if (A.size() < sizeof(float) * 10) + return false; + const float* ta = reinterpret_cast(A.data()); + const float* tb = reinterpret_cast(B.data()); + for (int k = 0; k < 10; ++k) + if (!IsNear(ta[k], tb[k])) + return false; + return true; + } + default:; } return std::memcmp(A.data(), B.data(), A.size()) == 0; } - + } // namespace VtxDiff diff --git a/sdk/include/vtx/differ/core/vtx_diff_validator.h b/sdk/include/vtx/differ/core/vtx_diff_validator.h index 36202fd..35be602 100644 --- a/sdk/include/vtx/differ/core/vtx_diff_validator.h +++ b/sdk/include/vtx/differ/core/vtx_diff_validator.h @@ -15,207 +15,216 @@ namespace VtxDiff { -inline uint64_t StableHash64(std::string_view s); - -template -requires CBinaryNodeView -class DiffValidator { -public: - static bool ValidatePatch(const TNodeView& NodeA, - const TNodeView& NodeB, - const PatchIndex& Patch, - const DiffOptions& Opt) - { - std::cout << "\n[VALIDATOR] Starting Patch Validator...\n"; - bool allValid = true; - int checkedOps = 0; - - for (const auto& Op : Patch.operations) { - const bool existsA = PatchAccessor::Exists(NodeA, Op); - const bool existsB = PatchAccessor::Exists(NodeB, Op); - const auto bytesA = PatchAccessor::GetRawBytes(NodeA, Op); - const auto bytesB = PatchAccessor::GetRawBytes(NodeB, Op); - - switch (Op.Operation) { - case DiffOperation::Replace: - if (!existsA || !existsB) { - std::cerr << "[FAIL] Replace on missing data. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } else if (bytesA.empty() || bytesB.empty()) { - std::cerr << "[FAIL] Replace without readable bytes. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } else if (AreBytesEqual(bytesA, bytesB, Opt)) { - std::cerr << "[FAIL] False positive replace. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; + inline uint64_t StableHash64(std::string_view s); + + template + requires CBinaryNodeView + class DiffValidator { + public: + static bool ValidatePatch(const TNodeView& NodeA, const TNodeView& NodeB, const PatchIndex& Patch, + const DiffOptions& Opt) { + std::cout << "\n[VALIDATOR] Starting Patch Validator...\n"; + bool allValid = true; + int checkedOps = 0; + + for (const auto& Op : Patch.operations) { + const bool existsA = PatchAccessor::Exists(NodeA, Op); + const bool existsB = PatchAccessor::Exists(NodeB, Op); + const auto bytesA = PatchAccessor::GetRawBytes(NodeA, Op); + const auto bytesB = PatchAccessor::GetRawBytes(NodeB, Op); + + switch (Op.Operation) { + case DiffOperation::Replace: + if (!existsA || !existsB) { + std::cerr << "[FAIL] Replace on missing data. Path: " << DecodePath(Patch, Op) << "\n"; + allValid = false; + } else if (bytesA.empty() || bytesB.empty()) { + std::cerr << "[FAIL] Replace without readable bytes. Path: " << DecodePath(Patch, Op) << "\n"; + allValid = false; + } else if (AreBytesEqual(bytesA, bytesB, Opt)) { + std::cerr << "[FAIL] False positive replace. Path: " << DecodePath(Patch, Op) << "\n"; + allValid = false; + } + break; + + case DiffOperation::ReplaceRange: + if (!existsA || !existsB) { + std::cerr << "[FAIL] ReplaceRange on missing data. Path: " << DecodePath(Patch, Op) << "\n"; + allValid = false; + } else if (bytesA.empty() || bytesB.empty()) { + std::cerr << "[FAIL] ReplaceRange without readable bytes. Path: " << DecodePath(Patch, Op) + << "\n"; + allValid = false; + } else if (AreBytesEqual(bytesA, bytesB, Opt)) { + std::cerr << "[FAIL] False positive replace range. Path: " << DecodePath(Patch, Op) << "\n"; + allValid = false; + } + break; + + case DiffOperation::Add: + if (existsA) { + std::cerr << "[FAIL] Add op but node already exists in A. Path: " << DecodePath(Patch, Op) + << "\n"; + allValid = false; + } + if (!existsB) { + std::cerr << "[FAIL] Add op but node missing in B. Path: " << DecodePath(Patch, Op) << "\n"; + allValid = false; + } + break; + + case DiffOperation::Remove: + if (!existsA) { + std::cerr << "[FAIL] Remove op but node missing in A. Path: " << DecodePath(Patch, Op) << "\n"; + allValid = false; + } + if (existsB) { + std::cerr << "[FAIL] Remove op but node still exists in B. Path: " << DecodePath(Patch, Op) + << "\n"; + allValid = false; + } + break; } - break; - - case DiffOperation::ReplaceRange: - if (!existsA || !existsB) { - std::cerr << "[FAIL] ReplaceRange on missing data. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } else if (bytesA.empty() || bytesB.empty()) { - std::cerr << "[FAIL] ReplaceRange without readable bytes. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } else if (AreBytesEqual(bytesA, bytesB, Opt)) { - std::cerr << "[FAIL] False positive replace range. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } - break; - case DiffOperation::Add: - if (existsA) { - std::cerr << "[FAIL] Add op but node already exists in A. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } - if (!existsB) { - std::cerr << "[FAIL] Add op but node missing in B. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } - break; + checkedOps++; + } - case DiffOperation::Remove: - if (!existsA) { - std::cerr << "[FAIL] Remove op but node missing in A. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } - if (existsB) { - std::cerr << "[FAIL] Remove op but node still exists in B. Path: " << DecodePath(Patch, Op) << "\n"; - allValid = false; - } - break; + if (allValid) { + std::cout << "[PASS] " << checkedOps << " operations verified successfully.\n"; + } else { + std::cout << "[WARN] Validation found inconsistencies.\n"; } - checkedOps++; + return allValid; } - if (allValid) { - std::cout << "[PASS] " << checkedOps << " operations verified successfully.\n"; - } else { - std::cout << "[WARN] Validation found inconsistencies.\n"; - } + static void PrintPatch(const PatchIndex& Patch, size_t max_ops_to_print = 50) { + std::cout << "\n================================================================================\n"; + std::cout << " PATCH DEBUG PRINTER \n"; + std::cout << "================================================================================\n"; + std::cout << "Total Ops: " << Patch.operations.size() << "\n"; + std::cout << "--------------------------------------------------------------------------------\n"; + + const size_t limit = std::min(Patch.operations.size(), max_ops_to_print); + for (size_t i = 0; i < limit; ++i) { + const auto& op = Patch.operations[i]; + + std::string op_name; + switch (op.Operation) { + case DiffOperation::Add: + op_name = "[ADD]"; + break; + case DiffOperation::Remove: + op_name = "[REMOVE]"; + break; + case DiffOperation::Replace: + op_name = "[REPLACE]"; + break; + case DiffOperation::ReplaceRange: + op_name = "[REPLACE RANGE]"; + break; + } - return allValid; - } - - static void PrintPatch(const PatchIndex& Patch, size_t max_ops_to_print = 50) { - std::cout << "\n================================================================================\n"; - std::cout << " PATCH DEBUG PRINTER \n"; - std::cout << "================================================================================\n"; - std::cout << "Total Ops: " << Patch.operations.size() << "\n"; - std::cout << "--------------------------------------------------------------------------------\n"; - - const size_t limit = std::min(Patch.operations.size(), max_ops_to_print); - for (size_t i = 0; i < limit; ++i) { - const auto& op = Patch.operations[i]; - - std::string op_name; - switch (op.Operation) { - case DiffOperation::Add: op_name = "[ADD]"; break; - case DiffOperation::Remove: op_name = "[REMOVE]"; break; - case DiffOperation::Replace: op_name = "[REPLACE]"; break; - case DiffOperation::ReplaceRange: op_name = "[REPLACE RANGE]"; break; + std::cout << std::left << std::setw(17) << op_name << " | Field: " << std::setw(22) + << TypeToFieldName(op.ContainerType); + if (op.Operation == DiffOperation::ReplaceRange) { + std::cout << " | Count: " << op.ReplaceRangeCount; + } + std::cout << " -> " << DecodePath(Patch, op) << "\n"; } - std::cout << std::left << std::setw(17) << op_name - << " | Field: " << std::setw(22) << TypeToFieldName(op.ContainerType); - if (op.Operation == DiffOperation::ReplaceRange) { - std::cout << " | Count: " << op.ReplaceRangeCount; + if (Patch.operations.size() > limit) { + std::cout << "\n " << (Patch.operations.size() - limit) << " hidden ops.\n"; } - std::cout << " -> " << DecodePath(Patch, op) << "\n"; - } - - if (Patch.operations.size() > limit) { - std::cout << "\n " << (Patch.operations.size() - limit) << " hidden ops.\n"; + std::cout << "================================================================================\n\n"; } - std::cout << "================================================================================\n\n"; - } -private: - static bool AreBytesEqual(std::span BytesA, std::span BytesB, const DiffOptions& Opt) { - if (BytesA.size() != BytesB.size()) { - return false; - } + private: + static bool AreBytesEqual(std::span BytesA, std::span BytesB, + const DiffOptions& Opt) { + if (BytesA.size() != BytesB.size()) { + return false; + } - bool different = false; - if (Opt.compare_floats_with_epsilon && BytesA.size() == BytesB.size()) { - if (BytesA.size() == sizeof(float)) { - float fa = 0.0f; - float fb = 0.0f; - std::memcpy(&fa, BytesA.data(), sizeof(float)); - std::memcpy(&fb, BytesB.data(), sizeof(float)); - different = std::abs(fa - fb) > Opt.float_epsilon; - } else if (BytesA.size() == sizeof(double)) { - double da = 0.0; - double db = 0.0; - std::memcpy(&da, BytesA.data(), sizeof(double)); - std::memcpy(&db, BytesB.data(), sizeof(double)); - different = std::abs(da - db) > static_cast(Opt.float_epsilon); + bool different = false; + if (Opt.compare_floats_with_epsilon && BytesA.size() == BytesB.size()) { + if (BytesA.size() == sizeof(float)) { + float fa = 0.0f; + float fb = 0.0f; + std::memcpy(&fa, BytesA.data(), sizeof(float)); + std::memcpy(&fb, BytesB.data(), sizeof(float)); + different = std::abs(fa - fb) > Opt.float_epsilon; + } else if (BytesA.size() == sizeof(double)) { + double da = 0.0; + double db = 0.0; + std::memcpy(&da, BytesA.data(), sizeof(double)); + std::memcpy(&db, BytesB.data(), sizeof(double)); + different = std::abs(da - db) > static_cast(Opt.float_epsilon); + } else { + different = std::memcmp(BytesA.data(), BytesB.data(), BytesA.size()) != 0; + } } else { different = std::memcmp(BytesA.data(), BytesB.data(), BytesA.size()) != 0; } - } else { - different = std::memcmp(BytesA.data(), BytesB.data(), BytesA.size()) != 0; + + return !different; } - return !different; - } + static std::string DecodePath(const PatchIndex& Patch, const DiffIndexOp& Op) { + if (Op.Path.size() == 0) { + return "root"; + } - static std::string DecodePath(const PatchIndex& Patch, const DiffIndexOp& Op) { - if (Op.Path.size() == 0) { - return "root"; - } + std::string result = "root"; + for (size_t i = 0; i < Op.Path.size(); ++i) { + const int32_t value = Op.Path.indices[i]; - std::string result = "root"; - for (size_t i = 0; i < Op.Path.size(); ++i) { - const int32_t value = Op.Path.indices[i]; + if (i == 0) { + const auto type = static_cast(value); + result += (type == EVTXContainerType::AnyStructArrays) ? ".entities" : "." + TypeToFieldName(type); + continue; + } - if (i == 0) { - const auto type = static_cast(value); - result += (type == EVTXContainerType::AnyStructArrays) ? ".entities" : "." + TypeToFieldName(type); - continue; - } + if (i == 1 && Op.Path.indices[0] == static_cast(EVTXContainerType::AnyStructArrays)) { + auto it = Patch.actor_id_by_key.find(value); + if (it != Patch.actor_id_by_key.end()) { + result += "[\"" + it->second + "\"]"; + } else if (!Op.ActorId.empty()) { + result += "[\"" + Op.ActorId + "\"]"; + } else { + result += "[Hash:" + std::to_string(value) + "]"; + } + continue; + } - if (i == 1 && Op.Path.indices[0] == static_cast(EVTXContainerType::AnyStructArrays)) { - auto it = Patch.actor_id_by_key.find(value); - if (it != Patch.actor_id_by_key.end()) { - result += "[\"" + it->second + "\"]"; - } else if (!Op.ActorId.empty()) { - result += "[\"" + Op.ActorId + "\"]"; - } else { - result += "[Hash:" + std::to_string(value) + "]"; + const int32_t previous = Op.Path.indices[i - 1]; + if (previous == static_cast(EVTXContainerType::MapProperties) || + previous == static_cast(EVTXContainerType::MapArrays)) { + if (!Op.MapKey.empty()) { + result += "{\"" + Op.MapKey + "\"}"; + } else { + result += "{Hash:" + std::to_string(value) + "}"; + } + continue; } - continue; - } - const int32_t previous = Op.Path.indices[i - 1]; - if (previous == static_cast(EVTXContainerType::MapProperties) || - previous == static_cast(EVTXContainerType::MapArrays)) { - if (!Op.MapKey.empty()) { - result += "{\"" + Op.MapKey + "\"}"; - } else { - result += "{Hash:" + std::to_string(value) + "}"; + if (previous >= static_cast(EVTXContainerType::BoolProperties) && + previous <= static_cast(EVTXContainerType::MapArrays)) { + result += "[" + std::to_string(value) + "]"; + continue; } - continue; - } - if (previous >= static_cast(EVTXContainerType::BoolProperties) && - previous <= static_cast(EVTXContainerType::MapArrays)) { - result += "[" + std::to_string(value) + "]"; - continue; + const auto type = static_cast(value); + const std::string field_name = TypeToFieldName(type); + if (!field_name.empty()) { + result += "." + field_name; + } else { + result += "[" + std::to_string(value) + "]"; + } } - const auto type = static_cast(value); - const std::string field_name = TypeToFieldName(type); - if (!field_name.empty()) { - result += "." + field_name; - } else { - result += "[" + std::to_string(value) + "]"; - } + return result; } - - return result; - } -}; + }; } // namespace VtxDiff diff --git a/sdk/include/vtx/differ/core/vtx_differ_facade.h b/sdk/include/vtx/differ/core/vtx_differ_facade.h index db1a974..899ee21 100644 --- a/sdk/include/vtx/differ/core/vtx_differ_facade.h +++ b/sdk/include/vtx/differ/core/vtx_differ_facade.h @@ -12,7 +12,8 @@ namespace VtxDiff { public: virtual ~IVtxDifferFacade() = default; - virtual PatchIndex DiffRawFrames(std::span frame_a,std::span frame_b,const DiffOptions& options = {}) = 0; + virtual PatchIndex DiffRawFrames(std::span frame_a, std::span frame_b, + const DiffOptions& options = {}) = 0; }; // Returns nullptr only if format is Unknown. diff --git a/sdk/include/vtx/differ/core/vtx_patch.h b/sdk/include/vtx/differ/core/vtx_patch.h index ff899d2..fe5b431 100644 --- a/sdk/include/vtx/differ/core/vtx_patch.h +++ b/sdk/include/vtx/differ/core/vtx_patch.h @@ -8,11 +8,10 @@ #include "vtx/differ/core/vtx_default_tree_diff.h" #include "vtx/differ/core/vtx_diff_types.h" -namespace DiffUtils -{ - template - VtxDiff::PatchIndex DiffSingleActorByIndex(const TNodeView& FrameA, const TNodeView& FrameB, uint32_t ActorIndexA, uint32_t ActorIndexB, const VtxDiff::DiffOptions& Opt) - { +namespace DiffUtils { + template + VtxDiff::PatchIndex DiffSingleActorByIndex(const TNodeView& FrameA, const TNodeView& FrameB, uint32_t ActorIndexA, + uint32_t ActorIndexB, const VtxDiff::DiffOptions& Opt) { VtxDiff::FieldDesc entities_field; entities_field.name = "entities"; entities_field.type = VtxDiff::EVTXContainerType::AnyStructArrays; @@ -23,12 +22,12 @@ namespace DiffUtils VtxDiff::DefaultTreeDiff differ; return differ.ComputeEntityDiff(entity_a, entity_b, Opt); } -} +} // namespace DiffUtils namespace VtxDiff { template - requires CBinaryNodeView + requires CBinaryNodeView class PatchAccessor { public: static TNodeView GetTargetNode(const TNodeView& Root, const DiffIndexPath& Path) { @@ -44,14 +43,12 @@ namespace VtxDiff { } private: - static constexpr size_t InvalidIndex() { - return std::numeric_limits::max(); - } + static constexpr size_t InvalidIndex() { return std::numeric_limits::max(); } struct ResolvedPath { bool Exists = false; - std::span Bytes{}; - TNodeView Node{}; + std::span Bytes {}; + TNodeView Node {}; }; static FieldDesc MakeField(EVTXContainerType Type) { @@ -83,14 +80,16 @@ namespace VtxDiff { const size_t count = Node.GetArraySize(entities_field); for (size_t i = 0; i < count; ++i) { auto actor = Node.GetArrayElementAsStruct(entities_field, i); - if (!actor.IsValid()) continue; + if (!actor.IsValid()) + continue; auto span_id = actor.GetFieldBytes(id_field); - if (span_id.empty()) continue; + if (span_id.empty()) + continue; - const int32_t current_hash = static_cast(VtxDiff::StableHash64( - { reinterpret_cast(span_id.data()), span_id.size() } - ) & 0x7FFFFFFF); + const int32_t current_hash = static_cast( + VtxDiff::StableHash64({reinterpret_cast(span_id.data()), span_id.size()}) & + 0x7FFFFFFF); if (current_hash == TargetHash) { return i; @@ -99,15 +98,19 @@ namespace VtxDiff { return InvalidIndex(); } - static size_t FindMapIndexByHash(const TNodeView& Node, const FieldDesc& Field, int32_t TargetHash, const std::string& ExpectedKey) { + static size_t FindMapIndexByHash(const TNodeView& Node, const FieldDesc& Field, int32_t TargetHash, + const std::string& ExpectedKey) { const size_t count = Node.GetMapSize(Field); for (size_t i = 0; i < count; ++i) { const std::string key = Node.GetMapKey(Field, i); - if (key.empty()) continue; + if (key.empty()) + continue; const int32_t key_hash = static_cast(VtxDiff::StableHash64(key) & 0x7FFFFFFF); - if (key_hash != TargetHash) continue; - if (!ExpectedKey.empty() && key != ExpectedKey) continue; + if (key_hash != TargetHash) + continue; + if (!ExpectedKey.empty() && key != ExpectedKey) + continue; return i; } return InvalidIndex(); @@ -119,7 +122,7 @@ namespace VtxDiff { } if (Path.size() == 0) { - return { true, {}, Root }; + return {true, {}, Root}; } if (Path[0] == static_cast(EVTXContainerType::AnyStructArrays) && Path.size() >= 2) { @@ -138,7 +141,7 @@ namespace VtxDiff { } if (Path.size() == 2) { - return { true, Root.GetArrayElementBytes(entities_field, actor_index), actor_node }; + return {true, Root.GetArrayElementBytes(entities_field, actor_index), actor_node}; } return ResolveNode(actor_node, Path, 2, MapKey); @@ -147,7 +150,8 @@ namespace VtxDiff { return ResolveNode(Root, Path, 0, MapKey); } - static ResolvedPath ResolveNode(const TNodeView& Node, const DiffIndexPath& Path, size_t PathIndex, const std::string& MapKey) { + static ResolvedPath ResolveNode(const TNodeView& Node, const DiffIndexPath& Path, size_t PathIndex, + const std::string& MapKey) { if (!Node.IsValid() || PathIndex >= Path.size()) { return {}; } @@ -157,7 +161,7 @@ namespace VtxDiff { const bool field_exists = HasField(Node, field); if (PathIndex + 1 >= Path.size()) { - return { field_exists, Node.GetFieldBytes(field), Node.GetNestedStruct(field) }; + return {field_exists, Node.GetFieldBytes(field), Node.GetNestedStruct(field)}; } const int32_t RawIndex = Path[PathIndex + 1]; @@ -177,7 +181,7 @@ namespace VtxDiff { } if (PathIndex + 2 >= Path.size()) { - return { true, Node.GetArrayElementBytes(field, struct_index), child }; + return {true, Node.GetArrayElementBytes(field, struct_index), child}; } return ResolveNode(child, Path, PathIndex + 2, MapKey); @@ -191,11 +195,11 @@ namespace VtxDiff { auto child = Node.GetMapValueAsStruct(field, map_index); if (!child.IsValid()) { - return { true, {}, {} }; + return {true, {}, {}}; } if (PathIndex + 2 >= Path.size()) { - return { true, {}, child }; + return {true, {}, child}; } return ResolveNode(child, Path, PathIndex + 2, MapKey); @@ -207,7 +211,7 @@ namespace VtxDiff { return {}; } - return { true, Node.GetSubArrayBytes(field, array_index), {} }; + return {true, Node.GetSubArrayBytes(field, array_index), {}}; } const size_t element_index = static_cast(RawIndex); @@ -215,7 +219,7 @@ namespace VtxDiff { return {}; } - return { true, Node.GetArrayElementBytes(field, element_index), {} }; + return {true, Node.GetArrayElementBytes(field, element_index), {}}; } }; -} +} // namespace VtxDiff diff --git a/sdk/include/vtx/reader/core/vtx_deserializer_service.h b/sdk/include/vtx/reader/core/vtx_deserializer_service.h index 541f43b..31c2bd6 100644 --- a/sdk/include/vtx/reader/core/vtx_deserializer_service.h +++ b/sdk/include/vtx/reader/core/vtx_deserializer_service.h @@ -3,15 +3,14 @@ #include #include -namespace cppvtx -{ +namespace cppvtx { class FrameChunk; class Frame; -} +} // namespace cppvtx namespace VTX { namespace ReplayUnpacker { std::string Decompress(const std::string& compressed_data); std::vector> Unpack(const cppvtx::FrameChunk& chunk); - }; -} + }; // namespace ReplayUnpacker +} // namespace VTX diff --git a/sdk/include/vtx/reader/core/vtx_frame_accessor.h b/sdk/include/vtx/reader/core/vtx_frame_accessor.h index 184309f..755142c 100644 --- a/sdk/include/vtx/reader/core/vtx_frame_accessor.h +++ b/sdk/include/vtx/reader/core/vtx_frame_accessor.h @@ -2,10 +2,9 @@ #include "vtx/common/vtx_concepts.h" #include "vtx/common/vtx_logger.h" #include "vtx/common/vtx_property_cache.h" -#include +#include -namespace VTX -{ +namespace VTX { struct PropertyMetadata { std::string name; VTX::FieldType type; @@ -16,31 +15,46 @@ namespace VTX const PropertyContainer* data_ = nullptr; public: - EntityView() : data_(nullptr) {} - explicit EntityView(const PropertyContainer& data) : data_(&data) {} + EntityView() + : data_(nullptr) {} + explicit EntityView(const PropertyContainer& data) + : data_(&data) {} template static constexpr auto GetContainerMember() { - if constexpr (std::same_as) return &PropertyContainer::bool_properties; - else if constexpr (std::same_as) return &PropertyContainer::int32_properties; - else if constexpr (std::same_as) return &PropertyContainer::int64_properties; - else if constexpr (std::same_as) return &PropertyContainer::float_properties; - else if constexpr (std::same_as) return &PropertyContainer::double_properties; - else if constexpr (std::same_as) return &PropertyContainer::string_properties; - else if constexpr (std::same_as) return &PropertyContainer::vector_properties; - else if constexpr (std::same_as) return &PropertyContainer::quat_properties; - else if constexpr (std::same_as) return &PropertyContainer::transform_properties; - else if constexpr (std::same_as) return &PropertyContainer::range_properties; - else if constexpr (std::same_as) return &PropertyContainer::any_struct_properties; - else static_assert(std::same_as, "type not supported in EntityView"); + if constexpr (std::same_as) + return &PropertyContainer::bool_properties; + else if constexpr (std::same_as) + return &PropertyContainer::int32_properties; + else if constexpr (std::same_as) + return &PropertyContainer::int64_properties; + else if constexpr (std::same_as) + return &PropertyContainer::float_properties; + else if constexpr (std::same_as) + return &PropertyContainer::double_properties; + else if constexpr (std::same_as) + return &PropertyContainer::string_properties; + else if constexpr (std::same_as) + return &PropertyContainer::vector_properties; + else if constexpr (std::same_as) + return &PropertyContainer::quat_properties; + else if constexpr (std::same_as) + return &PropertyContainer::transform_properties; + else if constexpr (std::same_as) + return &PropertyContainer::range_properties; + else if constexpr (std::same_as) + return &PropertyContainer::any_struct_properties; + else + static_assert(std::same_as, "type not supported in EntityView"); } - - template using ScalarRetType = std::conditional_t, bool, const U&>; + + template + using ScalarRetType = std::conditional_t, bool, const U&>; template ScalarRetType Get(PropertyKey key) const { if (!data_ || !key.IsValid()) { - static const T default_val = {}; + static const T default_val = {}; return default_val; } constexpr auto MemberPtr = GetContainerMember(); @@ -55,43 +69,57 @@ namespace VTX template auto GetArray(PropertyKey key) const { constexpr auto MemberPtr = GetArrayContainerMember(); - + using ExactSpanType = decltype((data_->*MemberPtr).GetSubArray(0)); - + if (!data_ || !key.IsValid()) { - return ExactSpanType{}; + return ExactSpanType {}; } - - return (data_->*MemberPtr).GetSubArray(key.index); + + return (data_->*MemberPtr).GetSubArray(key.index); } - + EntityView GetView(PropertyKey key) const { - if (!data_ || !key.IsValid() || static_cast(key.index) >= data_->any_struct_properties.size()) return EntityView{}; + if (!data_ || !key.IsValid() || static_cast(key.index) >= data_->any_struct_properties.size()) + return EntityView {}; return EntityView(data_->any_struct_properties[key.index]); } std::span GetViewArray(PropertyKey> key) const { - if (!data_ || !key.IsValid()) return {}; + if (!data_ || !key.IsValid()) + return {}; return data_->any_struct_arrays.GetSubArray(key.index); } template static constexpr auto GetArrayContainerMember() { - if constexpr (std::is_same_v) return &PropertyContainer::int32_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::int64_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::float_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::double_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::string_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::vector_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::quat_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::transform_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::range_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::bool_arrays; - else if constexpr (std::is_same_v) return &PropertyContainer::byte_array_properties; - else if constexpr (std::is_same_v) return &PropertyContainer::any_struct_arrays; - else static_assert(std::same_as, "type not supported in EntityView"); + if constexpr (std::is_same_v) + return &PropertyContainer::int32_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::int64_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::float_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::double_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::string_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::vector_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::quat_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::transform_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::range_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::bool_arrays; + else if constexpr (std::is_same_v) + return &PropertyContainer::byte_array_properties; + else if constexpr (std::is_same_v) + return &PropertyContainer::any_struct_arrays; + else + static_assert(std::same_as, "type not supported in EntityView"); } - }; class FrameAccessor { @@ -106,21 +134,20 @@ namespace VTX } return -1; } - + public: void Initialize(const SchemaAdaptable auto& schema) { SchemaAdapter>::BuildCache(schema, cache_); } - template requires SchemaAdaptable + template + requires SchemaAdaptable void Initialize(const std::unique_ptr& schema) { //if (schema) SchemaAdapter::BuildCache(*schema, cache_); } - - void InitializeFromCache(const PropertyAddressCache& prebuilt_cache) { - cache_ = prebuilt_cache; - } - + + void InitializeFromCache(const PropertyAddressCache& prebuilt_cache) { cache_ = prebuilt_cache; } + template PropertyKey Get(int32_t structId, const std::string& propName) const { auto structIt = cache_.structs.find(structId); @@ -128,14 +155,15 @@ namespace VTX auto propIt = structIt->second.properties.find(propName); if (propIt != structIt->second.properties.end()) { const PropertyAddress& addr = propIt->second; - if (addr.type_id == GetExpectedFieldType() && addr.container_type == VTX::FieldContainerType::None) { - return PropertyKey{ static_cast(addr.index) }; + if (addr.type_id == GetExpectedFieldType() && + addr.container_type == VTX::FieldContainerType::None) { + return PropertyKey {static_cast(addr.index)}; } else { VTX_ERROR("Type mismatch for struct ID {}. Property: {}", structId, propName); } } } - return PropertyKey{ -1 }; + return PropertyKey {-1}; } template , int> = 0> @@ -149,9 +177,9 @@ namespace VTX if (structId != -1) { return Get(structId, propName); } - return PropertyKey{ -1 }; + return PropertyKey {-1}; } - + template PropertyKey GetArray(int32_t structId, const std::string& propName) const { auto structIt = cache_.structs.find(structId); @@ -159,12 +187,13 @@ namespace VTX auto propIt = structIt->second.properties.find(propName); if (propIt != structIt->second.properties.end()) { const PropertyAddress& addr = propIt->second; - if (addr.type_id == GetExpectedFieldType() && addr.container_type == VTX::FieldContainerType::Array) { - return PropertyKey{ static_cast(addr.index) }; + if (addr.type_id == GetExpectedFieldType() && + addr.container_type == VTX::FieldContainerType::Array) { + return PropertyKey {static_cast(addr.index)}; } } } - return PropertyKey{ -1 }; + return PropertyKey {-1}; } template , int> = 0> @@ -178,23 +207,24 @@ namespace VTX if (structId != -1) { return GetArray(structId, propName); } - return PropertyKey{ -1 }; + return PropertyKey {-1}; } - + PropertyKey GetViewKey(int32_t structId, const std::string& propName) const { auto structIt = cache_.structs.find(structId); if (structIt != cache_.structs.end()) { auto propIt = structIt->second.properties.find(propName); if (propIt != structIt->second.properties.end()) { const PropertyAddress& addr = propIt->second; - if (addr.type_id == VTX::FieldType::Struct && addr.container_type == VTX::FieldContainerType::None) { - return PropertyKey{ static_cast(addr.index) }; + if (addr.type_id == VTX::FieldType::Struct && + addr.container_type == VTX::FieldContainerType::None) { + return PropertyKey {static_cast(addr.index)}; } else { VTX_ERROR("Type mismatch for struct ID {}. Property: {}", structId, propName); } } } - return PropertyKey{ -1 }; + return PropertyKey {-1}; } template , int> = 0> @@ -207,43 +237,47 @@ namespace VTX if (structId != -1) { return GetViewKey(structId, propName); } - return PropertyKey{ -1 }; + return PropertyKey {-1}; } - - - PropertyKey> GetViewArrayKey(int32_t structId, const std::string& propName) const { + + + PropertyKey> GetViewArrayKey(int32_t structId, + const std::string& propName) const { auto structIt = cache_.structs.find(structId); if (structIt != cache_.structs.end()) { auto propIt = structIt->second.properties.find(propName); if (propIt != structIt->second.properties.end()) { const PropertyAddress& addr = propIt->second; - if (addr.type_id == VTX::FieldType::Struct && addr.container_type == VTX::FieldContainerType::Array) { - return PropertyKey>{ static_cast(addr.index) }; + if (addr.type_id == VTX::FieldType::Struct && + addr.container_type == VTX::FieldContainerType::Array) { + return PropertyKey> {static_cast(addr.index)}; } else { VTX_ERROR("Type mismatch for struct ID {}. Property: {}", structId, propName); } } } - return PropertyKey>{ -1 }; + return PropertyKey> {-1}; } template , int> = 0> - PropertyKey> GetViewArrayKey(EnumType structEnum, const std::string& propName) const { + PropertyKey> GetViewArrayKey(EnumType structEnum, + const std::string& propName) const { return GetViewArrayKey(static_cast(structEnum), propName); } - PropertyKey> GetViewArrayKey(const std::string& structName, const std::string& propName) const { + PropertyKey> GetViewArrayKey(const std::string& structName, + const std::string& propName) const { const int32_t structId = FindStructId(structName); if (structId != -1) { return GetViewArrayKey(structId, propName); } - return PropertyKey>{ -1 }; + return PropertyKey> {-1}; } - + std::vector GetAvailableStructNames() const { std::vector names; - names.reserve(cache_.name_to_id.size()); + names.reserve(cache_.name_to_id.size()); for (const auto& [name, id] : cache_.name_to_id) { names.push_back(name); } @@ -272,7 +306,8 @@ namespace VTX bool HasProperty(int32_t structId, const std::string& propName) const { auto it = cache_.structs.find(structId); - if (it != cache_.structs.end()) return it->second.properties.contains(propName); + if (it != cache_.structs.end()) + return it->second.properties.contains(propName); return false; } @@ -284,4 +319,4 @@ namespace VTX return false; } }; -} +} // namespace VTX diff --git a/sdk/include/vtx/reader/core/vtx_reader.h b/sdk/include/vtx/reader/core/vtx_reader.h index 2d72ebd..6598696 100644 --- a/sdk/include/vtx/reader/core/vtx_reader.h +++ b/sdk/include/vtx/reader/core/vtx_reader.h @@ -47,7 +47,8 @@ namespace VTX { using FooterType = typename SerializerPolicy::FooterType; using SchemaType = typename SerializerPolicy::SchemaType; - explicit ReplayReader(const std::string& filepath) : filepath_(filepath) { + explicit ReplayReader(const std::string& filepath) + : filepath_(filepath) { std::ifstream init_stream(filepath_, std::ios::binary | std::ios::in); if (!init_stream.is_open()) { throw std::runtime_error("VTX Reader: Can not open the file " + filepath); @@ -74,11 +75,14 @@ namespace VTX { std::lock_guard lock(cache_mutex_); for (auto& kv : pending_loads_) { kv.second.stop.request_stop(); - if (kv.second.future.valid()) tasks.push_back(kv.second.future); + if (kv.second.future.valid()) + tasks.push_back(kv.second.future); } } - for (auto& task : tasks) { task.wait(); } + for (auto& task : tasks) { + task.wait(); + } } // Fix A4: protect events_ with a mutex. std::function is not @@ -96,6 +100,7 @@ namespace VTX { std::lock_guard lock(events_mutex_); return events_; } + public: int32_t GetTotalFrames() const { return SerializerPolicy::GetTotalFrames(footer_); } const std::vector& GetSeekTable() const { return chunk_index_table_; } @@ -130,9 +135,8 @@ namespace VTX { // it contributes to the EWMA reflects the real jump the caller // just committed to. void WarmAt(int32_t frame_index) { - auto it = std::lower_bound( - chunk_index_table_.begin(), chunk_index_table_.end(), frame_index, - [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); + auto it = std::lower_bound(chunk_index_table_.begin(), chunk_index_table_.end(), frame_index, + [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); if (it == chunk_index_table_.end() || frame_index < it->start_frame) { return; } @@ -141,7 +145,7 @@ namespace VTX { std::span GetRawFrameBytes(int32_t frame_index) { auto it = std::lower_bound(chunk_index_table_.begin(), chunk_index_table_.end(), frame_index, - [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); + [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); if (it == chunk_index_table_.end() || frame_index < it->start_frame) { return {}; @@ -185,9 +189,10 @@ namespace VTX { bool GetFrame(int32_t frame_index, VTX::Frame& out_frame) { auto it = std::lower_bound(chunk_index_table_.begin(), chunk_index_table_.end(), frame_index, - [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); + [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); - if (it == chunk_index_table_.end() || frame_index < it->start_frame) return false; + if (it == chunk_index_table_.end() || frame_index < it->start_frame) + return false; int32_t target_chunk = it->chunk_index; int32_t relative_idx = frame_index - it->start_frame; @@ -234,9 +239,10 @@ namespace VTX { const VTX::Frame* GetFramePtr(int32_t frame_index) { auto it = std::lower_bound(chunk_index_table_.begin(), chunk_index_table_.end(), frame_index, - [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); + [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); - if (it == chunk_index_table_.end() || frame_index < it->start_frame) return nullptr; + if (it == chunk_index_table_.end() || frame_index < it->start_frame) + return nullptr; int32_t target_chunk = it->chunk_index; int32_t relative_idx = frame_index - it->start_frame; @@ -256,7 +262,7 @@ namespace VTX { const VTX::Frame* GetFramePtrSync(int32_t frame_index) { auto it = std::lower_bound(chunk_index_table_.begin(), chunk_index_table_.end(), frame_index, - [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); + [](const ChunkIndexEntry& e, int32_t val) { return e.end_frame < val; }); if (it == chunk_index_table_.end() || frame_index < it->start_frame) { return nullptr; @@ -300,9 +306,10 @@ namespace VTX { } int32_t GetChunkFrameCountSafe(int32_t chunk_index) { - std::lock_guard lock(cache_mutex_); - if (chunk_cache_.contains(chunk_index)) return chunk_cache_[chunk_index].native_frames.size(); - return 0; + std::lock_guard lock(cache_mutex_); + if (chunk_cache_.contains(chunk_index)) + return chunk_cache_[chunk_index].native_frames.size(); + return 0; } FrameAccessor CreateAccessor() const { @@ -312,32 +319,21 @@ namespace VTX { } void InspectChunkHeader(int32_t index) const { - if (index < 0 || index >= chunk_index_table_.size()) return; + if (index < 0 || index >= chunk_index_table_.size()) + return; const auto& entry = chunk_index_table_[index]; - VTX_INFO("--- Chunk Inspection --- Index: {} Offset: {} Size: {}", entry.chunk_index, entry.file_offset, entry.chunk_size_bytes); - } - - VTX::FileHeader GetFileHeader() - { - return SerializerPolicy::GetVTXHeader(header_); + VTX_INFO("--- Chunk Inspection --- Index: {} Offset: {} Size: {}", entry.chunk_index, entry.file_offset, + entry.chunk_size_bytes); } - VTX::FileFooter GetFileFooter() - { - return SerializerPolicy::GetVTXFooter(footer_); - } + VTX::FileHeader GetFileHeader() { return SerializerPolicy::GetVTXHeader(header_); } - VTX::ContextualSchema GetContextualSchema() - { - return SerializerPolicy::GetVTXContextualSchema(header_); - } + VTX::FileFooter GetFileFooter() { return SerializerPolicy::GetVTXFooter(footer_); } + VTX::ContextualSchema GetContextualSchema() { return SerializerPolicy::GetVTXContextualSchema(header_); } - VTX::PropertyAddressCache GetPropertyAddressCache() - { - return property_address_cache_; - } + VTX::PropertyAddressCache GetPropertyAddressCache() { return property_address_cache_; } private: struct CachedChunk { @@ -357,7 +353,7 @@ namespace VTX { stream.seekg(0, std::ios::end); const auto end = stream.tellg(); stream.seekg(prev); - return (end < 0) ? int64_t{0} : static_cast(end); + return (end < 0) ? int64_t {0} : static_cast(end); } void ReadHeader(std::ifstream& stream) { @@ -422,8 +418,7 @@ namespace VTX { // Fix A3: guard against an implausible footer_size (either // malicious UINT32_MAX or garbage from a corrupt trailer) that // would seek before the beginning of the file. - if (footer_size == 0 || - static_cast(footer_size) + 8 > file_size) { + if (footer_size == 0 || static_cast(footer_size) + 8 > file_size) { throw std::runtime_error("VTX file has implausible footer size"); } @@ -494,18 +489,15 @@ namespace VTX { if (last_requested_chunk_ >= 0) { const int32_t dist = std::abs(current_idx - last_requested_chunk_); constexpr float alpha = 0.3f; - ewma_chunk_distance_ = - alpha * static_cast(dist) + - (1.0f - alpha) * ewma_chunk_distance_; - if (window_size > 0 && - ewma_chunk_distance_ > static_cast(window_size)) { + ewma_chunk_distance_ = alpha * static_cast(dist) + (1.0f - alpha) * ewma_chunk_distance_; + if (window_size > 0 && ewma_chunk_distance_ > static_cast(window_size)) { do_lateral_prefetch = false; } } last_requested_chunk_ = current_idx; // Reap prefetches that finished naturally since the last call. - for (auto it = pending_loads_.begin(); it != pending_loads_.end(); ) { + for (auto it = pending_loads_.begin(); it != pending_loads_.end();) { if (it->second.future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { it = pending_loads_.erase(it); } else { @@ -514,10 +506,13 @@ namespace VTX { } int32_t start = std::max(0, current_idx - static_cast(cache_backward_)); - int32_t end = std::min(static_cast(chunk_index_table_.size()) - 1, current_idx + (int32_t)cache_forward_); + int32_t end = + std::min(static_cast(chunk_index_table_.size()) - 1, current_idx + (int32_t)cache_forward_); - if (start == current_range_start_ && end == current_range_end_) return; - current_range_start_ = start; current_range_end_ = end; + if (start == current_range_start_ && end == current_range_end_) + return; + current_range_start_ = start; + current_range_end_ = end; // Cancel in-flight prefetches whose target chunk fell outside // the new window. AsyncLoadTask checks `stop_requested()` at @@ -546,9 +541,10 @@ namespace VTX { // while firing user callbacks. const auto evts = GetEventsSnapshot(); - for (auto it = chunk_cache_.begin(); it != chunk_cache_.end(); ) { + for (auto it = chunk_cache_.begin(); it != chunk_cache_.end();) { if (it->first < start || it->first > end) { - if (evts.OnChunkEvicted) evts.OnChunkEvicted(it->first); + if (evts.OnChunkEvicted) + evts.OnChunkEvicted(it->first); it = chunk_cache_.erase(it); } else { ++it; @@ -619,7 +615,8 @@ namespace VTX { trigger(current_idx); if (do_lateral_prefetch) { for (int32_t i = start; i <= end; ++i) { - if (i != current_idx) trigger(i); + if (i != current_idx) + trigger(i); } } } @@ -637,7 +634,8 @@ namespace VTX { chunk_cache_[idx] = std::move(data); pending_loads_.erase(idx); } - if (evts.OnChunkLoadFinished) evts.OnChunkLoadFinished(idx); + if (evts.OnChunkLoadFinished) + evts.OnChunkLoadFinished(idx); } } @@ -662,20 +660,24 @@ namespace VTX { if (thread_survived) { // A4: snapshot before invoking. const auto evts = GetEventsSnapshot(); - if (evts.OnChunkLoadFinished) evts.OnChunkLoadFinished(idx); + if (evts.OnChunkLoadFinished) + evts.OnChunkLoadFinished(idx); } } CachedChunk PerformHeavyLoading(int32_t idx, const std::stop_token& stop_token) { - if (idx < 0 || idx >= chunk_index_table_.size()) return {}; + if (idx < 0 || idx >= chunk_index_table_.size()) + return {}; const auto& entry = chunk_index_table_[idx]; - if (stop_token.stop_requested()) return {}; + if (stop_token.stop_requested()) + return {}; std::string compressed_blob; { std::ifstream local_stream(filepath_, std::ios::binary | std::ios::in); - if (!local_stream.is_open()) return {}; + if (!local_stream.is_open()) + return {}; // Fix A3: validate that the chunk is fully contained in the // file before seeking. A corrupt seek table can list offsets @@ -683,15 +685,11 @@ namespace VTX { // produce garbage that then crashes the deserialiser. const int64_t file_size = GetStreamSize(local_stream); const int64_t chunk_end = - static_cast(entry.file_offset) + - static_cast(entry.chunk_size_bytes); - if (entry.chunk_size_bytes == 0 || - static_cast(entry.file_offset) < 0 || + static_cast(entry.file_offset) + static_cast(entry.chunk_size_bytes); + if (entry.chunk_size_bytes == 0 || static_cast(entry.file_offset) < 0 || chunk_end > file_size) { - VTX_ERROR("[READER] Chunk {} offset/size out of bounds (offset={}, size={}, file_size={})", - idx, - static_cast(entry.file_offset), - static_cast(entry.chunk_size_bytes), + VTX_ERROR("[READER] Chunk {} offset/size out of bounds (offset={}, size={}, file_size={})", idx, + static_cast(entry.file_offset), static_cast(entry.chunk_size_bytes), file_size); return {}; } @@ -706,22 +704,24 @@ namespace VTX { // would otherwise leave `raw_buffer` half-filled with zeros // and feed that to the deserialiser. if (local_stream.gcount() != static_cast(entry.chunk_size_bytes)) { - VTX_ERROR("[READER] Chunk {} short read: expected {} bytes, got {}", - idx, entry.chunk_size_bytes, + VTX_ERROR("[READER] Chunk {} short read: expected {} bytes, got {}", idx, entry.chunk_size_bytes, static_cast(local_stream.gcount())); return {}; } - if (raw_buffer.size() <= 4) return {}; + if (raw_buffer.size() <= 4) + return {}; compressed_blob = raw_buffer.substr(4); } - if (stop_token.stop_requested()) return {}; + if (stop_token.stop_requested()) + return {}; try { CachedChunk cc; cc.index = idx; - SerializerPolicy::ProcessChunkData(idx, compressed_blob, stop_token, cc.native_frames, cc.decompressed_blob, cc.raw_frames_spans); + SerializerPolicy::ProcessChunkData(idx, compressed_blob, stop_token, cc.native_frames, + cc.decompressed_blob, cc.raw_frames_spans); return cc; } catch (const std::exception& e) { VTX_ERROR("[READER] Chunk {} deserialization failed: {}", idx, e.what()); @@ -758,7 +758,7 @@ namespace VTX { mutable std::mutex cache_mutex_; ReplayReaderEvents events_; - mutable std::mutex events_mutex_; // protects events_ (A4) + mutable std::mutex events_mutex_; // protects events_ (A4) uint32_t cache_backward_ = 2; uint32_t cache_forward_ = 2; @@ -776,7 +776,6 @@ namespace VTX { int32_t last_requested_chunk_ = -1; float ewma_chunk_distance_ = 0.0f; - PropertyAddressCache property_address_cache_={}; + PropertyAddressCache property_address_cache_ = {}; }; -} - +} // namespace VTX diff --git a/sdk/include/vtx/reader/core/vtx_reader_facade.h b/sdk/include/vtx/reader/core/vtx_reader_facade.h index 35fc6e9..ce8e1ba 100644 --- a/sdk/include/vtx/reader/core/vtx_reader_facade.h +++ b/sdk/include/vtx/reader/core/vtx_reader_facade.h @@ -42,7 +42,8 @@ namespace VTX { virtual const VTX::Frame* GetFrame(int32_t frame_index) = 0; virtual const VTX::Frame* GetFrameSync(int frame_index) = 0; virtual void GetFrameRange(int32_t start_frame, int32_t range, std::vector& out_frames) = 0; - virtual std::vector GetFrameContext(int32_t center_frame, int32_t back_range, int32_t forward_range) = 0; + virtual std::vector GetFrameContext(int32_t center_frame, int32_t back_range, + int32_t forward_range) = 0; virtual const std::vector& GetSeekTable() const = 0; virtual VTX::FileHeader GetHeader() = 0; virtual VTX::FileFooter GetFooter() = 0; @@ -59,7 +60,8 @@ namespace VTX { /// Bundles a reader, its detected format, chunk state, and file metadata. /// Returned by OpenReplayFile(). Chunk events are pre-wired automatically. struct ReaderContext { - ReaderContext() : chunk_state(std::make_unique()) {} + ReaderContext() + : chunk_state(std::make_unique()) {} explicit operator bool() const { return reader != nullptr; } IVtxReaderFacade* operator->() const { return reader.get(); } @@ -73,7 +75,8 @@ namespace VTX { // async chunk-load callbacks capture a raw pointer into // chunk_state, so chunk_state must outlive the reader. reader.reset(); - if (chunk_state) chunk_state->Reset(); + if (chunk_state) + chunk_state->Reset(); error.clear(); format = VtxFormat::Unknown; size_in_mb = 0.0f; @@ -95,4 +98,4 @@ namespace VTX { std::unique_ptr CreateProtobuffFacade(const std::string& filepath); ReaderContext OpenReplayFile(const std::string& filepath); -} +} // namespace VTX diff --git a/sdk/include/vtx/reader/core/vtx_schema_adapter.h b/sdk/include/vtx/reader/core/vtx_schema_adapter.h index 14802f7..8934567 100644 --- a/sdk/include/vtx/reader/core/vtx_schema_adapter.h +++ b/sdk/include/vtx/reader/core/vtx_schema_adapter.h @@ -11,12 +11,12 @@ namespace fbsvtx { } namespace VTX { - + // ========================================================================================= // PROTOBUF (cppvtx::PropertySchema) // ========================================================================================= template <> - struct SchemaAdapter { + struct SchemaAdapter { static void BuildCache(const cppvtx::ContextualSchema& src, PropertyAddressCache& cache); }; @@ -24,7 +24,7 @@ namespace VTX { // FLATBUFFERS (fbsvtx::PropertySchemaT) // ========================================================================================= template <> - struct SchemaAdapter { + struct SchemaAdapter { static void BuildCache(const fbsvtx::ContextualSchemaT& src, PropertyAddressCache& cache); }; @@ -42,5 +42,5 @@ namespace VTX { } }; */ - -} \ No newline at end of file + +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/reader/policies/formatters/flatbuffer_reader_policy.h b/sdk/include/vtx/reader/policies/formatters/flatbuffer_reader_policy.h index 90d257f..9813a56 100644 --- a/sdk/include/vtx/reader/policies/formatters/flatbuffer_reader_policy.h +++ b/sdk/include/vtx/reader/policies/formatters/flatbuffer_reader_policy.h @@ -4,14 +4,13 @@ #include #include "vtx/common/vtx_types.h" -namespace fbsvtx -{ +namespace fbsvtx { struct FileHeaderT; struct FileFooterT; struct ContextualSchemaT; struct FileHeaderT; struct FileFooterT; -} +} // namespace fbsvtx namespace VTX { @@ -23,8 +22,11 @@ namespace VTX { static HeaderType ParseHeader(const std::string& buffer); static std::string GetMagicBytes(); static FooterType ParseFooter(const std::string& buffer); - static void ProcessChunkData(int chunk_index,const std::string& compressed, std::stop_token st,std::vector& out_native_frames,std::vector& out_decompressed_blob,std::vector>& out_raw_frames_spans); - static void PopulateIndexTable(const FooterType& footer,std::vector& chunk_index_table); + static void ProcessChunkData(int chunk_index, const std::string& compressed, std::stop_token st, + std::vector& out_native_frames, + std::vector& out_decompressed_blob, + std::vector>& out_raw_frames_spans); + static void PopulateIndexTable(const FooterType& footer, std::vector& chunk_index_table); static void PopulateGameTimes(const FooterType& footer, VTX::GameTime::VTXGameTimes& game_times); static int32_t GetTotalFrames(const FooterType& footer); static const SchemaType& GetSchema(const HeaderType& header); @@ -32,4 +34,4 @@ namespace VTX { static VTX::FileHeader GetVTXHeader(const HeaderType& fbs_header); static VTX::FileFooter GetVTXFooter(const FooterType& fbs_footer); }; -} +} // namespace VTX diff --git a/sdk/include/vtx/reader/policies/formatters/protobuff_reader_policy.h b/sdk/include/vtx/reader/policies/formatters/protobuff_reader_policy.h index eb01976..0c7511b 100644 --- a/sdk/include/vtx/reader/policies/formatters/protobuff_reader_policy.h +++ b/sdk/include/vtx/reader/policies/formatters/protobuff_reader_policy.h @@ -9,7 +9,7 @@ namespace cppvtx { class FileHeader; class FileFooter; class ContextualSchema; -} +} // namespace cppvtx namespace VTX { struct ProtobufReaderPolicy { @@ -20,13 +20,16 @@ namespace VTX { static std::string GetMagicBytes(); static HeaderType ParseHeader(const std::string& buffer); static FooterType ParseFooter(const std::string& buffer); - static void ProcessChunkData(int chunk_index,const std::string& compressed, std::stop_token st,std::vector& out_native_frames,std::vector& out_decompressed_blob,std::vector>& out_raw_frames_spans); - static void PopulateIndexTable(const FooterType& footer,std::vector& chunk_index_table); - static void PopulateGameTimes(const FooterType& footer, VTX::GameTime::VTXGameTimes& game_times); + static void ProcessChunkData(int chunk_index, const std::string& compressed, std::stop_token st, + std::vector& out_native_frames, + std::vector& out_decompressed_blob, + std::vector>& out_raw_frames_spans); + static void PopulateIndexTable(const FooterType& footer, std::vector& chunk_index_table); + static void PopulateGameTimes(const FooterType& footer, VTX::GameTime::VTXGameTimes& game_times); static int32_t GetTotalFrames(const FooterType& footer); static const SchemaType& GetSchema(const HeaderType& header); static VTX::ContextualSchema GetVTXContextualSchema(const HeaderType& header); static VTX::FileHeader GetVTXHeader(const HeaderType& fbs_header); static VTX::FileFooter GetVTXFooter(const FooterType& fbs_footer); }; -} +} // namespace VTX diff --git a/sdk/include/vtx/reader/serialization/flatbuffers_to_vtx.h b/sdk/include/vtx/reader/serialization/flatbuffers_to_vtx.h index bb94c1a..e9a2d4b 100644 --- a/sdk/include/vtx/reader/serialization/flatbuffers_to_vtx.h +++ b/sdk/include/vtx/reader/serialization/flatbuffers_to_vtx.h @@ -15,7 +15,7 @@ namespace fbsvtx { struct ChunkIndexEntry; struct ReplayTimeData; struct TimelineEvent; -} +} // namespace fbsvtx namespace VTX { namespace Serialization { @@ -24,7 +24,7 @@ namespace VTX { void FromFlat(const fbsvtx::Quat* src, VTX::Quat& dst); void FromFlat(const fbsvtx::Transform* src, VTX::Transform& dst); void FromFlat(const fbsvtx::FloatRange* src, VTX::FloatRange& dst); - + void FromFlat(const fbsvtx::MapContainer* src, VTX::MapContainer& dst); void FromFlat(const fbsvtx::PropertyContainer* src, VTX::PropertyContainer& dst); void FromFlat(const fbsvtx::Bucket* src, VTX::Bucket& dst); diff --git a/sdk/include/vtx/reader/serialization/proto_to_vtx.h b/sdk/include/vtx/reader/serialization/proto_to_vtx.h index ec9dc65..8579f12 100644 --- a/sdk/include/vtx/reader/serialization/proto_to_vtx.h +++ b/sdk/include/vtx/reader/serialization/proto_to_vtx.h @@ -8,7 +8,7 @@ namespace cppvtx { class PropertyContainer; class Bucket; class Frame; -} +} // namespace cppvtx namespace VTX { namespace Serialization { @@ -19,5 +19,5 @@ namespace VTX { void FromProto(const cppvtx::PropertyContainer& proto, VTX::PropertyContainer& out); void FromProto(const cppvtx::Bucket& proto, VTX::Bucket& out); void FromProto(const cppvtx::Frame& proto, VTX::Frame& out); - } -} \ No newline at end of file + } // namespace Serialization +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/writer/core/schema_dynamic_loader.h b/sdk/include/vtx/writer/core/schema_dynamic_loader.h index 4291151..5671dd8 100644 --- a/sdk/include/vtx/writer/core/schema_dynamic_loader.h +++ b/sdk/include/vtx/writer/core/schema_dynamic_loader.h @@ -18,7 +18,7 @@ #include "vtx/common/vtx_types_helpers.h" namespace VTX { - + class SchemaDynamicLoader { private: const SchemaRegistry* registry_; @@ -26,25 +26,28 @@ namespace VTX { bool debug_mode_; public: - explicit SchemaDynamicLoader(const SchemaRegistry& registry, - const SchemaSanitizerRegistry* sanitizer = nullptr, - bool debug_mode = false) - : registry_(®istry), sanitizer_(sanitizer), debug_mode_(debug_mode) {} + explicit SchemaDynamicLoader(const SchemaRegistry& registry, const SchemaSanitizerRegistry* sanitizer = nullptr, + bool debug_mode = false) + : registry_(®istry) + , sanitizer_(sanitizer) + , debug_mode_(debug_mode) {} template void Load(const AdapterType& adapter, const std::string& structName, PropertyContainer& outContainer) { - if (debug_mode_) std::cout << "[Loader] Loading struct: '" << structName << "'" << std::endl; - - if(!registry_) - { - if (debug_mode_) std::cerr << "[Loader][Error] Schema is null: " << std::endl; + if (debug_mode_) + std::cout << "[Loader] Loading struct: '" << structName << "'" << std::endl; + + if (!registry_) { + if (debug_mode_) + std::cerr << "[Loader][Error] Schema is null: " << std::endl; return; } - + const SchemaStruct* schema = registry_->GetStruct(structName); if (!schema) { - if (debug_mode_) std::cerr << "[Loader][Error] Schema not found: " << structName << std::endl; + if (debug_mode_) + std::cerr << "[Loader][Error] Schema not found: " << structName << std::endl; return; } @@ -61,122 +64,134 @@ namespace VTX { if (field.container_type == FieldContainerType::Array) { LoadArray(child_adapter, field, outContainer); - } - else if (field.type_id == FieldType::Struct) { + } else if (field.type_id == FieldType::Struct) { Load(child_adapter, field.struct_type, outContainer.any_struct_properties[field.index]); - } - else { + } else { LoadSingleField(child_adapter, field, outContainer); } } if (sanitizer_) { - SanitizeContext ctx{structName,schema,true};// is_loading = true, we are in loading phase + SanitizeContext ctx {structName, schema, true}; // is_loading = true, we are in loading phase sanitizer_->TrySanitize(outContainer, ctx); } - + outContainer.content_hash = Helpers::CalculateContainerHash(outContainer); } private: - template void LoadSingleField(const AdapterType& node, const SchemaField& field, PropertyContainer& out) { const int32_t idx = field.index; try { switch (field.type_id) { - case FieldType::Int32: out.int32_properties[idx] = node.template GetValue(); break; - case FieldType::Int64: out.int64_properties[idx] = node.template GetValue(); break; - case FieldType::Float: out.float_properties[idx] = node.template GetValue(); break; - case FieldType::Double: out.double_properties[idx] = node.template GetValue(); break; - case FieldType::Bool: out.bool_properties[idx] = node.template GetValue(); break; - case FieldType::String: out.string_properties[idx] = node.template GetValue(); break; - - case FieldType::Vector: { - out.vector_properties[idx] = { - node.GetChild("x").template GetValue(), - node.GetChild("y").template GetValue(), - node.GetChild("z").template GetValue() - }; - break; - } - case FieldType::Quat: { - out.quat_properties[idx] = { - node.GetChild("x").template GetValue(), - node.GetChild("y").template GetValue(), - node.GetChild("z").template GetValue(), - node.GetChild("w").template GetValue() - }; - break; - } - case FieldType::FloatRange: { - out.range_properties[idx] = { - node.GetChild("min").template GetValue(), - node.GetChild("max").template GetValue(), - node.GetChild("value").template GetValue() - }; - break; - } - case FieldType::Transform: { - Transform t; - auto loc = node.GetChild("translation"); - t.translation = { loc.GetChild("x").template GetValue(), loc.GetChild("y").template GetValue(), loc.GetChild("z").template GetValue() }; - - auto rot = node.GetChild("rotation"); - t.rotation = { rot.GetChild("x").template GetValue(), rot.GetChild("y").template GetValue(), rot.GetChild("z").template GetValue(), rot.GetChild("w").template GetValue() }; - - auto scl = node.GetChild("scale"); - t.scale = { scl.GetChild("x").template GetValue(), scl.GetChild("y").template GetValue(), scl.GetChild("z").template GetValue() }; - - out.transform_properties[idx] = t; - break; - } - default: break; + case FieldType::Int32: + out.int32_properties[idx] = node.template GetValue(); + break; + case FieldType::Int64: + out.int64_properties[idx] = node.template GetValue(); + break; + case FieldType::Float: + out.float_properties[idx] = node.template GetValue(); + break; + case FieldType::Double: + out.double_properties[idx] = node.template GetValue(); + break; + case FieldType::Bool: + out.bool_properties[idx] = node.template GetValue(); + break; + case FieldType::String: + out.string_properties[idx] = node.template GetValue(); + break; + + case FieldType::Vector: { + out.vector_properties[idx] = {node.GetChild("x").template GetValue(), + node.GetChild("y").template GetValue(), + node.GetChild("z").template GetValue()}; + break; + } + case FieldType::Quat: { + out.quat_properties[idx] = { + node.GetChild("x").template GetValue(), node.GetChild("y").template GetValue(), + node.GetChild("z").template GetValue(), node.GetChild("w").template GetValue()}; + break; + } + case FieldType::FloatRange: { + out.range_properties[idx] = {node.GetChild("min").template GetValue(), + node.GetChild("max").template GetValue(), + node.GetChild("value").template GetValue()}; + break; + } + case FieldType::Transform: { + Transform t; + auto loc = node.GetChild("translation"); + t.translation = {loc.GetChild("x").template GetValue(), + loc.GetChild("y").template GetValue(), + loc.GetChild("z").template GetValue()}; + + auto rot = node.GetChild("rotation"); + t.rotation = { + rot.GetChild("x").template GetValue(), rot.GetChild("y").template GetValue(), + rot.GetChild("z").template GetValue(), rot.GetChild("w").template GetValue()}; + + auto scl = node.GetChild("scale"); + t.scale = {scl.GetChild("x").template GetValue(), + scl.GetChild("y").template GetValue(), + scl.GetChild("z").template GetValue()}; + + out.transform_properties[idx] = t; + break; + } + default: + break; } } catch (const std::exception& e) { - if (debug_mode_) std::cerr << "[Loader] Error loading field '" << field.name << "': " << e.what() << std::endl; + if (debug_mode_) + std::cerr << "[Loader] Error loading field '" << field.name << "': " << e.what() << std::endl; } } template void LoadArray(const AdapterType& arrayNode, const SchemaField& field, PropertyContainer& out) { - if (!arrayNode.IsArray()) return; - + if (!arrayNode.IsArray()) + return; + const size_t count = arrayNode.Size(); const int32_t idx = field.index; switch (field.type_id) { - case FieldType::Int32: - for (size_t i = 0; i < count; ++i) - out.int32_arrays.PushBack(idx, arrayNode.GetElement(i).template GetValue()); - break; - case FieldType::Float: - for (size_t i = 0; i < count; ++i) - out.float_arrays.PushBack(idx, arrayNode.GetElement(i).template GetValue()); - break; - case FieldType::String: - for (size_t i = 0; i < count; ++i) - out.string_arrays.PushBack(idx, arrayNode.GetElement(i).template GetValue()); - break; - case FieldType::Vector: - for (size_t i = 0; i < count; ++i) { - auto elem = arrayNode.GetElement(i); - Vector v{ elem.GetChild("x").template GetValue(), - elem.GetChild("y").template GetValue(), - elem.GetChild("z").template GetValue() }; - out.vector_arrays.PushBack(idx, v); - } - break; - case FieldType::Struct: - for (size_t i = 0; i < count; ++i) { - PropertyContainer childContainer; - Load(arrayNode.GetElement(i), field.struct_type, childContainer); - out.any_struct_arrays.PushBack(idx, childContainer); - } - break; - default: break; + case FieldType::Int32: + for (size_t i = 0; i < count; ++i) + out.int32_arrays.PushBack(idx, arrayNode.GetElement(i).template GetValue()); + break; + case FieldType::Float: + for (size_t i = 0; i < count; ++i) + out.float_arrays.PushBack(idx, arrayNode.GetElement(i).template GetValue()); + break; + case FieldType::String: + for (size_t i = 0; i < count; ++i) + out.string_arrays.PushBack(idx, arrayNode.GetElement(i).template GetValue()); + break; + case FieldType::Vector: + for (size_t i = 0; i < count; ++i) { + auto elem = arrayNode.GetElement(i); + Vector v {elem.GetChild("x").template GetValue(), + elem.GetChild("y").template GetValue(), + elem.GetChild("z").template GetValue()}; + out.vector_arrays.PushBack(idx, v); + } + break; + case FieldType::Struct: + for (size_t i = 0; i < count; ++i) { + PropertyContainer childContainer; + Load(arrayNode.GetElement(i), field.struct_type, childContainer); + out.any_struct_arrays.PushBack(idx, childContainer); + } + break; + default: + break; } } }; -} +} // namespace VTX diff --git a/sdk/include/vtx/writer/core/schema_sanitizer.h b/sdk/include/vtx/writer/core/schema_sanitizer.h index fcefe36..c8b6382 100644 --- a/sdk/include/vtx/writer/core/schema_sanitizer.h +++ b/sdk/include/vtx/writer/core/schema_sanitizer.h @@ -16,10 +16,9 @@ namespace VTX { class SchemaSanitizerRegistry { std::unordered_map _processors; + public: - void Register(const std::string& structName, const SanitizeFunc& func) { - _processors[structName] = func; - } + void Register(const std::string& structName, const SanitizeFunc& func) { _processors[structName] = func; } void TrySanitize(PropertyContainer& container, const SanitizeContext& ctx) const { auto it = _processors.find(ctx.struct_name); @@ -28,4 +27,4 @@ namespace VTX { } } }; -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/writer/core/vtx_data_source.h b/sdk/include/vtx/writer/core/vtx_data_source.h index ea51044..07a4120 100644 --- a/sdk/include/vtx/writer/core/vtx_data_source.h +++ b/sdk/include/vtx/writer/core/vtx_data_source.h @@ -11,4 +11,4 @@ namespace VTX { virtual size_t GetExpectedTotalFrames() const = 0; }; -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/writer/core/vtx_record_pipeline.h b/sdk/include/vtx/writer/core/vtx_record_pipeline.h index 75936df..e118cca 100644 --- a/sdk/include/vtx/writer/core/vtx_record_pipeline.h +++ b/sdk/include/vtx/writer/core/vtx_record_pipeline.h @@ -8,10 +8,11 @@ namespace VTX { class RecordPipeline { public: RecordPipeline(std::unique_ptr source, std::unique_ptr writer); - bool Run(std::function on_progress = nullptr); + bool Run(std::function on_progress = nullptr); + private: std::unique_ptr source_; std::unique_ptr writer_; }; -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/writer/core/vtx_writer_facade.h b/sdk/include/vtx/writer/core/vtx_writer_facade.h index 131e533..f10501a 100644 --- a/sdk/include/vtx/writer/core/vtx_writer_facade.h +++ b/sdk/include/vtx/writer/core/vtx_writer_facade.h @@ -15,12 +15,13 @@ namespace VTX { public: virtual ~IVtxWriterFacade() = default; - virtual void RecordFrame(VTX::Frame& native_frame, const VTX::GameTime::GameTimeRegister& game_time_register) = 0; + virtual void RecordFrame(VTX::Frame& native_frame, + const VTX::GameTime::GameTimeRegister& game_time_register) = 0; virtual void Flush() = 0; virtual void Stop() = 0; - virtual VTX::SchemaRegistry& GetSchema() = 0 ; + virtual VTX::SchemaRegistry& GetSchema() = 0; }; - + struct WriterFacadeConfig { std::string replay_name = ""; std::string replay_uuid = ""; @@ -31,19 +32,13 @@ namespace VTX { size_t chunk_max_bytes = 10 * 1024 * 1024; // 10 MB bool use_compression = true; std::string schema_json_path = ""; - }; - enum class SerializationFormat : uint8_t - { + enum class SerializationFormat : uint8_t { Flatbuffers, Protobuffs, }; - std::unique_ptr CreateFlatBuffersWriterFacade( - const WriterFacadeConfig& config - ); + std::unique_ptr CreateFlatBuffersWriterFacade(const WriterFacadeConfig& config); - std::unique_ptr CreateProtobuffWriterFacade( - const WriterFacadeConfig& config - ); -} + std::unique_ptr CreateProtobuffWriterFacade(const WriterFacadeConfig& config); +} // namespace VTX diff --git a/sdk/include/vtx/writer/core/writer.h b/sdk/include/vtx/writer/core/writer.h index 96ce3f1..b465198 100644 --- a/sdk/include/vtx/writer/core/writer.h +++ b/sdk/include/vtx/writer/core/writer.h @@ -6,8 +6,7 @@ namespace VTX { - namespace ChunkingPolicy - { + namespace ChunkingPolicy { struct ThresholdChunkPolicy { int32_t max_frames = 1000; size_t max_bytes = 10 * 1024 * 1024; @@ -19,18 +18,18 @@ namespace VTX { struct InstantFlushPolicy { bool ShouldFlush(size_t, size_t, size_t) const { return true; } }; - } + } // namespace ChunkingPolicy template class ReplayWriter { public: using Serializer = typename SinkPolicy::SerializerPolicy; - using FrameType = typename SinkPolicy::FrameType; + using FrameType = typename SinkPolicy::FrameType; using SchemaType = typename SinkPolicy::SchemaType; struct Config { - typename SinkPolicy::Config sink_config; + typename SinkPolicy::Config sink_config; float default_fps = 60.0f; bool is_increasing = true; ChunkingPolicy chunker_config; @@ -38,19 +37,19 @@ namespace VTX { }; ReplayWriter(Config config) - : sink_(config.sink_config), - chunker_(config.chunker_config), registry_({}), sanitizer_(nullptr) - { + : sink_(config.sink_config) + , chunker_(config.chunker_config) + , registry_({}) + , sanitizer_(nullptr) { timer_.Setup(config.default_fps, config.is_increasing); registry_.LoadFromJson(config.schema_json_path); auto schema = Serializer::CreateSchema(registry_); sink_.OnSessionStart(schema); } - void RecordFrame(VTX::Frame& native_frame, const VTX::GameTime::GameTimeRegister& game_time_register) - { + void RecordFrame(VTX::Frame& native_frame, const VTX::GameTime::GameTimeRegister& game_time_register) { timer_.CreateSnapshot(); - + if (!timer_.AddTimeRegistry(game_time_register)) { timer_.Rollback(); return; @@ -61,13 +60,12 @@ namespace VTX { timer_.Rollback(); return; } - + std::unique_ptr sink_frame = Serializer::FromNative(std::move(native_frame)); size_t frameSize = Serializer::GetFrameSize(*sink_frame); - - if (!pending_frames_.empty() && - chunker_.ShouldFlush(pending_frames_.size(), current_chunk_bytes_, frameSize)) - { + + if (!pending_frames_.empty() && + chunker_.ShouldFlush(pending_frames_.size(), current_chunk_bytes_, frameSize)) { Flush(); } @@ -77,13 +75,14 @@ namespace VTX { } void Flush() { - if (pending_frames_.empty()) return; + if (pending_frames_.empty()) + return; int32_t start_frame = total_frames_ - static_cast(pending_frames_.size()); - auto time_chunk = timer_.GetLastChunkCreatedUtc(); + auto time_chunk = timer_.GetLastChunkCreatedUtc(); sink_.SaveChunk(pending_frames_, time_chunk, start_frame, total_frames_); - - pending_frames_.clear(); + + pending_frames_.clear(); current_chunk_bytes_ = 0; timer_.UpdateChunkStartIndex(); } @@ -93,33 +92,31 @@ namespace VTX { VTX::SessionFooter footer_data; footer_data.total_frames = total_frames_; footer_data.duration_seconds = timer_.GetDuration(); - + const auto& v_gametime = timer_.GetGameTime(); - const auto& v_utc = timer_.GetCreatedUtc(); - const auto& v_gaps = timer_.GetTimelineGaps(); - const auto& v_seg = timer_.GetGameSegments(); + const auto& v_utc = timer_.GetCreatedUtc(); + const auto& v_gaps = timer_.GetTimelineGaps(); + const auto& v_seg = timer_.GetGameSegments(); - footer_data.game_times = &v_gametime; + footer_data.game_times = &v_gametime; footer_data.created_utc = &v_utc; - footer_data.gaps = &v_gaps; - footer_data.segments = &v_seg; - + footer_data.gaps = &v_gaps; + footer_data.segments = &v_seg; + sink_.Close(footer_data); } - VTX::SchemaRegistry& GetRegistry() - { - return registry_; - } + VTX::SchemaRegistry& GetRegistry() { return registry_; } + private: SinkPolicy sink_; ChunkingPolicy chunker_; - + VTX::SchemaRegistry registry_; const SchemaSanitizerRegistry* sanitizer_; std::vector> pending_frames_; - VTX::GameTime::VTXGameTimes timer_; + VTX::GameTime::VTXGameTimes timer_; size_t current_chunk_bytes_ = 0; int32_t total_frames_ = 0; }; -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/writer/policies/formatters/flatbuffers_vtx_policy.h b/sdk/include/vtx/writer/policies/formatters/flatbuffers_vtx_policy.h index 3bea488..f86ad2d 100644 --- a/sdk/include/vtx/writer/policies/formatters/flatbuffers_vtx_policy.h +++ b/sdk/include/vtx/writer/policies/formatters/flatbuffers_vtx_policy.h @@ -3,16 +3,15 @@ #include "vtx/common/readers/schema_reader/schema_registry.h" #include "vtx/writer/serialization/vtx_to_flatbuffer.h" -namespace fbsvtx -{ +namespace fbsvtx { struct ContextualSchemaT; } namespace VTX { - struct FlatBuffersVtxPolicy { - using HeaderType = VTX::SessionConfig; - using FrameType = VTX::Frame; + struct FlatBuffersVtxPolicy { + using HeaderType = VTX::SessionConfig; + using FrameType = VTX::Frame; using SchemaType = std::unique_ptr; static std::string GetMagicBytes(); @@ -20,7 +19,9 @@ namespace VTX { static size_t GetFrameSize(const FrameType& /*frame*/); static SchemaType CreateSchema(const SchemaRegistry& registry); static std::string SerializeHeader(const VTX::SessionConfig& config, const SchemaType& schema); - static std::string SerializeChunk(const std::vector>& frames, int32_t chunk_idx, bool is_compressed); - static std::string SerializeFooter(const std::vector& seek_table, const SessionFooter& footer_data); + static std::string SerializeChunk(const std::vector>& frames, int32_t chunk_idx, + bool is_compressed); + static std::string SerializeFooter(const std::vector& seek_table, + const SessionFooter& footer_data); }; -} +} // namespace VTX diff --git a/sdk/include/vtx/writer/policies/formatters/protobuff_vtx_policy.h b/sdk/include/vtx/writer/policies/formatters/protobuff_vtx_policy.h index 8bff563..edea78b 100644 --- a/sdk/include/vtx/writer/policies/formatters/protobuff_vtx_policy.h +++ b/sdk/include/vtx/writer/policies/formatters/protobuff_vtx_policy.h @@ -6,29 +6,29 @@ #include "vtx/common/vtx_types.h" #include "vtx/common/readers/schema_reader/schema_registry.h" -namespace cppvtx -{ +namespace cppvtx { class Frame; class ContextualSchema; -} +} // namespace cppvtx namespace VTX { struct ProtobufVtxPolicy { - using FrameType = cppvtx::Frame; + using FrameType = cppvtx::Frame; using SchemaType = cppvtx::ContextualSchema; using HeaderType = VTX::SessionConfig; static std::string GetMagicBytes(); - + static std::unique_ptr FromNative(VTX::Frame&& native); - + static size_t GetFrameSize(const FrameType& frame); static SchemaType CreateSchema(const SchemaRegistry& registry); - + static std::string SerializeHeader(const VTX::SessionConfig& config, const SchemaType& schema); - static std::string SerializeChunk(const std::vector>& frames, int32_t chunkIdx, bool is_compressed); - static std::string SerializeFooter(const std::vector& seekTable, + static std::string SerializeChunk(const std::vector>& frames, int32_t chunkIdx, + bool is_compressed); + static std::string SerializeFooter(const std::vector& seekTable, const SessionFooter& footerData); }; -} +} // namespace VTX diff --git a/sdk/include/vtx/writer/policies/sinks/file_sink.h b/sdk/include/vtx/writer/policies/sinks/file_sink.h index 64d2d4d..106c597 100644 --- a/sdk/include/vtx/writer/policies/sinks/file_sink.h +++ b/sdk/include/vtx/writer/policies/sinks/file_sink.h @@ -7,90 +7,86 @@ #include #include "vtx/common/vtx_types.h" #include "vtx/common/vtx_concepts.h" -namespace VTX -{ - +namespace VTX { + template - class ChunkedFileSink - { - public: + class ChunkedFileSink { + public: using SerializerPolicy = Policy; - using FrameType = typename SerializerPolicy::FrameType; - using SchemaType = typename SerializerPolicy::SchemaType; - using HeaderType = typename SerializerPolicy::HeaderType; - - struct Config { - std::string filename; - HeaderType header_config; - bool b_use_compression = true; - int8_t compression_level = 10; - }; - - explicit ChunkedFileSink(Config config) : config_(std::move(config)) { - file_.open(config_.filename, std::ios::binary | std::ios::out | std::ios::trunc); - if (!file_.is_open()) throw std::runtime_error("VTX: Could not open " + config_.filename); - } + using FrameType = typename SerializerPolicy::FrameType; + using SchemaType = typename SerializerPolicy::SchemaType; + using HeaderType = typename SerializerPolicy::HeaderType; - void OnSessionStart(const SchemaType& schema) { - - //this writes the vrx format, ie "VTXP"(protobuff) VTXF(flatbuffer) - std::string magic_bytes = SerializerPolicy::GetMagicBytes(); - WriteBlob(magic_bytes); - - std::string header_payload = SerializerPolicy::SerializeHeader(config_.header_config, schema); - header_payload = CompressIfBeneficial(std::move(header_payload)); - uint32_t final_size = static_cast(header_payload.size()); - file_.write(reinterpret_cast(&final_size), sizeof(final_size)); - file_.write(header_payload.data(), final_size); - } - - void SaveChunk(std::vector>& frames, - const std::vector& created_utc, - int32_t start_frame, - int32_t total_frames) - { - if (frames.empty()) return; - - float chunk_start_time = 0.0f; - float chunk_end_time = 0.0f; - if (!created_utc.empty()) { - chunk_start_time = static_cast(created_utc.front()); - chunk_end_time = static_cast(created_utc.back()); - } - - std::string payload = SerializerPolicy::SerializeChunk(frames, chunkIndex_,config_.b_use_compression); - payload = CompressIfBeneficial(std::move(payload)); - - uint64_t current_offset = file_.tellp(); - uint32_t final_size = static_cast(payload.size()); - - file_.write(reinterpret_cast(&final_size), sizeof(final_size)); - file_.write(payload.data(), final_size); - - ChunkIndexData indexEntry; - indexEntry.chunk_index = chunkIndex_++; - indexEntry.file_offset = current_offset; - indexEntry.start_frame = start_frame; - indexEntry.end_frame = total_frames - 1; - indexEntry.chunk_size_bytes = final_size + sizeof(uint32_t); - seek_table_.push_back(indexEntry); + struct Config { + std::string filename; + HeaderType header_config; + bool b_use_compression = true; + int8_t compression_level = 10; + }; + + explicit ChunkedFileSink(Config config) + : config_(std::move(config)) { + file_.open(config_.filename, std::ios::binary | std::ios::out | std::ios::trunc); + if (!file_.is_open()) + throw std::runtime_error("VTX: Could not open " + config_.filename); + } + + void OnSessionStart(const SchemaType& schema) { + //this writes the vrx format, ie "VTXP"(protobuff) VTXF(flatbuffer) + std::string magic_bytes = SerializerPolicy::GetMagicBytes(); + WriteBlob(magic_bytes); + + std::string header_payload = SerializerPolicy::SerializeHeader(config_.header_config, schema); + header_payload = CompressIfBeneficial(std::move(header_payload)); + uint32_t final_size = static_cast(header_payload.size()); + file_.write(reinterpret_cast(&final_size), sizeof(final_size)); + file_.write(header_payload.data(), final_size); + } + + void SaveChunk(std::vector>& frames, const std::vector& created_utc, + int32_t start_frame, int32_t total_frames) { + if (frames.empty()) + return; + + float chunk_start_time = 0.0f; + float chunk_end_time = 0.0f; + if (!created_utc.empty()) { + chunk_start_time = static_cast(created_utc.front()); + chunk_end_time = static_cast(created_utc.back()); } - + + std::string payload = SerializerPolicy::SerializeChunk(frames, chunkIndex_, config_.b_use_compression); + payload = CompressIfBeneficial(std::move(payload)); + + uint64_t current_offset = file_.tellp(); + uint32_t final_size = static_cast(payload.size()); + + file_.write(reinterpret_cast(&final_size), sizeof(final_size)); + file_.write(payload.data(), final_size); + + ChunkIndexData indexEntry; + indexEntry.chunk_index = chunkIndex_++; + indexEntry.file_offset = current_offset; + indexEntry.start_frame = start_frame; + indexEntry.end_frame = total_frames - 1; + indexEntry.chunk_size_bytes = final_size + sizeof(uint32_t); + seek_table_.push_back(indexEntry); + } + void Close(const SessionFooter& footerData) { - if (!file_.is_open()) return; + if (!file_.is_open()) + return; std::string footer_payload = SerializerPolicy::SerializeFooter(seek_table_, footerData); footer_payload = CompressIfBeneficial(std::move(footer_payload)); - + file_.write(footer_payload.data(), footer_payload.size()); uint32_t final_size = static_cast(footer_payload.size()); file_.write(reinterpret_cast(&final_size), sizeof(final_size)); WriteBlob(SerializerPolicy::GetMagicBytes()); } - private: - void WriteBlob(const std::string& data) { - file_.write(data.data(), data.size()); - } + private: + void WriteBlob(const std::string& data) { file_.write(data.data(), data.size()); } std::string CompressIfBeneficial(std::string payload) { if (!config_.b_use_compression || payload.size() < 512) { @@ -100,14 +96,11 @@ namespace VTX size_t const max_size = ZSTD_compressBound(payload.size()); std::string compressed_blob(max_size, '\0'); - size_t const compressed_size = ZSTD_compress( - compressed_blob.data(), max_size, - payload.data(), payload.size(), - config_.compression_level - ); + size_t const compressed_size = ZSTD_compress(compressed_blob.data(), max_size, payload.data(), + payload.size(), config_.compression_level); if (ZSTD_isError(compressed_size)) { - return payload; + return payload; } if (compressed_size >= payload.size()) { @@ -117,12 +110,10 @@ namespace VTX compressed_blob.resize(compressed_size); return compressed_blob; } - + Config config_; std::ofstream file_; int32_t chunkIndex_ = 0; - std::vector seek_table_;//Generic tables, format agnostic + std::vector seek_table_; //Generic tables, format agnostic }; -}; - - +}; // namespace VTX diff --git a/sdk/include/vtx/writer/serialization/vtx_to_flatbuffer.h b/sdk/include/vtx/writer/serialization/vtx_to_flatbuffer.h index 05a331d..24dd7ee 100644 --- a/sdk/include/vtx/writer/serialization/vtx_to_flatbuffer.h +++ b/sdk/include/vtx/writer/serialization/vtx_to_flatbuffer.h @@ -4,7 +4,8 @@ #include namespace flatbuffers { - template struct Offset; + template + struct Offset; } namespace fbsvtx { @@ -16,20 +17,22 @@ namespace fbsvtx { struct MapContainer; struct Bucket; struct Frame; -} +} // namespace fbsvtx namespace VTX { namespace Serialization { - fbsvtx::Vector ToFlat(VTX::Vector& v); - fbsvtx::Quat ToFlat(VTX::Quat& q); - fbsvtx::Transform ToFlat(VTX::Transform& t); + fbsvtx::Vector ToFlat(VTX::Vector& v); + fbsvtx::Quat ToFlat(VTX::Quat& q); + fbsvtx::Transform ToFlat(VTX::Transform& t); fbsvtx::FloatRange ToFlat(VTX::FloatRange& r); - flatbuffers::Offset ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::MapContainer& src); - flatbuffers::Offset ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::PropertyContainer& src); - flatbuffers::Offset ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::Bucket& src); - flatbuffers::Offset ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::Frame& src); + flatbuffers::Offset ToFlat(flatbuffers::FlatBufferBuilder& builder, + VTX::MapContainer& src); + flatbuffers::Offset ToFlat(flatbuffers::FlatBufferBuilder& builder, + VTX::PropertyContainer& src); + flatbuffers::Offset ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::Bucket& src); + flatbuffers::Offset ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::Frame& src); } // namespace Serialization } // namespace VTX \ No newline at end of file diff --git a/sdk/include/vtx/writer/serialization/vtx_to_proto.h b/sdk/include/vtx/writer/serialization/vtx_to_proto.h index 68bc3c7..62e5b70 100644 --- a/sdk/include/vtx/writer/serialization/vtx_to_proto.h +++ b/sdk/include/vtx/writer/serialization/vtx_to_proto.h @@ -30,7 +30,7 @@ namespace VTX { * @param src The source native Quat (x, y, z, w). * @param dst The destination Protobuf message pointer. */ - inline void ToProto( VTX::Quat& src, cppvtx::Quat* dst) { + inline void ToProto(VTX::Quat& src, cppvtx::Quat* dst) { dst->set_x(src.x); dst->set_y(src.y); dst->set_z(src.z); @@ -58,7 +58,7 @@ namespace VTX { dst->set_max(src.max); dst->set_value_normalized(src.value_normalized); } - + /** * @brief Serializes a PropertyContainer containing various data types. * * This function handles: @@ -70,96 +70,125 @@ namespace VTX { * @param dst The destination Protobuf message pointer. */ inline void ToProto(VTX::PropertyContainer& src, cppvtx::PropertyContainer* dst) { - src.content_hash = VTX::Helpers::CalculateContainerHash(src); - + dst->set_type_id(src.entity_type_id); dst->set_content_hash(src.content_hash); // --- Scalar Properties --- - for (auto val : src.int32_properties) dst->add_int32_properties(val); - for (auto val : src.int64_properties) dst->add_int64_properties(val); - for (auto val : src.float_properties) dst->add_float_properties(val); - for (auto val : src.double_properties) dst->add_double_properties(val); - for (auto val : src.bool_properties) dst->add_bool_properties(val); - for (auto& val : src.string_properties) dst->add_string_properties(val); + for (auto val : src.int32_properties) + dst->add_int32_properties(val); + for (auto val : src.int64_properties) + dst->add_int64_properties(val); + for (auto val : src.float_properties) + dst->add_float_properties(val); + for (auto val : src.double_properties) + dst->add_double_properties(val); + for (auto val : src.bool_properties) + dst->add_bool_properties(val); + for (auto& val : src.string_properties) + dst->add_string_properties(val); // --- Object Properties --- - for (auto& val : src.vector_properties) ToProto(val, dst->add_vector_properties()); - for (auto& val : src.quat_properties) ToProto(val, dst->add_quat_properties()); - for (auto& val : src.transform_properties) ToProto(val, dst->add_transform_properties()); - for (auto& val : src.range_properties) ToProto(val, dst->add_range_properties()); + for (auto& val : src.vector_properties) + ToProto(val, dst->add_vector_properties()); + for (auto& val : src.quat_properties) + ToProto(val, dst->add_quat_properties()); + for (auto& val : src.transform_properties) + ToProto(val, dst->add_transform_properties()); + for (auto& val : src.range_properties) + ToProto(val, dst->add_range_properties()); // --- Array Properties (Flattened Bucket + offsets) --- // Each block checks if the source data is non-empty before mutating the destination. if (!src.byte_array_properties.data.empty()) { auto* dest_arr = dst->mutable_byte_array_properties(); - - dest_arr->add_data(src.byte_array_properties.data.data(),src.byte_array_properties.data.size()); + + dest_arr->add_data(src.byte_array_properties.data.data(), src.byte_array_properties.data.size()); for (auto o : src.byte_array_properties.offsets) { dest_arr->add_offsets(o); } } - + if (!src.int32_arrays.data.empty()) { auto* dest_arr = dst->mutable_int32_arrays(); - for (auto v : src.int32_arrays.data) dest_arr->add_data(v); - for (auto o : src.int32_arrays.offsets) dest_arr->add_offsets(o); + for (auto v : src.int32_arrays.data) + dest_arr->add_data(v); + for (auto o : src.int32_arrays.offsets) + dest_arr->add_offsets(o); } if (!src.int64_arrays.data.empty()) { auto* dest_arr = dst->mutable_int64_arrays(); - for (auto v : src.int64_arrays.data) dest_arr->add_data(v); - for (auto o : src.int64_arrays.offsets) dest_arr->add_offsets(o); + for (auto v : src.int64_arrays.data) + dest_arr->add_data(v); + for (auto o : src.int64_arrays.offsets) + dest_arr->add_offsets(o); } if (!src.float_arrays.data.empty()) { auto* dest_arr = dst->mutable_float_arrays(); - for (auto v : src.float_arrays.data) dest_arr->add_data(v); - for (auto o : src.float_arrays.offsets) dest_arr->add_offsets(o); + for (auto v : src.float_arrays.data) + dest_arr->add_data(v); + for (auto o : src.float_arrays.offsets) + dest_arr->add_offsets(o); } if (!src.double_arrays.data.empty()) { auto* dest_arr = dst->mutable_double_arrays(); - for (auto v : src.double_arrays.data) dest_arr->add_data(v); - for (auto o : src.double_arrays.offsets) dest_arr->add_offsets(o); + for (auto v : src.double_arrays.data) + dest_arr->add_data(v); + for (auto o : src.double_arrays.offsets) + dest_arr->add_offsets(o); } if (!src.bool_arrays.data.empty()) { auto* dest_arr = dst->mutable_bool_arrays(); - for (auto v : src.bool_arrays.data) dest_arr->add_data(v); - for (auto o : src.bool_arrays.offsets) dest_arr->add_offsets(o); + for (auto v : src.bool_arrays.data) + dest_arr->add_data(v); + for (auto o : src.bool_arrays.offsets) + dest_arr->add_offsets(o); } if (!src.string_arrays.data.empty()) { auto* dest_arr = dst->mutable_string_arrays(); - for (auto& v : src.string_arrays.data) dest_arr->add_data(v); - for (auto o : src.string_arrays.offsets) dest_arr->add_offsets(o); + for (auto& v : src.string_arrays.data) + dest_arr->add_data(v); + for (auto o : src.string_arrays.offsets) + dest_arr->add_offsets(o); } - + if (!src.vector_arrays.data.empty()) { auto* dest_arr = dst->mutable_vector_arrays(); - for (auto& v : src.vector_arrays.data) ToProto(v, dest_arr->add_data()); - for (auto o : src.vector_arrays.offsets) dest_arr->add_offsets(o); + for (auto& v : src.vector_arrays.data) + ToProto(v, dest_arr->add_data()); + for (auto o : src.vector_arrays.offsets) + dest_arr->add_offsets(o); } if (!src.quat_arrays.data.empty()) { auto* dest_arr = dst->mutable_quat_arrays(); - for (auto& v : src.quat_arrays.data) ToProto(v, dest_arr->add_data()); - for (auto o : src.quat_arrays.offsets) dest_arr->add_offsets(o); + for (auto& v : src.quat_arrays.data) + ToProto(v, dest_arr->add_data()); + for (auto o : src.quat_arrays.offsets) + dest_arr->add_offsets(o); } if (!src.transform_arrays.data.empty()) { auto* dest_arr = dst->mutable_transform_arrays(); - for (auto& v : src.transform_arrays.data) ToProto(v, dest_arr->add_data()); - for (auto o : src.transform_arrays.offsets) dest_arr->add_offsets(o); + for (auto& v : src.transform_arrays.data) + ToProto(v, dest_arr->add_data()); + for (auto o : src.transform_arrays.offsets) + dest_arr->add_offsets(o); } if (!src.range_arrays.data.empty()) { auto* dest_arr = dst->mutable_range_arrays(); - for (auto& v : src.range_arrays.data) ToProto(v, dest_arr->add_data()); - for (auto o : src.range_arrays.offsets) dest_arr->add_offsets(o); + for (auto& v : src.range_arrays.data) + ToProto(v, dest_arr->add_data()); + for (auto o : src.range_arrays.offsets) + dest_arr->add_offsets(o); } - + // --- AnyStruct Properties & Arrays --- for (auto& child : src.any_struct_properties) { ToProto(child, dst->add_any_struct_properties()); @@ -177,18 +206,23 @@ namespace VTX { for (auto& map_item : src.map_properties) { auto* dstMap = dst->add_map_properties(); - for(auto& key : map_item.keys) dstMap->add_keys(key); - for(auto& val : map_item.values) ToProto(val, dstMap->add_values()); + for (auto& key : map_item.keys) + dstMap->add_keys(key); + for (auto& val : map_item.values) + ToProto(val, dstMap->add_values()); } if (!src.map_arrays.data.empty()) { auto* dst_arr = dst->mutable_map_arrays(); for (auto& map_item : src.map_arrays.data) { auto* dst_map = dst_arr->add_data(); - for(auto& key : map_item.keys) dst_map->add_keys(key); - for(auto& val : map_item.values) ToProto(val, dst_map->add_values()); + for (auto& key : map_item.keys) + dst_map->add_keys(key); + for (auto& val : map_item.values) + ToProto(val, dst_map->add_values()); } - for (auto o : src.map_arrays.offsets) dst_arr->add_offsets(o); + for (auto o : src.map_arrays.offsets) + dst_arr->add_offsets(o); } } @@ -198,15 +232,15 @@ namespace VTX { * @param dst The destination Protobuf message pointer. */ inline void ToProto(VTX::Bucket& src, cppvtx::Bucket* dst) { - dst->mutable_unique_ids()->Reserve(static_cast(src.unique_ids.size())); - for (auto& id : src.unique_ids) dst->add_unique_ids(id); - + for (auto& id : src.unique_ids) + dst->add_unique_ids(id); + dst->mutable_entities()->Reserve(static_cast(src.entities.size())); for (auto& entity : src.entities) { ToProto(entity, dst->add_entities()); } - + for (auto& native_range : src.type_ranges) { cppvtx::EntityRange* proto_range = dst->add_type_ranges(); proto_range->set_start_index(native_range.start_index); @@ -224,5 +258,5 @@ namespace VTX { ToProto(dataBlock, dst->add_data()); } } - } -} \ No newline at end of file + } // namespace Serialization +} // namespace VTX \ No newline at end of file diff --git a/sdk/src/vtx_common/src/vtx/common/readers/schema_reader/schema_registry.cpp b/sdk/src/vtx_common/src/vtx/common/readers/schema_reader/schema_registry.cpp index 3b4ca36..a23444d 100644 --- a/sdk/src/vtx_common/src/vtx/common/readers/schema_reader/schema_registry.cpp +++ b/sdk/src/vtx_common/src/vtx/common/readers/schema_reader/schema_registry.cpp @@ -13,48 +13,31 @@ using json = nlohmann::json; static VTX::FieldType StringToTypeEnum(const std::string& type) { static const std::unordered_map map_type = { - {"int8", VTX::FieldType::Int8}, - {"Int8", VTX::FieldType::Int8}, - {"uint8", VTX::FieldType::Int8}, - {"UInt8", VTX::FieldType::Int8}, - {"int32", VTX::FieldType::Int32}, - {"Int32", VTX::FieldType::Int32}, - {"uint32", VTX::FieldType::Int32}, - {"UInt32", VTX::FieldType::Int32}, - {"int", VTX::FieldType::Int32}, - {"Int", VTX::FieldType::Int32}, - {"int64", VTX::FieldType::Int64}, - {"Int64", VTX::FieldType::Int64}, - {"uint64", VTX::FieldType::Int64}, - {"UInt64", VTX::FieldType::Int64}, - {"long", VTX::FieldType::Int64}, - {"Long", VTX::FieldType::Int64}, - {"float", VTX::FieldType::Float}, - {"Float", VTX::FieldType::Float}, - {"double", VTX::FieldType::Double}, - {"Double", VTX::FieldType::Double}, - {"bool", VTX::FieldType::Bool}, - {"Bool", VTX::FieldType::Bool}, - {"string", VTX::FieldType::String}, - {"String", VTX::FieldType::String}, - {"vector", VTX::FieldType::Vector}, - {"Vector", VTX::FieldType::Vector}, - {"quat", VTX::FieldType::Quat}, - {"Quat", VTX::FieldType::Quat}, - {"transform", VTX::FieldType::Transform}, - {"Transform", VTX::FieldType::Transform}, - {"struct", VTX::FieldType::Struct}, - {"Struct", VTX::FieldType::Struct} - }; + {"int8", VTX::FieldType::Int8}, {"Int8", VTX::FieldType::Int8}, + {"uint8", VTX::FieldType::Int8}, {"UInt8", VTX::FieldType::Int8}, + {"int32", VTX::FieldType::Int32}, {"Int32", VTX::FieldType::Int32}, + {"uint32", VTX::FieldType::Int32}, {"UInt32", VTX::FieldType::Int32}, + {"int", VTX::FieldType::Int32}, {"Int", VTX::FieldType::Int32}, + {"int64", VTX::FieldType::Int64}, {"Int64", VTX::FieldType::Int64}, + {"uint64", VTX::FieldType::Int64}, {"UInt64", VTX::FieldType::Int64}, + {"long", VTX::FieldType::Int64}, {"Long", VTX::FieldType::Int64}, + {"float", VTX::FieldType::Float}, {"Float", VTX::FieldType::Float}, + {"double", VTX::FieldType::Double}, {"Double", VTX::FieldType::Double}, + {"bool", VTX::FieldType::Bool}, {"Bool", VTX::FieldType::Bool}, + {"string", VTX::FieldType::String}, {"String", VTX::FieldType::String}, + {"vector", VTX::FieldType::Vector}, {"Vector", VTX::FieldType::Vector}, + {"quat", VTX::FieldType::Quat}, {"Quat", VTX::FieldType::Quat}, + {"transform", VTX::FieldType::Transform}, {"Transform", VTX::FieldType::Transform}, + {"struct", VTX::FieldType::Struct}, {"Struct", VTX::FieldType::Struct}}; auto it = map_type.find(type); - if (it != map_type.end()) return it->second; + if (it != map_type.end()) + return it->second; return VTX::FieldType::None; }; - static VTX::FieldContainerType StringToContainerEnum(const std::string& container) { static const std::unordered_map map_container = { {"array", VTX::FieldContainerType::Array}, @@ -62,17 +45,17 @@ static VTX::FieldContainerType StringToContainerEnum(const std::string& containe }; auto it = map_container.find(container); - if (it != map_container.end()) return it->second; + if (it != map_container.end()) + return it->second; return VTX::FieldContainerType::None; } -VTX::SchemaRegistry::SchemaRegistry() -{ +VTX::SchemaRegistry::SchemaRegistry() { b_is_valid_ = true; } -bool VTX::SchemaRegistry::LoadFromJson(const std::string& json_path,ELoadMethod load_method) { +bool VTX::SchemaRegistry::LoadFromJson(const std::string& json_path, ELoadMethod load_method) { std::ifstream file(json_path); if (!file.is_open()) { VTX_ERROR("Cannot open JSON: {}", json_path); @@ -87,14 +70,12 @@ bool VTX::SchemaRegistry::LoadFromJson(const std::string& json_path,ELoadMethod return LoadFromRawString(buffer.str()); } -bool VTX::SchemaRegistry::LoadFromRawString(const std::string& raw_json) -{ +bool VTX::SchemaRegistry::LoadFromRawString(const std::string& raw_json) { json_content_ = raw_json; json j; try { j = json::parse(json_content_); - } - catch (const json::parse_error& e) { + } catch (const json::parse_error& e) { VTX_ERROR("Error parsing JSON: {}", e.what()); b_is_valid_ = false; return b_is_valid_; @@ -112,7 +93,8 @@ bool VTX::SchemaRegistry::LoadFromRawString(const std::string& raw_json) for (const auto& struct_json : j["property_mapping"]) { std::string struct_name = struct_json.value("struct", ""); - if (struct_name.empty()) continue; + if (struct_name.empty()) + continue; struct_type_ids_[struct_name] = current_type_id_; current_type_id_++; @@ -168,19 +150,18 @@ bool VTX::SchemaRegistry::LoadFromRawString(const std::string& raw_json) } } - for (const auto& field : current_struct.fields) - { + for (const auto& field : current_struct.fields) { current_struct.field_map[field.name] = &field; - if (field.container_type == VTX::FieldContainerType::None) - { + if (field.container_type == VTX::FieldContainerType::None) { size_t typeIdx = static_cast(field.type_id); if (typeIdx >= current_struct.type_max_indices.size()) { current_struct.type_max_indices.resize(typeIdx + 1, 0); } - current_struct.type_max_indices[typeIdx] = std::max(current_struct.type_max_indices[typeIdx], field.index + 1); + current_struct.type_max_indices[typeIdx] = + std::max(current_struct.type_max_indices[typeIdx], field.index + 1); } } } @@ -206,7 +187,9 @@ bool VTX::SchemaRegistry::LoadFromRawString(const std::string& raw_json) addr.container_type = field.container_type; addr.child_type_name = field.struct_type; struct_cache.properties[field.name] = addr; - struct_cache.names_by_lookup_key[VTX::MakePropertyLookupKey(field.index, field.type_id, field.container_type)] = field.name; + struct_cache + .names_by_lookup_key[VTX::MakePropertyLookupKey(field.index, field.type_id, field.container_type)] = + field.name; struct_cache.property_order.push_back(field.name); } } @@ -240,7 +223,7 @@ const VTX::SchemaStruct* VTX::SchemaRegistry::GetStruct(const std::string& name) return nullptr; } -bool VTX::SchemaRegistry::GetIsValid() const{ +bool VTX::SchemaRegistry::GetIsValid() const { return b_is_valid_; } @@ -248,9 +231,11 @@ const std::unordered_map& VTX::SchemaRegistry::G return structs_; } -const VTX::SchemaField* VTX::SchemaRegistry::GetField(const std::string& struct_name, const std::string& field_name) const { +const VTX::SchemaField* VTX::SchemaRegistry::GetField(const std::string& struct_name, + const std::string& field_name) const { const auto* s = GetStruct(struct_name); - if (!s) return nullptr; + if (!s) + return nullptr; auto it = s->field_map.find(field_name); if (it != s->field_map.end()) { @@ -259,8 +244,7 @@ const VTX::SchemaField* VTX::SchemaRegistry::GetField(const std::string& struct_ return nullptr; } -int32_t VTX::SchemaRegistry::GetStructTypeId(const std::string& name) const -{ +int32_t VTX::SchemaRegistry::GetStructTypeId(const std::string& name) const { auto it = struct_type_ids_.find(name); if (it != struct_type_ids_.end()) { return it->second; diff --git a/sdk/src/vtx_differ/src/vtx/differ/adapters/flatbuffers/vtx_flatbuffer_view_adapter.cpp b/sdk/src/vtx_differ/src/vtx/differ/adapters/flatbuffers/vtx_flatbuffer_view_adapter.cpp index e087bd3..a2163bf 100644 --- a/sdk/src/vtx_differ/src/vtx/differ/adapters/flatbuffers/vtx_flatbuffer_view_adapter.cpp +++ b/sdk/src/vtx_differ/src/vtx/differ/adapters/flatbuffers/vtx_flatbuffer_view_adapter.cpp @@ -9,449 +9,549 @@ namespace VtxDiff::Flatbuffers { -namespace { - const reflection::Field* FindFieldByName(const reflection::Object* Obj, std::string_view Name) { - if (!Obj || !Obj->fields()) return nullptr; - for (auto* F : *Obj->fields()) { - if (F && F->name() && F->name()->string_view() == Name) return F; + namespace { + const reflection::Field* FindFieldByName(const reflection::Object* Obj, std::string_view Name) { + if (!Obj || !Obj->fields()) + return nullptr; + for (auto* F : *Obj->fields()) { + if (F && F->name() && F->name()->string_view() == Name) + return F; + } + return nullptr; } - return nullptr; - } - EVTXContainerType InferTypeFromName(std::string_view Name) { - - if (Name == "bool_properties") return EVTXContainerType::BoolProperties; - if (Name == "int32_properties") return EVTXContainerType::Int32Properties; - if (Name == "int64_properties") return EVTXContainerType::Int64Properties; - if (Name == "float_properties") return EVTXContainerType::FloatProperties; - if (Name == "double_properties") return EVTXContainerType::DoubleProperties; - if (Name == "string_properties") return EVTXContainerType::StringProperties; - if (Name == "transform_properties") return EVTXContainerType::TransformProperties; - if (Name == "vector_properties") return EVTXContainerType::VectorProperties; - if (Name == "quat_properties") return EVTXContainerType::QuatProperties; - if (Name == "range_properties") return EVTXContainerType::RangeProperties; - if (Name == "byte_array_properties") return EVTXContainerType::ByteArrayProperties; - - if (Name == "int32_arrays") return EVTXContainerType::Int32Arrays; - if (Name == "int64_arrays") return EVTXContainerType::Int64Arrays; - if (Name == "float_arrays") return EVTXContainerType::FloatArrays; - if (Name == "double_arrays") return EVTXContainerType::DoubleArrays; - if (Name == "string_arrays") return EVTXContainerType::StringArrays; - if (Name == "transform_arrays") return EVTXContainerType::TransformArrays; - if (Name == "vector_arrays") return EVTXContainerType::VectorArrays; - if (Name == "quat_arrays") return EVTXContainerType::QuatArrays; - if (Name == "range_arrays") return EVTXContainerType::RangeArrays; - if (Name == "bool_arrays") return EVTXContainerType::BoolArrays; - - if (Name == "any_struct_properties") return EVTXContainerType::AnyStructProperties; - if (Name == "any_struct_arrays" || Name == "entities") return EVTXContainerType::AnyStructArrays; - if (Name == "map_properties") return EVTXContainerType::MapProperties; - if (Name == "map_arrays") return EVTXContainerType::MapArrays; - - return EVTXContainerType::Unknown; - - return EVTXContainerType::Unknown; - } - - bool IsFlatArrayType(EVTXContainerType Type) { - switch (Type) { - case EVTXContainerType::Int32Arrays: - case EVTXContainerType::Int64Arrays: - case EVTXContainerType::FloatArrays: - case EVTXContainerType::DoubleArrays: - case EVTXContainerType::StringArrays: - case EVTXContainerType::TransformArrays: - case EVTXContainerType::VectorArrays: - case EVTXContainerType::QuatArrays: - case EVTXContainerType::RangeArrays: - case EVTXContainerType::BoolArrays: - case EVTXContainerType::AnyStructArrays: - case EVTXContainerType::MapArrays: - return true; - default: - return false; + EVTXContainerType InferTypeFromName(std::string_view Name) { + if (Name == "bool_properties") + return EVTXContainerType::BoolProperties; + if (Name == "int32_properties") + return EVTXContainerType::Int32Properties; + if (Name == "int64_properties") + return EVTXContainerType::Int64Properties; + if (Name == "float_properties") + return EVTXContainerType::FloatProperties; + if (Name == "double_properties") + return EVTXContainerType::DoubleProperties; + if (Name == "string_properties") + return EVTXContainerType::StringProperties; + if (Name == "transform_properties") + return EVTXContainerType::TransformProperties; + if (Name == "vector_properties") + return EVTXContainerType::VectorProperties; + if (Name == "quat_properties") + return EVTXContainerType::QuatProperties; + if (Name == "range_properties") + return EVTXContainerType::RangeProperties; + if (Name == "byte_array_properties") + return EVTXContainerType::ByteArrayProperties; + + if (Name == "int32_arrays") + return EVTXContainerType::Int32Arrays; + if (Name == "int64_arrays") + return EVTXContainerType::Int64Arrays; + if (Name == "float_arrays") + return EVTXContainerType::FloatArrays; + if (Name == "double_arrays") + return EVTXContainerType::DoubleArrays; + if (Name == "string_arrays") + return EVTXContainerType::StringArrays; + if (Name == "transform_arrays") + return EVTXContainerType::TransformArrays; + if (Name == "vector_arrays") + return EVTXContainerType::VectorArrays; + if (Name == "quat_arrays") + return EVTXContainerType::QuatArrays; + if (Name == "range_arrays") + return EVTXContainerType::RangeArrays; + if (Name == "bool_arrays") + return EVTXContainerType::BoolArrays; + + if (Name == "any_struct_properties") + return EVTXContainerType::AnyStructProperties; + if (Name == "any_struct_arrays" || Name == "entities") + return EVTXContainerType::AnyStructArrays; + if (Name == "map_properties") + return EVTXContainerType::MapProperties; + if (Name == "map_arrays") + return EVTXContainerType::MapArrays; + + return EVTXContainerType::Unknown; + + return EVTXContainerType::Unknown; } - } -} + bool IsFlatArrayType(EVTXContainerType Type) { + switch (Type) { + case EVTXContainerType::Int32Arrays: + case EVTXContainerType::Int64Arrays: + case EVTXContainerType::FloatArrays: + case EVTXContainerType::DoubleArrays: + case EVTXContainerType::StringArrays: + case EVTXContainerType::TransformArrays: + case EVTXContainerType::VectorArrays: + case EVTXContainerType::QuatArrays: + case EVTXContainerType::RangeArrays: + case EVTXContainerType::BoolArrays: + case EVTXContainerType::AnyStructArrays: + case EVTXContainerType::MapArrays: + return true; + default: + return false; + } + } + } // namespace -void FbSchemaCache::Build(const reflection::Schema* Schema) { - RawSchema = Schema; - ObjectCache.clear(); - if (!Schema || !Schema->objects()) return; - for (auto* Obj : *Schema->objects()) { - if (!Obj || !Obj->fields()) continue; + void FbSchemaCache::Build(const reflection::Schema* Schema) { + RawSchema = Schema; + ObjectCache.clear(); + if (!Schema || !Schema->objects()) + return; - CachedObject CObj; - CObj.EnumerableFields.reserve(Obj->fields()->size()); + for (auto* Obj : *Schema->objects()) { + if (!Obj || !Obj->fields()) + continue; - for (auto* F : *Obj->fields()) { - if (!F || !F->name()) continue; + CachedObject CObj; + CObj.EnumerableFields.reserve(Obj->fields()->size()); - CachedField CF; - CF.FbField = F; - std::string_view Name = F->name()->string_view(); - CF.VtxType = InferTypeFromName(Name); - CF.BaseType = F->type()->base_type(); - CF.ElementType = F->type()->element(); + for (auto* F : *Obj->fields()) { + if (!F || !F->name()) + continue; - if (CF.BaseType != reflection::Vector && CF.BaseType != reflection::Obj) { - CF.ByteSize = flatbuffers::GetTypeSize(static_cast(static_cast(CF.BaseType))); - } else if (CF.BaseType == reflection::Obj) { - const auto* ObjSchema = Schema->objects()->Get(F->type()->index()); - if (ObjSchema && ObjSchema->is_struct()) CF.ByteSize = static_cast(ObjSchema->bytesize()); - } + CachedField CF; + CF.FbField = F; + std::string_view Name = F->name()->string_view(); + CF.VtxType = InferTypeFromName(Name); + CF.BaseType = F->type()->base_type(); + CF.ElementType = F->type()->element(); - if (CF.BaseType == reflection::Vector) { - if (CF.ElementType == reflection::Obj) { + if (CF.BaseType != reflection::Vector && CF.BaseType != reflection::Obj) { + CF.ByteSize = flatbuffers::GetTypeSize( + static_cast(static_cast(CF.BaseType))); + } else if (CF.BaseType == reflection::Obj) { const auto* ObjSchema = Schema->objects()->Get(F->type()->index()); - if (ObjSchema && ObjSchema->is_struct()) CF.ElementByteSize = static_cast(ObjSchema->bytesize()); - } else { - CF.ElementByteSize = flatbuffers::GetTypeSize(static_cast(static_cast(CF.ElementType))); + if (ObjSchema && ObjSchema->is_struct()) + CF.ByteSize = static_cast(ObjSchema->bytesize()); } - } - FieldDesc D; - D.name = std::string(Name); - D.type = CF.VtxType; - D.is_array_like = IsFlatArrayType(CF.VtxType); - D.is_map_like = (D.type == EVTXContainerType::MapProperties || D.type == EVTXContainerType::MapArrays); - CObj.EnumerableFields.push_back(D); - - if (D.is_array_like && CF.BaseType == reflection::Obj) { - CF.NestedObj = Schema->objects()->Get(F->type()->index()); - if (CF.NestedObj) { - CF.SoaDataField = FindFieldByName(CF.NestedObj, "data"); - CF.SoaOffsetsField = FindFieldByName(CF.NestedObj, "offsets"); + if (CF.BaseType == reflection::Vector) { + if (CF.ElementType == reflection::Obj) { + const auto* ObjSchema = Schema->objects()->Get(F->type()->index()); + if (ObjSchema && ObjSchema->is_struct()) + CF.ElementByteSize = static_cast(ObjSchema->bytesize()); + } else { + CF.ElementByteSize = flatbuffers::GetTypeSize(static_cast( + static_cast(CF.ElementType))); + } } - } - if (D.is_map_like && CF.BaseType == reflection::Vector) { - CF.NestedObj = Schema->objects()->Get(F->type()->index()); - if (CF.NestedObj) { - CF.MapKeysField = FindFieldByName(CF.NestedObj, "keys"); - CF.MapValuesField = FindFieldByName(CF.NestedObj, "values"); - if (CF.MapValuesField) { - CF.MapValuesObj = Schema->objects()->Get(CF.MapValuesField->type()->index()); + FieldDesc D; + D.name = std::string(Name); + D.type = CF.VtxType; + D.is_array_like = IsFlatArrayType(CF.VtxType); + D.is_map_like = (D.type == EVTXContainerType::MapProperties || D.type == EVTXContainerType::MapArrays); + CObj.EnumerableFields.push_back(D); + + if (D.is_array_like && CF.BaseType == reflection::Obj) { + CF.NestedObj = Schema->objects()->Get(F->type()->index()); + if (CF.NestedObj) { + CF.SoaDataField = FindFieldByName(CF.NestedObj, "data"); + CF.SoaOffsetsField = FindFieldByName(CF.NestedObj, "offsets"); + } + } + + if (D.is_map_like && CF.BaseType == reflection::Vector) { + CF.NestedObj = Schema->objects()->Get(F->type()->index()); + if (CF.NestedObj) { + CF.MapKeysField = FindFieldByName(CF.NestedObj, "keys"); + CF.MapValuesField = FindFieldByName(CF.NestedObj, "values"); + if (CF.MapValuesField) { + CF.MapValuesObj = Schema->objects()->Get(CF.MapValuesField->type()->index()); + } } } - } - CObj.FieldsByName[Name] = CF; + CObj.FieldsByName[Name] = CF; + } + ObjectCache[Obj] = std::move(CObj); } - ObjectCache[Obj] = std::move(CObj); } -} - -const CachedField* FbSchemaCache::GetCachedField(const reflection::Object* Obj, std::string_view FieldName) const { - auto It = ObjectCache.find(Obj); - if (It != ObjectCache.end()) { - auto FieldIt = It->second.FieldsByName.find(FieldName); - if (FieldIt != It->second.FieldsByName.end()) { - return &FieldIt->second; + + const CachedField* FbSchemaCache::GetCachedField(const reflection::Object* Obj, std::string_view FieldName) const { + auto It = ObjectCache.find(Obj); + if (It != ObjectCache.end()) { + auto FieldIt = It->second.FieldsByName.find(FieldName); + if (FieldIt != It->second.FieldsByName.end()) { + return &FieldIt->second; + } } + return nullptr; } - return nullptr; -} - -const std::vector& FbSchemaCache::GetEnumerableFields(const reflection::Object* Obj) const { - auto It = ObjectCache.find(Obj); - if (It != ObjectCache.end()) return It->second.EnumerableFields; - static std::vector Empty; - return Empty; -} - - -FlatbufferViewAdapter::FlatbufferViewAdapter(const FbSchemaCache* Cache, - const reflection::Object* Object, - const flatbuffers::Table* TablePtr) - : Cache(Cache), Object(Object), TablePtr(TablePtr) {} - -void FlatbufferViewAdapter::Reset() -{ - -} - -bool FlatbufferViewAdapter::IsValid() const -{ - return TablePtr && Cache && Object; -} - -std::optional FlatbufferViewAdapter::CreateRoot(const FbSchemaCache* Cache, const char* RootObjectName, const uint8_t* Buffer, size_t BufferSize) { - if (!Cache || !Cache->RawSchema || !RootObjectName || !Buffer) return std::nullopt; - - const reflection::Object* RootObj = nullptr; - for (auto* O : *Cache->RawSchema->objects()) { - if (O && O->name() && std::strcmp(O->name()->c_str(), RootObjectName) == 0) { - RootObj = O; - break; - } + + const std::vector& FbSchemaCache::GetEnumerableFields(const reflection::Object* Obj) const { + auto It = ObjectCache.find(Obj); + if (It != ObjectCache.end()) + return It->second.EnumerableFields; + static std::vector Empty; + return Empty; } - if (!RootObj) return std::nullopt; - auto* Tbl = flatbuffers::GetRoot(Buffer); - return FlatbufferViewAdapter(Cache, RootObj, Tbl); -} -std::span FlatbufferViewAdapter::EnumerateFields() const { - return Cache->GetEnumerableFields(Object); -} + FlatbufferViewAdapter::FlatbufferViewAdapter(const FbSchemaCache* Cache, const reflection::Object* Object, + const flatbuffers::Table* TablePtr) + : Cache(Cache) + , Object(Object) + , TablePtr(TablePtr) {} -const reflection::Field* FlatbufferViewAdapter::FindField(std::string_view FieldName) const { - return FindFieldByName(Object, FieldName); -} + void FlatbufferViewAdapter::Reset() {} -const uint8_t* FlatbufferViewAdapter::GetVectorData(const flatbuffers::Table* Tbl, const reflection::Field* F, size_t& Count, size_t& ElemSize) const { - Count = 0; ElemSize = 0; - if (!Tbl || !F || F->type()->base_type() != reflection::Vector) return nullptr; + bool FlatbufferViewAdapter::IsValid() const { + return TablePtr && Cache && Object; + } - auto VecU8 = Tbl->GetPointer*>(F->offset()); - if (!VecU8) return nullptr; - Count = VecU8->size(); + std::optional FlatbufferViewAdapter::CreateRoot(const FbSchemaCache* Cache, + const char* RootObjectName, + const uint8_t* Buffer, size_t BufferSize) { + if (!Cache || !Cache->RawSchema || !RootObjectName || !Buffer) + return std::nullopt; + + const reflection::Object* RootObj = nullptr; + for (auto* O : *Cache->RawSchema->objects()) { + if (O && O->name() && std::strcmp(O->name()->c_str(), RootObjectName) == 0) { + RootObj = O; + break; + } + } + if (!RootObj) + return std::nullopt; - if (F->type()->element() == reflection::Obj) { - const auto* ObjSchema = Cache->RawSchema->objects()->Get(F->type()->index()); - ElemSize = (ObjSchema && ObjSchema->is_struct()) ? static_cast(ObjSchema->bytesize()) : 0; - } else { - ElemSize = flatbuffers::GetTypeSize(static_cast(static_cast(F->type()->element()))); + auto* Tbl = flatbuffers::GetRoot(Buffer); + return FlatbufferViewAdapter(Cache, RootObj, Tbl); } - return VecU8->Data(); -} - -bool FlatbufferViewAdapter::IsFlatArrayType(EVTXContainerType Type) const { - return (Type >= EVTXContainerType::ByteArrayProperties && Type <= EVTXContainerType::BoolArrays) || - Type == EVTXContainerType::AnyStructArrays || - Type == EVTXContainerType::MapArrays; -} - -size_t FlatbufferViewAdapter::GetArraySize(const FieldDesc& Fd) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF || !CF->FbField) return 0; - - if (CF->SoaOffsetsField) { - const flatbuffers::Table* NestedTable = TablePtr->GetPointer(CF->FbField->offset()); - if (!NestedTable) return 0; + + std::span FlatbufferViewAdapter::EnumerateFields() const { + return Cache->GetEnumerableFields(Object); + } + + const reflection::Field* FlatbufferViewAdapter::FindField(std::string_view FieldName) const { + return FindFieldByName(Object, FieldName); + } + + const uint8_t* FlatbufferViewAdapter::GetVectorData(const flatbuffers::Table* Tbl, const reflection::Field* F, + size_t& Count, size_t& ElemSize) const { + Count = 0; + ElemSize = 0; + if (!Tbl || !F || F->type()->base_type() != reflection::Vector) + return nullptr; + + auto VecU8 = Tbl->GetPointer*>(F->offset()); + if (!VecU8) + return nullptr; + Count = VecU8->size(); + + if (F->type()->element() == reflection::Obj) { + const auto* ObjSchema = Cache->RawSchema->objects()->Get(F->type()->index()); + ElemSize = (ObjSchema && ObjSchema->is_struct()) ? static_cast(ObjSchema->bytesize()) : 0; + } else { + ElemSize = flatbuffers::GetTypeSize( + static_cast(static_cast(F->type()->element()))); + } + return VecU8->Data(); + } + + bool FlatbufferViewAdapter::IsFlatArrayType(EVTXContainerType Type) const { + return (Type >= EVTXContainerType::ByteArrayProperties && Type <= EVTXContainerType::BoolArrays) || + Type == EVTXContainerType::AnyStructArrays || Type == EVTXContainerType::MapArrays; + } + + size_t FlatbufferViewAdapter::GetArraySize(const FieldDesc& Fd) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF || !CF->FbField) + return 0; + + if (CF->SoaOffsetsField) { + const flatbuffers::Table* NestedTable = + TablePtr->GetPointer(CF->FbField->offset()); + if (!NestedTable) + return 0; + size_t count = 0, elemSize = 0; + GetVectorData(NestedTable, CF->SoaOffsetsField, count, elemSize); + return count; + } + size_t count = 0, elemSize = 0; - GetVectorData(NestedTable, CF->SoaOffsetsField, count, elemSize); + GetVectorData(TablePtr, CF->FbField, count, elemSize); return count; } - size_t count = 0, elemSize = 0; - GetVectorData(TablePtr, CF->FbField, count, elemSize); - return count; -} + std::span FlatbufferViewAdapter::GetFieldBytes(const FieldDesc& Fd) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF || !CF->FbField) + return {}; -std::span FlatbufferViewAdapter::GetFieldBytes(const FieldDesc& Fd) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF || !CF->FbField) return {}; + auto Voff = static_cast(CF->FbField->offset()); - auto Voff = static_cast(CF->FbField->offset()); + if (CF->BaseType == reflection::String) { + const flatbuffers::String* S = TablePtr->GetPointer(Voff); + if (!S) + return {}; + return {reinterpret_cast(S->c_str()), S->size()}; + } - if (CF->BaseType == reflection::String) { - const flatbuffers::String* S = TablePtr->GetPointer(Voff); - if (!S) return {}; - return { reinterpret_cast(S->c_str()), S->size() }; + if (CF->BaseType != reflection::Vector && CF->BaseType != reflection::Obj) { + const uint8_t* Ptr = TablePtr->GetAddressOf(Voff); + if (!Ptr) + return {}; + return {reinterpret_cast(Ptr), CF->ByteSize}; + } + + if (CF->BaseType == reflection::Obj && CF->ByteSize > 0) { + const uint8_t* Ptr = TablePtr->GetStruct(Voff); + if (!Ptr) + return {}; + return {reinterpret_cast(Ptr), CF->ByteSize}; + } + return {}; } - if (CF->BaseType != reflection::Vector && CF->BaseType != reflection::Obj) { - const uint8_t* Ptr = TablePtr->GetAddressOf(Voff); - if (!Ptr) return {}; - return { reinterpret_cast(Ptr), CF->ByteSize }; + std::span FlatbufferViewAdapter::GetArrayElementBytes(const FieldDesc& Fd, size_t Index) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF) + return {}; + + if (CF->SoaDataField) + return GetSubArrayBytes(Fd, Index); + + if (!CF->FbField || CF->BaseType != reflection::Vector) + return {}; + + if (CF->ElementType == reflection::String) { + auto Vec = TablePtr->GetPointer>*>( + CF->FbField->offset()); + if (!Vec || Index >= Vec->size()) + return {}; + const flatbuffers::String* S = Vec->Get(Index); + return S ? std::span {reinterpret_cast(S->c_str()), S->size()} + : std::span {}; + } + + auto VecU8 = TablePtr->GetPointer*>(CF->FbField->offset()); + if (!VecU8 || Index >= VecU8->size() || CF->ElementByteSize == 0) + return {}; + + return {reinterpret_cast(VecU8->Data() + Index * CF->ElementByteSize), CF->ElementByteSize}; } - if (CF->BaseType == reflection::Obj && CF->ByteSize > 0) { - const uint8_t* Ptr = TablePtr->GetStruct(Voff); - if (!Ptr) return {}; - return { reinterpret_cast(Ptr), CF->ByteSize }; + std::span FlatbufferViewAdapter::GetSubArrayBytes(const FieldDesc& Fd, size_t SubIndex) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF || !CF->SoaDataField || !CF->SoaOffsetsField) + return {}; + + const flatbuffers::Table* NestedTable = TablePtr->GetPointer(CF->FbField->offset()); + if (!NestedTable) + return {}; + + auto VecOffsets = NestedTable->GetPointer*>(CF->SoaOffsetsField->offset()); + if (!VecOffsets || SubIndex >= VecOffsets->size()) + return {}; + + uint32_t StartElem = VecOffsets->Get(static_cast(SubIndex)); + + if (CF->SoaDataField->type()->element() == reflection::String) { + auto Vec = NestedTable->GetPointer>*>( + CF->SoaDataField->offset()); + if (!Vec) + return {}; + + uint32_t EndElem = (SubIndex + 1 < VecOffsets->size()) + ? VecOffsets->Get(static_cast(SubIndex + 1)) + : static_cast(Vec->size()); + if (EndElem < StartElem || EndElem > Vec->size()) + return {}; + + size_t totalBytes = 0; + for (uint32_t i = StartElem; i < EndElem; ++i) { + const flatbuffers::String* S = Vec->Get(i); + totalBytes += 4 + (S ? S->size() : 0); + } + + static thread_local std::vector Scratch; + Scratch.resize(totalBytes); + + std::byte* Dest = Scratch.data(); + for (uint32_t i = StartElem; i < EndElem; ++i) { + const flatbuffers::String* S = Vec->Get(i); + uint32_t Len = S ? static_cast(S->size()) : 0u; + std::memcpy(Dest, &Len, 4); + Dest += 4; + if (Len) { + std::memcpy(Dest, S->c_str(), Len); + Dest += Len; + } + } + return {Scratch.data(), Scratch.size()}; + } + + auto VecData = NestedTable->GetPointer*>(CF->SoaDataField->offset()); + if (!VecData) + return {}; + + size_t DataElemSize = flatbuffers::GetTypeSize(static_cast( + static_cast(CF->SoaDataField->type()->element()))); + + uint32_t EndElem = (SubIndex + 1 < VecOffsets->size()) + ? VecOffsets->Get(static_cast(SubIndex + 1)) + : static_cast(VecData->size() / DataElemSize); + if (EndElem < StartElem) + return {}; + + size_t ByteStart = StartElem * DataElemSize; + size_t ByteLen = (EndElem - StartElem) * DataElemSize; + + return {reinterpret_cast(VecData->Data() + ByteStart), ByteLen}; } - return {}; -} -std::span FlatbufferViewAdapter::GetArrayElementBytes(const FieldDesc& Fd, size_t Index) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF) return {}; + FlatbufferViewAdapter FlatbufferViewAdapter::GetArrayElementAsStruct(const FieldDesc& Fd, size_t Index) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF || !CF->FbField || CF->FbField->type()->base_type() != reflection::Vector || + CF->FbField->type()->element() != reflection::Obj) + return {}; // Devuelve vacío - if (CF->SoaDataField) return GetSubArrayBytes(Fd, Index); + const auto* NestedObj = Cache->RawSchema->objects()->Get(CF->FbField->type()->index()); + if (!NestedObj || NestedObj->is_struct()) + return {}; + + auto Vec = TablePtr->GetPointer>*>( + CF->FbField->offset()); + if (!Vec || Index >= Vec->size()) + return {}; + return FlatbufferViewAdapter(Cache, NestedObj, Vec->Get(Index)); + } - if (!CF->FbField || CF->BaseType != reflection::Vector) return {}; + FlatbufferViewAdapter FlatbufferViewAdapter::GetNestedStruct(const FieldDesc& Fd) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF || !CF->FbField || CF->FbField->type()->base_type() != reflection::Obj) + return {}; + + const auto* NestedObj = Cache->RawSchema->objects()->Get(CF->FbField->type()->index()); + if (!NestedObj || NestedObj->is_struct()) + return {}; - if (CF->ElementType == reflection::String) { - auto Vec = TablePtr->GetPointer>*>(CF->FbField->offset()); - if (!Vec || Index >= Vec->size()) return {}; - const flatbuffers::String* S = Vec->Get(Index); - return S ? std::span{reinterpret_cast(S->c_str()), S->size()} : std::span{}; + const flatbuffers::Table* NestedTbl = TablePtr->GetPointer(CF->FbField->offset()); + if (!NestedTbl) + return {}; + + return FlatbufferViewAdapter(Cache, NestedObj, NestedTbl); } - auto VecU8 = TablePtr->GetPointer*>(CF->FbField->offset()); - if (!VecU8 || Index >= VecU8->size() || CF->ElementByteSize == 0) return {}; - - return { reinterpret_cast(VecU8->Data() + Index * CF->ElementByteSize), CF->ElementByteSize }; -} + FlatbufferViewAdapter FlatbufferViewAdapter::GetMapValueAsStruct(const FieldDesc& Fd, size_t I) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF || !CF->MapValuesField || !CF->MapValuesObj) + return {}; -std::span FlatbufferViewAdapter::GetSubArrayBytes(const FieldDesc& Fd, size_t SubIndex) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF || !CF->SoaDataField || !CF->SoaOffsetsField) return {}; + auto Vec = TablePtr->GetPointer>*>( + CF->FbField->offset()); + if (!Vec || I >= Vec->size()) + return {}; - const flatbuffers::Table* NestedTable = TablePtr->GetPointer(CF->FbField->offset()); - if (!NestedTable) return {}; + const flatbuffers::Table* MC = Vec->Get(I); + if (!MC) + return {}; - auto VecOffsets = NestedTable->GetPointer*>(CF->SoaOffsetsField->offset()); - if (!VecOffsets || SubIndex >= VecOffsets->size()) return {}; + auto Values = MC->GetPointer>*>( + CF->MapValuesField->offset()); + if (!Values || Values->size() == 0) + return {}; - uint32_t StartElem = VecOffsets->Get(static_cast(SubIndex)); + size_t ValIndex = std::min(I, Values->size() - 1); + return FlatbufferViewAdapter(Cache, CF->MapValuesObj, + Values->Get(static_cast(ValIndex))); + } - if (CF->SoaDataField->type()->element() == reflection::String) { - auto Vec = NestedTable->GetPointer>*>(CF->SoaDataField->offset()); - if (!Vec) return {}; + FlatbufferViewAdapter FlatbufferViewAdapter::GetFieldByName(const std::string& FieldName) const { + FieldDesc fd; + fd.name = FieldName; + return GetNestedStruct(fd); + } - uint32_t EndElem = (SubIndex + 1 < VecOffsets->size()) ? VecOffsets->Get(static_cast(SubIndex + 1)) : static_cast(Vec->size()); - if (EndElem < StartElem || EndElem > Vec->size()) return {}; + size_t FlatbufferViewAdapter::GetMapSize(const FieldDesc& Fd) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF || !CF->FbField) + return 0; + auto Vec = TablePtr->GetPointer>*>( + CF->FbField->offset()); + return Vec ? Vec->size() : 0; + } - size_t totalBytes = 0; - for (uint32_t i = StartElem; i < EndElem; ++i) { - const flatbuffers::String* S = Vec->Get(i); - totalBytes += 4 + (S ? S->size() : 0); - } + std::string FlatbufferViewAdapter::GetMapKey(const FieldDesc& Fd, size_t I) const { + const auto* CF = Cache->GetCachedField(Object, Fd.name); + if (!CF || !CF->MapKeysField) + return ""; - static thread_local std::vector Scratch; - Scratch.resize(totalBytes); - - std::byte* Dest = Scratch.data(); - for (uint32_t i = StartElem; i < EndElem; ++i) { - const flatbuffers::String* S = Vec->Get(i); - uint32_t Len = S ? static_cast(S->size()) : 0u; - std::memcpy(Dest, &Len, 4); - Dest += 4; - if (Len) { - std::memcpy(Dest, S->c_str(), Len); - Dest += Len; - } - } - return { Scratch.data(), Scratch.size() }; + auto Vec = TablePtr->GetPointer>*>( + CF->FbField->offset()); + if (!Vec || I >= Vec->size()) + return ""; + + const flatbuffers::Table* MC = Vec->Get(I); + if (!MC) + return ""; + + auto Keys = MC->GetPointer>*>( + CF->MapKeysField->offset()); + if (!Keys || Keys->size() == 0) + return ""; + + size_t KeyIndex = std::min(I, Keys->size() - 1); + return Keys->Get(static_cast(KeyIndex))->str(); } - auto VecData = NestedTable->GetPointer*>(CF->SoaDataField->offset()); - if (!VecData) return {}; - - size_t DataElemSize = flatbuffers::GetTypeSize(static_cast(static_cast(CF->SoaDataField->type()->element()))); - - uint32_t EndElem = (SubIndex + 1 < VecOffsets->size()) ? VecOffsets->Get(static_cast(SubIndex + 1)) : static_cast(VecData->size() / DataElemSize); - if (EndElem < StartElem) return {}; - - size_t ByteStart = StartElem * DataElemSize; - size_t ByteLen = (EndElem - StartElem) * DataElemSize; - - return { reinterpret_cast(VecData->Data() + ByteStart), ByteLen }; -} - -FlatbufferViewAdapter FlatbufferViewAdapter::GetArrayElementAsStruct(const FieldDesc& Fd, size_t Index) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF || !CF->FbField || CF->FbField->type()->base_type() != reflection::Vector || CF->FbField->type()->element() != reflection::Obj) return {}; // Devuelve vacío - - const auto* NestedObj = Cache->RawSchema->objects()->Get(CF->FbField->type()->index()); - if (!NestedObj || NestedObj->is_struct()) return {}; - - auto Vec = TablePtr->GetPointer>*>(CF->FbField->offset()); - if (!Vec || Index >= Vec->size()) return {}; - return FlatbufferViewAdapter(Cache, NestedObj, Vec->Get(Index)); -} - -FlatbufferViewAdapter FlatbufferViewAdapter::GetNestedStruct(const FieldDesc& Fd) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF || !CF->FbField || CF->FbField->type()->base_type() != reflection::Obj) return {}; - - const auto* NestedObj = Cache->RawSchema->objects()->Get(CF->FbField->type()->index()); - if (!NestedObj || NestedObj->is_struct()) return {}; - - const flatbuffers::Table* NestedTbl = TablePtr->GetPointer(CF->FbField->offset()); - if (!NestedTbl) return {}; - - return FlatbufferViewAdapter(Cache, NestedObj, NestedTbl); -} - -FlatbufferViewAdapter FlatbufferViewAdapter::GetMapValueAsStruct(const FieldDesc& Fd, size_t I) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF || !CF->MapValuesField || !CF->MapValuesObj) return {}; - - auto Vec = TablePtr->GetPointer>*>(CF->FbField->offset()); - if (!Vec || I >= Vec->size()) return {}; - - const flatbuffers::Table* MC = Vec->Get(I); - if (!MC) return {}; - - auto Values = MC->GetPointer>*>(CF->MapValuesField->offset()); - if (!Values || Values->size() == 0) return {}; - - size_t ValIndex = std::min(I, Values->size() - 1); - return FlatbufferViewAdapter(Cache, CF->MapValuesObj, Values->Get(static_cast(ValIndex))); -} - -FlatbufferViewAdapter FlatbufferViewAdapter::GetFieldByName(const std::string& FieldName) const { - FieldDesc fd; fd.name = FieldName; return GetNestedStruct(fd); -} - -size_t FlatbufferViewAdapter::GetMapSize(const FieldDesc& Fd) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF || !CF->FbField) return 0; - auto Vec = TablePtr->GetPointer>*>(CF->FbField->offset()); - return Vec ? Vec->size() : 0; -} - -std::string FlatbufferViewAdapter::GetMapKey(const FieldDesc& Fd, size_t I) const { - const auto* CF = Cache->GetCachedField(Object, Fd.name); - if (!CF || !CF->MapKeysField) return ""; - - auto Vec = TablePtr->GetPointer>*>(CF->FbField->offset()); - if (!Vec || I >= Vec->size()) return ""; - - const flatbuffers::Table* MC = Vec->Get(I); - if (!MC) return ""; - - auto Keys = MC->GetPointer>*>(CF->MapKeysField->offset()); - if (!Keys || Keys->size() == 0) return ""; - - size_t KeyIndex = std::min(I, Keys->size() - 1); - return Keys->Get(static_cast(KeyIndex))->str(); -} - -std::string FlatbufferViewAdapter::GetScalarFieldString(const std::string& FieldName) const { - if (!TablePtr || !Object) return {}; - - const reflection::Field* FbField = FindField(FieldName); - if (!FbField) return {}; - - auto voffset = static_cast(FbField->offset()); - auto base_type = FbField->type()->base_type(); - - switch (base_type) { - case reflection::String: - { - const auto* s = TablePtr->GetPointer(voffset); - return s ? s->str() : std::string{}; - } + std::string FlatbufferViewAdapter::GetScalarFieldString(const std::string& FieldName) const { + if (!TablePtr || !Object) + return {}; + + const reflection::Field* FbField = FindField(FieldName); + if (!FbField) + return {}; + + auto voffset = static_cast(FbField->offset()); + auto base_type = FbField->type()->base_type(); + + switch (base_type) { + case reflection::String: { + const auto* s = TablePtr->GetPointer(voffset); + return s ? s->str() : std::string {}; + } case reflection::ULong: - return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); + return std::to_string( + TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); case reflection::Long: - return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); + return std::to_string( + TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); case reflection::UInt: - return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); + return std::to_string( + TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); case reflection::Int: - return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); + return std::to_string( + TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); case reflection::Float: return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_real()))); case reflection::Double: return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_real()))); case reflection::Bool: - return TablePtr->GetField(voffset, static_cast(FbField->default_integer())) ? "true" : "false"; + return TablePtr->GetField(voffset, static_cast(FbField->default_integer())) ? "true" + : "false"; case reflection::UByte: - return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); + return std::to_string( + TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); case reflection::Byte: return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); case reflection::UShort: - return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); + return std::to_string( + TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); case reflection::Short: - return std::to_string(TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); + return std::to_string( + TablePtr->GetField(voffset, static_cast(FbField->default_integer()))); case reflection::None: case reflection::UType: case reflection::Vector: @@ -462,43 +562,47 @@ std::string FlatbufferViewAdapter::GetScalarFieldString(const std::string& Field case reflection::MaxBaseType: default: return {}; + } } -} - -uint64_t FlatbufferViewAdapter::GetUint64Field(const std::string& FieldName) const -{ - const reflection::Field* FbField = FindField(FieldName); - if (!FbField) return 0; - - auto voffset = static_cast(FbField->offset()); - auto base_type = FbField->type()->base_type(); - if ( base_type == reflection::ULong) - { - return TablePtr->GetField(voffset, static_cast(FbField->default_integer())); + + uint64_t FlatbufferViewAdapter::GetUint64Field(const std::string& FieldName) const { + const reflection::Field* FbField = FindField(FieldName); + if (!FbField) + return 0; + + auto voffset = static_cast(FbField->offset()); + auto base_type = FbField->type()->base_type(); + if (base_type == reflection::ULong) { + return TablePtr->GetField(voffset, static_cast(FbField->default_integer())); + } + return 0; + } + + + std::optional FbViewFactory::CreateRoot(std::span Buffer) { + if (RootName.empty() || Buffer.empty() || !GlobalCache.RawSchema) + return std::nullopt; + return FlatbufferViewAdapter::CreateRoot(&GlobalCache, RootName.c_str(), + reinterpret_cast(Buffer.data()), Buffer.size()); + } + + bool FbViewFactory::InitFromFile(const std::string& Path, const std::string& RootType) { + return false; + } + + bool FbViewFactory::InitFromMemory(const uint8_t* Data, size_t Size, const std::string& RootType) { + const uint8_t* schemaData = fbsvtx::FileHeaderBinarySchema::data(); + size_t schemaSize = fbsvtx::FileHeaderBinarySchema::size(); + + if (!schemaData || schemaSize == 0) + return false; + const reflection::Schema* loadedSchema = reflection::GetSchema(schemaData); + if (!loadedSchema) + return false; + + GlobalCache.Build(loadedSchema); + RootName = RootType; + return true; } - return 0; -} - - -std::optional FbViewFactory::CreateRoot(std::span Buffer){ - if (RootName.empty() || Buffer.empty() || !GlobalCache.RawSchema) return std::nullopt; - return FlatbufferViewAdapter::CreateRoot(&GlobalCache, RootName.c_str(), reinterpret_cast(Buffer.data()), Buffer.size()); -} - -bool FbViewFactory::InitFromFile(const std::string& Path, const std::string& RootType) { return false; } - -bool FbViewFactory::InitFromMemory(const uint8_t* Data, size_t Size, const std::string& RootType) { - - const uint8_t* schemaData = fbsvtx::FileHeaderBinarySchema::data(); - size_t schemaSize = fbsvtx::FileHeaderBinarySchema::size(); - - if (!schemaData || schemaSize == 0) return false; - const reflection::Schema* loadedSchema = reflection::GetSchema(schemaData); - if (!loadedSchema) return false; - - GlobalCache.Build(loadedSchema); - RootName = RootType; - return true; -} } // namespace VtxDiff::Flatbuffers \ No newline at end of file diff --git a/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_proto_verify.h b/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_proto_verify.h index 85051ff..ebcf1a6 100644 --- a/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_proto_verify.h +++ b/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_proto_verify.h @@ -12,19 +12,20 @@ #include "google/protobuf/util/message_differencer.h" -namespace VtxDiff::Protobuf -{ - static bool ProtoEquals(const google::protobuf::Message& A,const google::protobuf::Message& B,std::string* OutDiff) - { - using google::protobuf::util::MessageDifferencer; - MessageDifferencer Diff; - Diff.set_message_field_comparison(MessageDifferencer::EQUIVALENT); - - std::string report; - if (OutDiff) Diff.ReportDifferencesToString(&report); - - const bool Ok = Diff.Compare(A, B); - if (!Ok && OutDiff) *OutDiff = report; - return Ok; - } -} \ No newline at end of file +namespace VtxDiff::Protobuf { + static bool ProtoEquals(const google::protobuf::Message& A, const google::protobuf::Message& B, + std::string* OutDiff) { + using google::protobuf::util::MessageDifferencer; + MessageDifferencer Diff; + Diff.set_message_field_comparison(MessageDifferencer::EQUIVALENT); + + std::string report; + if (OutDiff) + Diff.ReportDifferencesToString(&report); + + const bool Ok = Diff.Compare(A, B); + if (!Ok && OutDiff) + *OutDiff = report; + return Ok; + } +} // namespace VtxDiff::Protobuf \ No newline at end of file diff --git a/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.cpp b/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.cpp index e35bd7f..3ad328d 100644 --- a/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.cpp +++ b/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.cpp @@ -24,21 +24,16 @@ using namespace google::protobuf; - - -namespace VtxDiff::Protobuf -{ +namespace VtxDiff::Protobuf { // ============================================================================ // Persistent factory per descriptor pool // ============================================================================ - namespace - { + namespace { using FactoryPtr = std::unique_ptr; std::mutex GlobalFactoryMutex; std::unordered_map GlobalFactories; - DynamicMessageFactory* GetFactoryForPool(const DescriptorPool* Pool) - { + DynamicMessageFactory* GetFactoryForPool(const DescriptorPool* Pool) { std::lock_guard Lock(GlobalFactoryMutex); auto& Entry = GlobalFactories[Pool]; if (!Entry) @@ -46,8 +41,7 @@ namespace VtxDiff::Protobuf return Entry.get(); } - std::unique_ptr NewOwnedMessage(const DescriptorPool* Pool, const Descriptor* Desc) - { + std::unique_ptr NewOwnedMessage(const DescriptorPool* Pool, const Descriptor* Desc) { DynamicMessageFactory* Factory = GetFactoryForPool(Pool); const Message* Proto = Factory->GetPrototype(Desc); if (!Proto) @@ -55,8 +49,7 @@ namespace VtxDiff::Protobuf return std::unique_ptr(Proto->New()); } - std::unique_ptr CloneMessage(const Message& Src) - { + std::unique_ptr CloneMessage(const Message& Src) { const Descriptor* Desc = Src.GetDescriptor(); const DescriptorPool* Pool = Desc->file()->pool(); auto Copy = NewOwnedMessage(Pool, Desc); @@ -64,132 +57,154 @@ namespace VtxDiff::Protobuf Copy->CopyFrom(Src); return Copy; } - } - -// ============================================================================ -// Byte utilities -// ============================================================================ + } // namespace + + // ============================================================================ + // Byte utilities + // ============================================================================ -template -static void PutLE(std::vector& Dst, T Value) -{ - static_assert(std::is_trivially_copyable_v, "T must be trivially copyable"); - std::array buf{}; - std::memcpy(buf.data(), &Value, sizeof(T)); - #if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + template + static void PutLE(std::vector& Dst, T Value) { + static_assert(std::is_trivially_copyable_v, "T must be trivially copyable"); + std::array buf {}; + std::memcpy(buf.data(), &Value, sizeof(T)); +#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) std::reverse(buf.begin(), buf.end()); - #elif defined(_BIG_ENDIAN) && _BIG_ENDIAN +#elif defined(_BIG_ENDIAN) && _BIG_ENDIAN std::reverse(buf.begin(), buf.end()); - #endif - for (unsigned char b : buf) Dst.push_back(static_cast(b)); -} +#endif + for (unsigned char b : buf) + Dst.push_back(static_cast(b)); + } -static inline void PutF(std::vector& dst, float v) { PutLE(dst, v); } + static inline void PutF(std::vector& dst, float v) { + PutLE(dst, v); + } -// ============================================================================ -// Vector/Quat/Transform packing helpers -// ============================================================================ + // ============================================================================ + // Vector/Quat/Transform packing helpers + // ============================================================================ -static float SafeGetFloat(const Message& m, const Reflection* r, const FieldDescriptor* f) { - if (!f) return 0.0f; + static float SafeGetFloat(const Message& m, const Reflection* r, const FieldDescriptor* f) { + if (!f) + return 0.0f; - if (f->cpp_type() == FieldDescriptor::CPPTYPE_DOUBLE) { - return static_cast(r->GetDouble(m, f)); - } else if (f->cpp_type() == FieldDescriptor::CPPTYPE_FLOAT) { - return r->GetFloat(m, f); + if (f->cpp_type() == FieldDescriptor::CPPTYPE_DOUBLE) { + return static_cast(r->GetDouble(m, f)); + } else if (f->cpp_type() == FieldDescriptor::CPPTYPE_FLOAT) { + return r->GetFloat(m, f); + } + return 0.0f; } - return 0.0f; -} static bool PackVector(const Message& m, std::vector& out) { - auto* r = m.GetReflection(); - const Descriptor* d = m.GetDescriptor(); - const FieldDescriptor* fx = d->FindFieldByName("x"); if (!fx) fx = d->FindFieldByName("r"); - const FieldDescriptor* fy = d->FindFieldByName("y"); if (!fy) fy = d->FindFieldByName("g"); - const FieldDescriptor* fz = d->FindFieldByName("z"); if (!fz) fz = d->FindFieldByName("b"); - const FieldDescriptor* fw = d->FindFieldByName("w"); - - if (!fx || !fy || !fz) return false; - - PutF(out, SafeGetFloat(m, r, fx)); - PutF(out, SafeGetFloat(m, r, fy)); - PutF(out, SafeGetFloat(m, r, fz)); - if (fw) PutF(out, SafeGetFloat(m, r, fw)); - - return true; -} - -static bool PackQuat(const Message& m, std::vector& out) { - auto* d = m.GetDescriptor(); auto* r = m.GetReflection(); - const char* names[4] = { "x","y","z","w" }; - for (int i = 0; i < 4; ++i) { - auto* f = d->FindFieldByName(names[i]); - if (!f) return false; - PutF(out, SafeGetFloat(m, r, f)); + auto* r = m.GetReflection(); + const Descriptor* d = m.GetDescriptor(); + const FieldDescriptor* fx = d->FindFieldByName("x"); + if (!fx) + fx = d->FindFieldByName("r"); + const FieldDescriptor* fy = d->FindFieldByName("y"); + if (!fy) + fy = d->FindFieldByName("g"); + const FieldDescriptor* fz = d->FindFieldByName("z"); + if (!fz) + fz = d->FindFieldByName("b"); + const FieldDescriptor* fw = d->FindFieldByName("w"); + + if (!fx || !fy || !fz) + return false; + + PutF(out, SafeGetFloat(m, r, fx)); + PutF(out, SafeGetFloat(m, r, fy)); + PutF(out, SafeGetFloat(m, r, fz)); + if (fw) + PutF(out, SafeGetFloat(m, r, fw)); + + return true; } - return true; -} - -static bool PackFloatRange(const Message& m, std::vector& out) { - auto* r = m.GetReflection(); - const Descriptor* d = m.GetDescriptor(); - auto fmin = d->FindFieldByName("min"); - auto fmax = d->FindFieldByName("max"); - - PutF(out, SafeGetFloat(m, r, fmin)); - PutF(out, SafeGetFloat(m, r, fmax)); - return true; -} - -static bool PackTransform(const Message& m, std::vector& out) { - auto* d = m.GetDescriptor(); - auto* r = m.GetReflection(); - - auto packVec = [&](const FieldDescriptor* f)->bool { - if (!f || f->cpp_type() != FieldDescriptor::CPPTYPE_MESSAGE) return false; - const Message& sub = r->GetMessage(m, f); - return PackVector(sub, out); // x,y,z - }; - - auto* fT = d->FindFieldByName("translation"); - auto* fR = d->FindFieldByName("rotation"); - auto* fS = d->FindFieldByName("scale"); - if (!fT || !fR || !fS) return false; - - if (!packVec(fT)) return false; - - // quat (x,y,z,w) - { - const Message& q = r->GetMessage(m, fR); - if (!PackQuat(q, out)) return false; + + static bool PackQuat(const Message& m, std::vector& out) { + auto* d = m.GetDescriptor(); + auto* r = m.GetReflection(); + const char* names[4] = {"x", "y", "z", "w"}; + for (int i = 0; i < 4; ++i) { + auto* f = d->FindFieldByName(names[i]); + if (!f) + return false; + PutF(out, SafeGetFloat(m, r, f)); + } + return true; } - if (!packVec(fS)) return false; - return true; -} - -static inline bool IsKnownStructMsg(const Descriptor* md) { - if (!md) return false; - const std::string& n = md->name(); - return n == "Vector" || n == "Quat" || n == "Transform" || n == "FloatRange"; -} - -static bool PackKnownStruct(const Message& m, std::vector& out) { - const std::string& n = m.GetDescriptor()->name(); - if (n == "Vector") return PackVector(m, out); - if (n == "Quat") return PackQuat(m, out); - if (n == "Transform") return PackTransform(m, out); - if (n == "FloatRange") return PackFloatRange(m, out); - return false; -} - -// ============================================================ -// Constructor / Destructor -// ============================================================ - + static bool PackFloatRange(const Message& m, std::vector& out) { + auto* r = m.GetReflection(); + const Descriptor* d = m.GetDescriptor(); + auto fmin = d->FindFieldByName("min"); + auto fmax = d->FindFieldByName("max"); + + PutF(out, SafeGetFloat(m, r, fmin)); + PutF(out, SafeGetFloat(m, r, fmax)); + return true; + } + + static bool PackTransform(const Message& m, std::vector& out) { + auto* d = m.GetDescriptor(); + auto* r = m.GetReflection(); + + auto packVec = [&](const FieldDescriptor* f) -> bool { + if (!f || f->cpp_type() != FieldDescriptor::CPPTYPE_MESSAGE) + return false; + const Message& sub = r->GetMessage(m, f); + return PackVector(sub, out); // x,y,z + }; + + auto* fT = d->FindFieldByName("translation"); + auto* fR = d->FindFieldByName("rotation"); + auto* fS = d->FindFieldByName("scale"); + if (!fT || !fR || !fS) + return false; + + if (!packVec(fT)) + return false; + + // quat (x,y,z,w) + { + const Message& q = r->GetMessage(m, fR); + if (!PackQuat(q, out)) + return false; + } + + if (!packVec(fS)) + return false; + return true; + } + + static inline bool IsKnownStructMsg(const Descriptor* md) { + if (!md) + return false; + const std::string& n = md->name(); + return n == "Vector" || n == "Quat" || n == "Transform" || n == "FloatRange"; + } + + static bool PackKnownStruct(const Message& m, std::vector& out) { + const std::string& n = m.GetDescriptor()->name(); + if (n == "Vector") + return PackVector(m, out); + if (n == "Quat") + return PackQuat(m, out); + if (n == "Transform") + return PackTransform(m, out); + if (n == "FloatRange") + return PackFloatRange(m, out); + return false; + } + + // ============================================================ + // Constructor / Destructor + // ============================================================ + FProtobufViewAdapter::FProtobufViewAdapter(const google::protobuf::Message* InMsg, bool bOwnsMessage) - : External(InMsg) - { + : External(InMsg) { if (!bOwnsMessage) {} } @@ -199,205 +214,206 @@ static bool PackKnownStruct(const Message& m, std::vector& out) { FProtobufViewAdapter& FProtobufViewAdapter::operator=(FProtobufViewAdapter&&) noexcept = default; -// ============================================================ -// CloneMessage -// ------------------------------------------------------------ -// Creates a deep copy of a protobuf message using the same -// DescriptorPool and DynamicMessageFactory. -// This is used to produce isolated views for diffing without -// modifying the original protobuf instance. -// ============================================================ - -std::unique_ptr FProtobufViewAdapter::CloneMessage(const Message& Src) -{ - const google::protobuf::Descriptor* Desc = Src.GetDescriptor(); - const google::protobuf::DescriptorPool* Pool = Desc ? Desc->file()->pool() : google::protobuf::DescriptorPool::generated_pool(); - - // Always use the same pool as the source - auto* Factory = GetFactoryForPool(Pool); - const google::protobuf::Message* Proto = Factory->GetPrototype(Desc); - if (!Proto) return nullptr; - - auto Copy = std::unique_ptr(Proto->New()); - Copy->CopyFrom(Src); - return Copy; -} - -void FProtobufViewAdapter::Reset() -{ - ResetArena(); -} - -// ============================================================ -// CreateRoot -// ------------------------------------------------------------ -// Creates a ProtobufViewAdapter from a serialized buffer and -// the provided root message type name. -// Used when reading frames directly from binary memory. -// ============================================================ -std::optional FProtobufViewAdapter::CreateRoot(const DescriptorPool* Pool, const std::string& RootType, const uint8_t* Buffer, size_t Size) -{ - if (!Pool || !Buffer || Size == 0) return std::nullopt; - const google::protobuf::Descriptor* Desc = Pool->FindMessageTypeByName(RootType); - if (!Desc) return std::nullopt; - auto OwnedMsg = NewOwnedMessage(Pool, Desc); - if (!OwnedMsg) return std::nullopt; - if (!OwnedMsg->ParseFromArray(Buffer, static_cast(Size))) return std::nullopt; - - FProtobufViewAdapter Adapter(OwnedMsg.get(), true); - Adapter.Owned = std::move(OwnedMsg); - return Adapter; -} - -// ============================================================ -// FromMessage -// ------------------------------------------------------------ -// Creates a ProtobufViewAdapter from an existing in-memory -// protobuf message by cloning it internally. -// This avoids modifying the original instance during diffing. -// ============================================================ - -std::optional FProtobufViewAdapter::FromMessage(const Message& M) -{ - auto OwnedMsg = CloneMessage(M); - if (!OwnedMsg) return std::nullopt; - - FProtobufViewAdapter Adapter(OwnedMsg.get(), true); - Adapter.Owned = std::move(OwnedMsg); - return Adapter; -} - -void FProtobufViewAdapter::SetSubArrayNames(std::unordered_map Map) -{ - SubNames = std::move(Map); -} -// ============================================================ -// FindField -// ------------------------------------------------------------ -// Locates a field descriptor by its name within the current -// message descriptor. Returns nullptr if not found. -// ============================================================ -const google::protobuf::FieldDescriptor* FProtobufViewAdapter::FindField(std::string_view Name) const -{ - const auto* D = GetDesc(); - if (!D) - return nullptr; - - return D->FindFieldByName(std::string(Name)); -} - - -// ============================================================ -// GetScalarFieldString -// ------------------------------------------------------------ -// Retrieves the string value of a scalar (non-repeated) field -// from the current protobuf message. Returns an empty string -// if the field does not exist or is not a string type. -// ============================================================ - -std::string FProtobufViewAdapter::GetScalarFieldString(const std::string& FieldName) const -{ - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); - - if (!M || !D || !R) - return {}; + // ============================================================ + // CloneMessage + // ------------------------------------------------------------ + // Creates a deep copy of a protobuf message using the same + // DescriptorPool and DynamicMessageFactory. + // This is used to produce isolated views for diffing without + // modifying the original protobuf instance. + // ============================================================ - const auto* Field = D->FindFieldByName(FieldName); - - if (!Field || Field->is_repeated()) - return {}; + std::unique_ptr FProtobufViewAdapter::CloneMessage(const Message& Src) { + const google::protobuf::Descriptor* Desc = Src.GetDescriptor(); + const google::protobuf::DescriptorPool* Pool = + Desc ? Desc->file()->pool() : google::protobuf::DescriptorPool::generated_pool(); - switch (Field->cpp_type()) - { - case google::protobuf::FieldDescriptor::CPPTYPE_STRING: - return R->GetString(*M, Field); + // Always use the same pool as the source + auto* Factory = GetFactoryForPool(Pool); + const google::protobuf::Message* Proto = Factory->GetPrototype(Desc); + if (!Proto) + return nullptr; - case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: - return std::to_string(R->GetUInt64(*M, Field)); + auto Copy = std::unique_ptr(Proto->New()); + Copy->CopyFrom(Src); + return Copy; + } - case google::protobuf::FieldDescriptor::CPPTYPE_INT64: - return std::to_string(R->GetInt64(*M, Field)); + void FProtobufViewAdapter::Reset() { + ResetArena(); + } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: - return std::to_string(R->GetUInt32(*M, Field)); + // ============================================================ + // CreateRoot + // ------------------------------------------------------------ + // Creates a ProtobufViewAdapter from a serialized buffer and + // the provided root message type name. + // Used when reading frames directly from binary memory. + // ============================================================ + std::optional FProtobufViewAdapter::CreateRoot(const DescriptorPool* Pool, + const std::string& RootType, + const uint8_t* Buffer, size_t Size) { + if (!Pool || !Buffer || Size == 0) + return std::nullopt; + const google::protobuf::Descriptor* Desc = Pool->FindMessageTypeByName(RootType); + if (!Desc) + return std::nullopt; + auto OwnedMsg = NewOwnedMessage(Pool, Desc); + if (!OwnedMsg) + return std::nullopt; + if (!OwnedMsg->ParseFromArray(Buffer, static_cast(Size))) + return std::nullopt; + + FProtobufViewAdapter Adapter(OwnedMsg.get(), true); + Adapter.Owned = std::move(OwnedMsg); + return Adapter; + } - case google::protobuf::FieldDescriptor::CPPTYPE_INT32: - return std::to_string(R->GetInt32(*M, Field)); + // ============================================================ + // FromMessage + // ------------------------------------------------------------ + // Creates a ProtobufViewAdapter from an existing in-memory + // protobuf message by cloning it internally. + // This avoids modifying the original instance during diffing. + // ============================================================ - case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: - return std::to_string(R->GetFloat(*M, Field)); + std::optional FProtobufViewAdapter::FromMessage(const Message& M) { + auto OwnedMsg = CloneMessage(M); + if (!OwnedMsg) + return std::nullopt; - case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: - return std::to_string(R->GetDouble(*M, Field)); + FProtobufViewAdapter Adapter(OwnedMsg.get(), true); + Adapter.Owned = std::move(OwnedMsg); + return Adapter; + } - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: - return R->GetBool(*M, Field) ? "true" : "false"; + void FProtobufViewAdapter::SetSubArrayNames(std::unordered_map Map) { + SubNames = std::move(Map); + } + // ============================================================ + // FindField + // ------------------------------------------------------------ + // Locates a field descriptor by its name within the current + // message descriptor. Returns nullptr if not found. + // ============================================================ + const google::protobuf::FieldDescriptor* FProtobufViewAdapter::FindField(std::string_view Name) const { + const auto* D = GetDesc(); + if (!D) + return nullptr; - case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: - return std::to_string(R->GetEnumValue(*M, Field)); + return D->FindFieldByName(std::string(Name)); + } - default: - return {}; + + // ============================================================ + // GetScalarFieldString + // ------------------------------------------------------------ + // Retrieves the string value of a scalar (non-repeated) field + // from the current protobuf message. Returns an empty string + // if the field does not exist or is not a string type. + // ============================================================ + + std::string FProtobufViewAdapter::GetScalarFieldString(const std::string& FieldName) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); + + if (!M || !D || !R) + return {}; + + const auto* Field = D->FindFieldByName(FieldName); + + if (!Field || Field->is_repeated()) + return {}; + + switch (Field->cpp_type()) { + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: + return R->GetString(*M, Field); + + case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: + return std::to_string(R->GetUInt64(*M, Field)); + + case google::protobuf::FieldDescriptor::CPPTYPE_INT64: + return std::to_string(R->GetInt64(*M, Field)); + + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: + return std::to_string(R->GetUInt32(*M, Field)); + + case google::protobuf::FieldDescriptor::CPPTYPE_INT32: + return std::to_string(R->GetInt32(*M, Field)); + + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: + return std::to_string(R->GetFloat(*M, Field)); + + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: + return std::to_string(R->GetDouble(*M, Field)); + + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: + return R->GetBool(*M, Field) ? "true" : "false"; + + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: + return std::to_string(R->GetEnumValue(*M, Field)); + + default: + return {}; + } } -} -uint64_t FProtobufViewAdapter::GetUint64Field(const std::string& FieldName) const -{ - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); + uint64_t FProtobufViewAdapter::GetUint64Field(const std::string& FieldName) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); + + if (!M || !D || !R) + return 0; + + const auto* Field = D->FindFieldByName(FieldName); - if (!M || !D || !R) + if (!Field || Field->is_repeated()) + return {}; + + if (Field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_UINT64) { + return R->GetUInt64(*M, Field); + } return 0; + } - const auto* Field = D->FindFieldByName(FieldName); - - if (!Field || Field->is_repeated()) - return {}; - - if ( Field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_UINT64) - { - return R->GetUInt64(*M, Field); + // ============================================================ + // GetFieldByName + // ------------------------------------------------------------ + // Returns a nested node view (sub-structure) for the given field + // name, if that field represents a singular message type. + // ============================================================ + FProtobufViewAdapter FProtobufViewAdapter::GetFieldByName(const std::string& FieldName) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); + if (!M || !D || !R) + return {}; + const auto* Field = FindField(FieldName); + if (!Field || Field->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE || Field->is_repeated()) + return {}; + if (!R->HasField(*M, Field)) + return {}; + + const google::protobuf::Message& Nested = R->GetMessage(*M, Field); + return FProtobufViewAdapter(&Nested, false); } - return 0; -} - -// ============================================================ -// GetFieldByName -// ------------------------------------------------------------ -// Returns a nested node view (sub-structure) for the given field -// name, if that field represents a singular message type. -// ============================================================ -FProtobufViewAdapter FProtobufViewAdapter::GetFieldByName(const std::string& FieldName) const -{ - const auto* M = GetMsg(); const auto* D = GetDesc(); const auto* R = GetRefl(); - if (!M || !D || !R) return {}; - const auto* Field = FindField(FieldName); - if (!Field || Field->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE || Field->is_repeated()) return {}; - if (!R->HasField(*M, Field)) return {}; - - const google::protobuf::Message& Nested = R->GetMessage(*M, Field); - return FProtobufViewAdapter(&Nested, false); -} - - -// ============================================================ -// IsScalar -// ------------------------------------------------------------ -// Returns true if a protobuf field is a scalar POD type -// (int, float, double, bool, enum, etc). -// ============================================================ - -bool FProtobufViewAdapter::IsScalar(const FieldDescriptor* Field) const -{ - if (!Field) - return false; - using FD = google::protobuf::FieldDescriptor; - switch (Field->cpp_type()) - { + + // ============================================================ + // IsScalar + // ------------------------------------------------------------ + // Returns true if a protobuf field is a scalar POD type + // (int, float, double, bool, enum, etc). + // ============================================================ + + bool FProtobufViewAdapter::IsScalar(const FieldDescriptor* Field) const { + if (!Field) + return false; + + using FD = google::protobuf::FieldDescriptor; + switch (Field->cpp_type()) { case FD::CPPTYPE_INT32: case FD::CPPTYPE_UINT32: case FD::CPPTYPE_INT64: @@ -409,572 +425,565 @@ bool FProtobufViewAdapter::IsScalar(const FieldDescriptor* Field) const return true; default: return false; + } } -} - -// ============================================================ -// InferContainerType -// ------------------------------------------------------------ -// Determines the EVTXContainerType that corresponds to a given -// protobuf field. This mapping follows the naming and layout -// rules used in FVTXPropertyContainerArrays. -// ============================================================ - -EVTXContainerType FProtobufViewAdapter::InferContainerType(const FieldDescriptor* Field) const -{ - if (!Field) return EVTXContainerType::Unknown; - - const std::string_view N = Field->name(); - - using Pair = std::pair; - static constexpr std::array kByName = { { - {"transform_properties", EVTXContainerType::TransformProperties}, - {"int32_properties", EVTXContainerType::Int32Properties}, - {"int64_properties", EVTXContainerType::Int64Properties}, - {"float_properties", EVTXContainerType::FloatProperties}, - {"double_properties", EVTXContainerType::DoubleProperties}, - {"vector_properties", EVTXContainerType::VectorProperties}, - {"quat_properties", EVTXContainerType::QuatProperties}, - {"range_properties", EVTXContainerType::RangeProperties}, - {"bool_properties", EVTXContainerType::BoolProperties}, - {"string_properties", EVTXContainerType::StringProperties}, - - {"byte_array_properties",EVTXContainerType::ByteArrayProperties}, - {"int32_arrays", EVTXContainerType::Int32Arrays}, - {"int64_arrays", EVTXContainerType::Int64Arrays}, - {"float_arrays", EVTXContainerType::FloatArrays}, - {"double_arrays", EVTXContainerType::DoubleArrays}, - {"vector_arrays", EVTXContainerType::VectorArrays}, - {"quat_arrays", EVTXContainerType::QuatArrays}, - {"transform_arrays", EVTXContainerType::TransformArrays}, - {"range_arrays", EVTXContainerType::RangeArrays}, - {"bool_arrays", EVTXContainerType::BoolArrays}, - {"string_arrays", EVTXContainerType::StringArrays}, - - {"any_struct_properties", EVTXContainerType::AnyStructProperties}, - {"any_struct_arrays", EVTXContainerType::AnyStructArrays}, - {"map_properties", EVTXContainerType::MapProperties}, - {"map_arrays", EVTXContainerType::MapArrays} - } }; - - for (const auto& [K, V] : kByName) - if (K == N) return V; - - return EVTXContainerType::Unknown; -} - -std::span FProtobufViewAdapter::Publish(const void* Data, size_t Size) const -{ - if (!Data || Size == 0) - return {}; - const auto* Bytes = static_cast(Data); - Arena.emplace_back(Bytes, Bytes + Size); + // ============================================================ + // InferContainerType + // ------------------------------------------------------------ + // Determines the EVTXContainerType that corresponds to a given + // protobuf field. This mapping follows the naming and layout + // rules used in FVTXPropertyContainerArrays. + // ============================================================ + + EVTXContainerType FProtobufViewAdapter::InferContainerType(const FieldDescriptor* Field) const { + if (!Field) + return EVTXContainerType::Unknown; + + const std::string_view N = Field->name(); + + using Pair = std::pair; + static constexpr std::array kByName = { + {{"transform_properties", EVTXContainerType::TransformProperties}, + {"int32_properties", EVTXContainerType::Int32Properties}, + {"int64_properties", EVTXContainerType::Int64Properties}, + {"float_properties", EVTXContainerType::FloatProperties}, + {"double_properties", EVTXContainerType::DoubleProperties}, + {"vector_properties", EVTXContainerType::VectorProperties}, + {"quat_properties", EVTXContainerType::QuatProperties}, + {"range_properties", EVTXContainerType::RangeProperties}, + {"bool_properties", EVTXContainerType::BoolProperties}, + {"string_properties", EVTXContainerType::StringProperties}, + + {"byte_array_properties", EVTXContainerType::ByteArrayProperties}, + {"int32_arrays", EVTXContainerType::Int32Arrays}, + {"int64_arrays", EVTXContainerType::Int64Arrays}, + {"float_arrays", EVTXContainerType::FloatArrays}, + {"double_arrays", EVTXContainerType::DoubleArrays}, + {"vector_arrays", EVTXContainerType::VectorArrays}, + {"quat_arrays", EVTXContainerType::QuatArrays}, + {"transform_arrays", EVTXContainerType::TransformArrays}, + {"range_arrays", EVTXContainerType::RangeArrays}, + {"bool_arrays", EVTXContainerType::BoolArrays}, + {"string_arrays", EVTXContainerType::StringArrays}, + + {"any_struct_properties", EVTXContainerType::AnyStructProperties}, + {"any_struct_arrays", EVTXContainerType::AnyStructArrays}, + {"map_properties", EVTXContainerType::MapProperties}, + {"map_arrays", EVTXContainerType::MapArrays}}}; + + for (const auto& [K, V] : kByName) + if (K == N) + return V; + + return EVTXContainerType::Unknown; + } - const auto& Last = Arena.back(); - return { Last.data(), Last.size() }; -} + std::span FProtobufViewAdapter::Publish(const void* Data, size_t Size) const { + if (!Data || Size == 0) + return {}; -std::span FProtobufViewAdapter::Publish(const std::vector& Buffer) const -{ - if (Buffer.empty()) - return {}; + const auto* Bytes = static_cast(Data); + Arena.emplace_back(Bytes, Bytes + Size); - Arena.emplace_back(Buffer.begin(), Buffer.end()); - const auto& Last = Arena.back(); - return { Last.data(), Last.size() }; -} - -std::span FProtobufViewAdapter::Publish(std::vector&& Buffer) const -{ - Arena.push_back(std::move(Buffer)); - const auto& Last = Arena.back(); - return { Last.data(), Last.size() }; -} - -void FProtobufViewAdapter::ResetArena() const -{ - Arena.clear(); - Scratch.clear(); - TempString.clear(); -} - -const google::protobuf::Descriptor* FProtobufViewAdapter::ActiveDesc() const -{ - return GetDesc(); -} + const auto& Last = Arena.back(); + return {Last.data(), Last.size()}; + } - // ============================================================ -// EnumerateFields -// ------------------------------------------------------------ -// Returns a list of all protobuf fields in the current message -// that contain meaningful data. This function dynamically queries -// the descriptor and reflection interface, without relying on any -// cached pointers. -// ============================================================ -std::span FProtobufViewAdapter::EnumerateFields() const -{ - if (bFieldsCached) return CachedFields; - - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); - - if (!M || !D || !R) return {}; - - CachedFields.reserve(D->field_count()); - - auto hasData = [&](const google::protobuf::FieldDescriptor* f) -> bool { - if (!f) return false; - if (f->is_repeated()) return R->FieldSize(*M, f) > 0; - - using FD = google::protobuf::FieldDescriptor; - switch (f->cpp_type()) { - case FD::CPPTYPE_MESSAGE: return R->HasField(*M, f); - case FD::CPPTYPE_STRING: return !R->GetString(*M, f).empty(); - case FD::CPPTYPE_BOOL: return R->GetBool(*M, f); - case FD::CPPTYPE_INT32: return R->GetInt32(*M, f) != 0; - case FD::CPPTYPE_UINT32: return R->GetUInt32(*M, f) != 0; - case FD::CPPTYPE_INT64: return R->GetInt64(*M, f) != 0; - case FD::CPPTYPE_UINT64: return R->GetUInt64(*M, f) != 0; - case FD::CPPTYPE_FLOAT: return R->GetFloat(*M, f) != 0.0f; - case FD::CPPTYPE_DOUBLE: return R->GetDouble(*M, f) != 0.0; - case FD::CPPTYPE_ENUM: return R->GetEnumValue(*M, f) != 0; - default: return false; - } - }; + std::span FProtobufViewAdapter::Publish(const std::vector& Buffer) const { + if (Buffer.empty()) + return {}; - for (int i = 0; i < D->field_count(); ++i) { - const auto* f = D->field(i); - if (!f || !hasData(f)) continue; + Arena.emplace_back(Buffer.begin(), Buffer.end()); + const auto& Last = Arena.back(); + return {Last.data(), Last.size()}; + } - VtxDiff::FieldDesc d; - d.name = f->name(); - d.is_actors_field = (d.name == "entities"); - d.type = InferContainerType(f); - d.is_array_like = f->is_repeated(); - d.is_map_like = f->is_map(); + std::span FProtobufViewAdapter::Publish(std::vector&& Buffer) const { + Arena.push_back(std::move(Buffer)); + const auto& Last = Arena.back(); + return {Last.data(), Last.size()}; + } - CachedFields.push_back(std::move(d)); + void FProtobufViewAdapter::ResetArena() const { + Arena.clear(); + Scratch.clear(); + TempString.clear(); } - bFieldsCached = true; - return CachedFields; -} - -// ============================================================ -// GetFieldBytes -// ------------------------------------------------------------ -// Retrieves the binary data for a specific field in the message. -// Handles scalar, repeated, and nested message types. -// ============================================================ -std::span FProtobufViewAdapter::GetFieldBytes(const VtxDiff::FieldDesc& Field) const -{ - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); - - if (!M || !D || !R) - return {}; + const google::protobuf::Descriptor* FProtobufViewAdapter::ActiveDesc() const { + return GetDesc(); + } - const auto* F = D->FindFieldByName(Field.name); - if (!F) - return {}; + // ============================================================ + // EnumerateFields + // ------------------------------------------------------------ + // Returns a list of all protobuf fields in the current message + // that contain meaningful data. This function dynamically queries + // the descriptor and reflection interface, without relying on any + // cached pointers. + // ============================================================ + std::span FProtobufViewAdapter::EnumerateFields() const { + if (bFieldsCached) + return CachedFields; + + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); - Scratch.clear(); + if (!M || !D || !R) + return {}; + + CachedFields.reserve(D->field_count()); + + auto hasData = [&](const google::protobuf::FieldDescriptor* f) -> bool { + if (!f) + return false; + if (f->is_repeated()) + return R->FieldSize(*M, f) > 0; + + using FD = google::protobuf::FieldDescriptor; + switch (f->cpp_type()) { + case FD::CPPTYPE_MESSAGE: + return R->HasField(*M, f); + case FD::CPPTYPE_STRING: + return !R->GetString(*M, f).empty(); + case FD::CPPTYPE_BOOL: + return R->GetBool(*M, f); + case FD::CPPTYPE_INT32: + return R->GetInt32(*M, f) != 0; + case FD::CPPTYPE_UINT32: + return R->GetUInt32(*M, f) != 0; + case FD::CPPTYPE_INT64: + return R->GetInt64(*M, f) != 0; + case FD::CPPTYPE_UINT64: + return R->GetUInt64(*M, f) != 0; + case FD::CPPTYPE_FLOAT: + return R->GetFloat(*M, f) != 0.0f; + case FD::CPPTYPE_DOUBLE: + return R->GetDouble(*M, f) != 0.0; + case FD::CPPTYPE_ENUM: + return R->GetEnumValue(*M, f) != 0; + default: + return false; + } + }; + + for (int i = 0; i < D->field_count(); ++i) { + const auto* f = D->field(i); + if (!f || !hasData(f)) + continue; + + VtxDiff::FieldDesc d; + d.name = f->name(); + d.is_actors_field = (d.name == "entities"); + d.type = InferContainerType(f); + d.is_array_like = f->is_repeated(); + d.is_map_like = f->is_map(); + + CachedFields.push_back(std::move(d)); + } + + bFieldsCached = true; + return CachedFields; + } // ============================================================ - // Handle scalar (non-repeated, non-message) types + // GetFieldBytes + // ------------------------------------------------------------ + // Retrieves the binary data for a specific field in the message. + // Handles scalar, repeated, and nested message types. // ============================================================ - if (!F->is_repeated() && F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) - { - switch (F->cpp_type()) - { + std::span FProtobufViewAdapter::GetFieldBytes(const VtxDiff::FieldDesc& Field) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); + + if (!M || !D || !R) + return {}; + + const auto* F = D->FindFieldByName(Field.name); + if (!F) + return {}; + + Scratch.clear(); + + // ============================================================ + // Handle scalar (non-repeated, non-message) types + // ============================================================ + if (!F->is_repeated() && F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { + switch (F->cpp_type()) { case google::protobuf::FieldDescriptor::CPPTYPE_INT32: - PutLE(Scratch, R->GetInt32(*M, F)); break; + PutLE(Scratch, R->GetInt32(*M, F)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: - PutLE(Scratch, R->GetUInt32(*M, F)); break; + PutLE(Scratch, R->GetUInt32(*M, F)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_INT64: - PutLE(Scratch, R->GetInt64(*M, F)); break; + PutLE(Scratch, R->GetInt64(*M, F)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: - PutLE(Scratch, R->GetUInt64(*M, F)); break; + PutLE(Scratch, R->GetUInt64(*M, F)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: - PutLE(Scratch, R->GetFloat(*M, F)); break; + PutLE(Scratch, R->GetFloat(*M, F)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: - PutLE(Scratch, R->GetDouble(*M, F)); break; - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: - { + PutLE(Scratch, R->GetDouble(*M, F)); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { uint8_t val = R->GetBool(*M, F) ? 1 : 0; Scratch.push_back(static_cast(val)); break; } - case google::protobuf::FieldDescriptor::CPPTYPE_STRING: - { + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { std::string s = R->GetString(*M, F); return Publish(s.data(), s.size()); } default: break; - } + } - return Publish(Scratch); - } + return Publish(Scratch); + } - // ============================================================ - // Handle singular nested messages (e.g., Vector, Quat, Transform) - // ============================================================ - if (!F->is_repeated() && F->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) - { - const google::protobuf::Message& Nested = R->GetMessage(*M, F); + // ============================================================ + // Handle singular nested messages (e.g., Vector, Quat, Transform) + // ============================================================ + if (!F->is_repeated() && F->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { + const google::protobuf::Message& Nested = R->GetMessage(*M, F); - // Try to pack known math-style structs efficiently - if (PackKnownStruct(Nested, Scratch)) - return Publish(Scratch); + // Try to pack known math-style structs efficiently + if (PackKnownStruct(Nested, Scratch)) + return Publish(Scratch); - // Fallback: serialize full message - std::vector Data(static_cast(Nested.ByteSizeLong())); - Nested.SerializePartialToArray(Data.data(), static_cast(Data.size())); - return Publish(std::move(Data)); - } + // Fallback: serialize full message + std::vector Data(static_cast(Nested.ByteSizeLong())); + Nested.SerializePartialToArray(Data.data(), static_cast(Data.size())); + return Publish(std::move(Data)); + } - // ============================================================ - // Handle repeated scalar fields - // ============================================================ - if (F->is_repeated() && F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) - { - const int Count = R->FieldSize(*M, F); - Scratch.reserve(Count * 8); // estimated element size + // ============================================================ + // Handle repeated scalar fields + // ============================================================ + if (F->is_repeated() && F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { + const int Count = R->FieldSize(*M, F); + Scratch.reserve(Count * 8); // estimated element size - for (int i = 0; i < Count; ++i) - { - switch (F->cpp_type()) - { + for (int i = 0; i < Count; ++i) { + switch (F->cpp_type()) { case google::protobuf::FieldDescriptor::CPPTYPE_INT32: - PutLE(Scratch, R->GetRepeatedInt32(*M, F, i)); break; + PutLE(Scratch, R->GetRepeatedInt32(*M, F, i)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: - PutLE(Scratch, R->GetRepeatedUInt32(*M, F, i)); break; + PutLE(Scratch, R->GetRepeatedUInt32(*M, F, i)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_INT64: - PutLE(Scratch, R->GetRepeatedInt64(*M, F, i)); break; + PutLE(Scratch, R->GetRepeatedInt64(*M, F, i)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: - PutLE(Scratch, R->GetRepeatedUInt64(*M, F, i)); break; + PutLE(Scratch, R->GetRepeatedUInt64(*M, F, i)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: - PutLE(Scratch, R->GetRepeatedFloat(*M, F, i)); break; + PutLE(Scratch, R->GetRepeatedFloat(*M, F, i)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: - PutLE(Scratch, R->GetRepeatedDouble(*M, F, i)); break; - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: - { + PutLE(Scratch, R->GetRepeatedDouble(*M, F, i)); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { uint8_t val = R->GetRepeatedBool(*M, F, i) ? 1 : 0; Scratch.push_back(static_cast(val)); break; } default: break; + } } + + return Publish(Scratch); } - return Publish(Scratch); + return {}; } - return {}; -} - -// ============================================================ -// GetArraySize -// ------------------------------------------------------------ -// Returns the number of elements in a repeated field or 1 for -// scalar fields. Returns 0 if the field is invalid or unset. -// ============================================================ -size_t FProtobufViewAdapter::GetArraySize(const VtxDiff::FieldDesc& Fd) const -{ - const auto* M = GetMsg(); - const auto* R = GetRefl(); + // ============================================================ + // GetArraySize + // ------------------------------------------------------------ + // Returns the number of elements in a repeated field or 1 for + // scalar fields. Returns 0 if the field is invalid or unset. + // ============================================================ + size_t FProtobufViewAdapter::GetArraySize(const VtxDiff::FieldDesc& Fd) const { + const auto* M = GetMsg(); + const auto* R = GetRefl(); - if (!M || !R) - { - VTX_WARN("[GetArraySize] Invalid message or reflection for field {}", Fd.name); - return 0; - } + if (!M || !R) { + VTX_WARN("[GetArraySize] Invalid message or reflection for field {}", Fd.name); + return 0; + } - const auto* F = FindField(Fd.name); - if (!F) - { - VTX_ERROR("[GetArraySize] Field not found in message: {}", Fd.name); + const auto* F = FindField(Fd.name); + if (!F) { + VTX_ERROR("[GetArraySize] Field not found in message: {}", Fd.name); - return 0; - } + return 0; + } - const int Size = F->is_repeated() ? R->FieldSize(*M, F) : 1; + const int Size = F->is_repeated() ? R->FieldSize(*M, F) : 1; #if defined(UE_LOG) - UE_LOG(LogTemp, Verbose, TEXT("[GetArraySize] Field %s size = %d (is_repeated=%d)"), - *FString(Fd.Name.c_str()), Size, static_cast(F->is_repeated())); + UE_LOG(LogTemp, Verbose, TEXT("[GetArraySize] Field %s size = %d (is_repeated=%d)"), *FString(Fd.Name.c_str()), + Size, static_cast(F->is_repeated())); #endif - return static_cast(Size); -} - -// ============================================================ -// GetArrayElementBytes -// ------------------------------------------------------------ -// Returns a binary span for a specific element of a repeated field. -// Handles scalar and message element types. -// ============================================================ -std::span FProtobufViewAdapter::GetArrayElementBytes( - const VtxDiff::FieldDesc& Field, size_t Index) const -{ - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); - - if (!M || !D || !R) - return {}; + return static_cast(Size); + } - const auto* F = D->FindFieldByName(Field.name); - if (!F || !F->is_repeated()) - return {}; + // ============================================================ + // GetArrayElementBytes + // ------------------------------------------------------------ + // Returns a binary span for a specific element of a repeated field. + // Handles scalar and message element types. + // ============================================================ + std::span FProtobufViewAdapter::GetArrayElementBytes(const VtxDiff::FieldDesc& Field, + size_t Index) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); - const int Count = R->FieldSize(*M, F); - if (Index >= static_cast(Count)) - return {}; + if (!M || !D || !R) + return {}; - Scratch.clear(); + const auto* F = D->FindFieldByName(Field.name); + if (!F || !F->is_repeated()) + return {}; - // Handle repeated message elements (Vector, Quat, Transform, etc.) - if (F->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) - { - const google::protobuf::Message& Nested = - R->GetRepeatedMessage(*M, F, static_cast(Index)); + const int Count = R->FieldSize(*M, F); + if (Index >= static_cast(Count)) + return {}; - // Attempt to pack known math/struct types directly - if (PackKnownStruct(Nested, Scratch)) - return Publish(Scratch); + Scratch.clear(); - // Fallback: serialize the entire sub-message - std::vector Data(static_cast(Nested.ByteSizeLong())); - Nested.SerializePartialToArray(Data.data(), static_cast(Data.size())); - return Publish(std::move(Data)); - } + // Handle repeated message elements (Vector, Quat, Transform, etc.) + if (F->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { + const google::protobuf::Message& Nested = R->GetRepeatedMessage(*M, F, static_cast(Index)); + + // Attempt to pack known math/struct types directly + if (PackKnownStruct(Nested, Scratch)) + return Publish(Scratch); - // Handle repeated scalar types - switch (F->cpp_type()) - { + // Fallback: serialize the entire sub-message + std::vector Data(static_cast(Nested.ByteSizeLong())); + Nested.SerializePartialToArray(Data.data(), static_cast(Data.size())); + return Publish(std::move(Data)); + } + + // Handle repeated scalar types + switch (F->cpp_type()) { case google::protobuf::FieldDescriptor::CPPTYPE_INT32: - PutLE(Scratch, R->GetRepeatedInt32(*M, F, static_cast(Index))); break; + PutLE(Scratch, R->GetRepeatedInt32(*M, F, static_cast(Index))); + break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: - PutLE(Scratch, R->GetRepeatedUInt32(*M, F, static_cast(Index))); break; + PutLE(Scratch, R->GetRepeatedUInt32(*M, F, static_cast(Index))); + break; case google::protobuf::FieldDescriptor::CPPTYPE_INT64: - PutLE(Scratch, R->GetRepeatedInt64(*M, F, static_cast(Index))); break; + PutLE(Scratch, R->GetRepeatedInt64(*M, F, static_cast(Index))); + break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: - PutLE(Scratch, R->GetRepeatedUInt64(*M, F, static_cast(Index))); break; + PutLE(Scratch, R->GetRepeatedUInt64(*M, F, static_cast(Index))); + break; case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: - PutLE(Scratch, R->GetRepeatedFloat(*M, F, static_cast(Index))); break; + PutLE(Scratch, R->GetRepeatedFloat(*M, F, static_cast(Index))); + break; case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: - PutLE(Scratch, R->GetRepeatedDouble(*M, F, static_cast(Index))); break; - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: - { + PutLE(Scratch, R->GetRepeatedDouble(*M, F, static_cast(Index))); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { uint8_t val = R->GetRepeatedBool(*M, F, static_cast(Index)) ? 1 : 0; Scratch.push_back(static_cast(val)); break; } - case google::protobuf::FieldDescriptor::CPPTYPE_STRING: - { + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { std::string Str = R->GetRepeatedString(*M, F, static_cast(Index)); return Publish(Str.data(), Str.size()); } default: break; - } + } - return Publish(Scratch); -} + return Publish(Scratch); + } -// ============================================================ -// GetSubArrayBytes -// ------------------------------------------------------------ -// Returns a binary span representing the serialized data of a -// sub-array (nested repeated message fields, e.g. TArray>). -// ============================================================ -std::span FProtobufViewAdapter::GetSubArrayBytes( - const VtxDiff::FieldDesc& Field, size_t SubIndex) const -{ - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); + // ============================================================ + // GetSubArrayBytes + // ------------------------------------------------------------ + // Returns a binary span representing the serialized data of a + // sub-array (nested repeated message fields, e.g. TArray>). + // ============================================================ + std::span FProtobufViewAdapter::GetSubArrayBytes(const VtxDiff::FieldDesc& Field, + size_t SubIndex) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); - if (!M || !D || !R) - return {}; + if (!M || !D || !R) + return {}; #if defined(UE_LOG) - UE_LOG(LogTemp, Verbose, TEXT("[GetSubArrayBytes] Msg type = %hs | Field = %hs"), - UTF8_TO_TCHAR(M->GetDescriptor()->full_name().c_str()), - UTF8_TO_TCHAR(Field.Name.c_str())); + UE_LOG(LogTemp, Verbose, TEXT("[GetSubArrayBytes] Msg type = %hs | Field = %hs"), + UTF8_TO_TCHAR(M->GetDescriptor()->full_name().c_str()), UTF8_TO_TCHAR(Field.Name.c_str())); #endif - const auto* F = D->FindFieldByName(Field.name); - if (!F || !F->is_repeated() || - F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) - return {}; - - const int OuterCount = R->FieldSize(*M, F); - if (SubIndex >= static_cast(OuterCount)) - return {}; - - Scratch.clear(); + const auto* F = D->FindFieldByName(Field.name); + if (!F || !F->is_repeated() || F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) + return {}; - // Retrieve the message representing one "subarray" - const google::protobuf::Message& SubArrayMsg = - R->GetRepeatedMessage(*M, F, static_cast(SubIndex)); + const int OuterCount = R->FieldSize(*M, F); + if (SubIndex >= static_cast(OuterCount)) + return {}; - const auto* SubDesc = SubArrayMsg.GetDescriptor(); - const auto* SubRefl = SubArrayMsg.GetReflection(); - if (!SubDesc || !SubRefl) - return {}; + Scratch.clear(); - // Iterate over nested repeated fields (usually one) - for (int i = 0; i < SubDesc->field_count(); ++i) - { - const auto* InnerField = SubDesc->field(i); - if (!InnerField || !InnerField->is_repeated()) - continue; + // Retrieve the message representing one "subarray" + const google::protobuf::Message& SubArrayMsg = R->GetRepeatedMessage(*M, F, static_cast(SubIndex)); - const int InnerCount = SubRefl->FieldSize(SubArrayMsg, InnerField); - if (InnerCount == 0) - continue; + const auto* SubDesc = SubArrayMsg.GetDescriptor(); + const auto* SubRefl = SubArrayMsg.GetReflection(); + if (!SubDesc || !SubRefl) + return {}; - // Serialize all elements inside the subarray - for (int j = 0; j < InnerCount; ++j) - { - if (InnerField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) - { - const google::protobuf::Message& ElemMsg = - SubRefl->GetRepeatedMessage(SubArrayMsg, InnerField, j); - - // Attempt to pack known math structs directly - if (!PackKnownStruct(ElemMsg, Scratch)) - { - std::vector Data(static_cast(ElemMsg.ByteSizeLong())); - ElemMsg.SerializePartialToArray(Data.data(), static_cast(Data.size())); - Scratch.insert(Scratch.end(), Data.begin(), Data.end()); - } - } - else if (InnerField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_STRING) - { - std::string s = SubRefl->GetRepeatedString(SubArrayMsg, InnerField, j); - Scratch.insert(Scratch.end(), - reinterpret_cast(s.data()), - reinterpret_cast(s.data()) + s.size()); - } - else - { - // Handle scalar types - switch (InnerField->cpp_type()) - { + // Iterate over nested repeated fields (usually one) + for (int i = 0; i < SubDesc->field_count(); ++i) { + const auto* InnerField = SubDesc->field(i); + if (!InnerField || !InnerField->is_repeated()) + continue; + + const int InnerCount = SubRefl->FieldSize(SubArrayMsg, InnerField); + if (InnerCount == 0) + continue; + + // Serialize all elements inside the subarray + for (int j = 0; j < InnerCount; ++j) { + if (InnerField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { + const google::protobuf::Message& ElemMsg = SubRefl->GetRepeatedMessage(SubArrayMsg, InnerField, j); + + // Attempt to pack known math structs directly + if (!PackKnownStruct(ElemMsg, Scratch)) { + std::vector Data(static_cast(ElemMsg.ByteSizeLong())); + ElemMsg.SerializePartialToArray(Data.data(), static_cast(Data.size())); + Scratch.insert(Scratch.end(), Data.begin(), Data.end()); + } + } else if (InnerField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_STRING) { + std::string s = SubRefl->GetRepeatedString(SubArrayMsg, InnerField, j); + Scratch.insert(Scratch.end(), reinterpret_cast(s.data()), + reinterpret_cast(s.data()) + s.size()); + } else { + // Handle scalar types + switch (InnerField->cpp_type()) { case google::protobuf::FieldDescriptor::CPPTYPE_INT32: - PutLE(Scratch, SubRefl->GetRepeatedInt32(SubArrayMsg, InnerField, j)); break; + PutLE(Scratch, SubRefl->GetRepeatedInt32(SubArrayMsg, InnerField, j)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: - PutLE(Scratch, SubRefl->GetRepeatedUInt32(SubArrayMsg, InnerField, j)); break; + PutLE(Scratch, SubRefl->GetRepeatedUInt32(SubArrayMsg, InnerField, j)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_INT64: - PutLE(Scratch, SubRefl->GetRepeatedInt64(SubArrayMsg, InnerField, j)); break; + PutLE(Scratch, SubRefl->GetRepeatedInt64(SubArrayMsg, InnerField, j)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: - PutLE(Scratch, SubRefl->GetRepeatedUInt64(SubArrayMsg, InnerField, j)); break; + PutLE(Scratch, SubRefl->GetRepeatedUInt64(SubArrayMsg, InnerField, j)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: - PutLE(Scratch, SubRefl->GetRepeatedFloat(SubArrayMsg, InnerField, j)); break; + PutLE(Scratch, SubRefl->GetRepeatedFloat(SubArrayMsg, InnerField, j)); + break; case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: - PutLE(Scratch, SubRefl->GetRepeatedDouble(SubArrayMsg, InnerField, j)); break; - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: - { + PutLE(Scratch, SubRefl->GetRepeatedDouble(SubArrayMsg, InnerField, j)); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { uint8_t val = SubRefl->GetRepeatedBool(SubArrayMsg, InnerField, j) ? 1 : 0; Scratch.push_back(static_cast(val)); break; } default: break; + } } } + + // Only process the first repeated field per subarray + break; } - // Only process the first repeated field per subarray - break; + return Publish(Scratch); } - return Publish(Scratch); -} - -// ============================================================ -// GetMapSize -// ------------------------------------------------------------ -// Returns the number of key-value pairs in a map field. -// Supports both native protobuf map and Unreal-style -// FVTXMapContainer (Keys + Values arrays). -// ============================================================ -size_t FProtobufViewAdapter::GetMapSize(const VtxDiff::FieldDesc& Field) const -{ - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); - - if (!M || !D || !R) - return 0; - - const auto* F = D->FindFieldByName(Field.name); - if (!F) - return 0; + // ============================================================ + // GetMapSize + // ------------------------------------------------------------ + // Returns the number of key-value pairs in a map field. + // Supports both native protobuf map and Unreal-style + // FVTXMapContainer (Keys + Values arrays). + // ============================================================ + size_t FProtobufViewAdapter::GetMapSize(const VtxDiff::FieldDesc& Field) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); - // Case 1: native protobuf map - if (F->is_map()) - return static_cast(R->FieldSize(*M, F)); + if (!M || !D || !R) + return 0; - // Case 2: Unreal-style "FVTXMapContainer" (Keys + Values arrays) - if (Field.type == EVTXContainerType::MapProperties) - { - const auto* KeysField = D->FindFieldByName("Keys"); - const auto* ValuesField = D->FindFieldByName("Values"); - if (!KeysField || !ValuesField) + const auto* F = D->FindFieldByName(Field.name); + if (!F) return 0; - const int KeyCount = R->FieldSize(*M, KeysField); - const int ValueCount = R->FieldSize(*M, ValuesField); - return static_cast(std::min(KeyCount, ValueCount)); - } + // Case 1: native protobuf map + if (F->is_map()) + return static_cast(R->FieldSize(*M, F)); - return 0; -} + // Case 2: Unreal-style "FVTXMapContainer" (Keys + Values arrays) + if (Field.type == EVTXContainerType::MapProperties) { + const auto* KeysField = D->FindFieldByName("Keys"); + const auto* ValuesField = D->FindFieldByName("Values"); + if (!KeysField || !ValuesField) + return 0; + const int KeyCount = R->FieldSize(*M, KeysField); + const int ValueCount = R->FieldSize(*M, ValuesField); + return static_cast(std::min(KeyCount, ValueCount)); + } -// ============================================================ -// GetMapKey -// ------------------------------------------------------------ -// Retrieves the string representation of a map key at the given -// index. Supports both native protobuf maps and FVTXMapContainer. -// ============================================================ -std::string FProtobufViewAdapter::GetMapKey(const VtxDiff::FieldDesc& Field, size_t Index) const -{ - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); + return 0; + } - if (!M || !D || !R) - return {}; - const auto* F = D->FindFieldByName(Field.name); - if (!F) - return {}; + // ============================================================ + // GetMapKey + // ------------------------------------------------------------ + // Retrieves the string representation of a map key at the given + // index. Supports both native protobuf maps and FVTXMapContainer. + // ============================================================ + std::string FProtobufViewAdapter::GetMapKey(const VtxDiff::FieldDesc& Field, size_t Index) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); - // --- Case 1: Native protobuf map --- - if (F->is_map()) - { - const google::protobuf::Message& Entry = - R->GetRepeatedMessage(*M, F, static_cast(Index)); + if (!M || !D || !R) + return {}; - const auto* EntryRefl = Entry.GetReflection(); - const auto* KeyField = F->message_type()->map_key(); - if (!KeyField || !EntryRefl) + const auto* F = D->FindFieldByName(Field.name); + if (!F) return {}; - switch (KeyField->cpp_type()) - { + // --- Case 1: Native protobuf map --- + if (F->is_map()) { + const google::protobuf::Message& Entry = R->GetRepeatedMessage(*M, F, static_cast(Index)); + + const auto* EntryRefl = Entry.GetReflection(); + const auto* KeyField = F->message_type()->map_key(); + if (!KeyField || !EntryRefl) + return {}; + + switch (KeyField->cpp_type()) { case google::protobuf::FieldDescriptor::CPPTYPE_STRING: return EntryRefl->GetString(Entry, KeyField); case google::protobuf::FieldDescriptor::CPPTYPE_INT32: @@ -987,171 +996,184 @@ std::string FProtobufViewAdapter::GetMapKey(const VtxDiff::FieldDesc& Field, siz return std::to_string(EntryRefl->GetUInt64(Entry, KeyField)); default: return {}; + } } - } - // --- Case 2: FVTXMapContainer (Keys + Values arrays) --- - if (Field.type == EVTXContainerType::MapProperties) - { - const auto* KeysField = D->FindFieldByName("Keys"); - if (!KeysField || !KeysField->is_repeated()) + // --- Case 2: FVTXMapContainer (Keys + Values arrays) --- + if (Field.type == EVTXContainerType::MapProperties) { + const auto* KeysField = D->FindFieldByName("Keys"); + if (!KeysField || !KeysField->is_repeated()) + return {}; + + if (Index >= static_cast(R->FieldSize(*M, KeysField))) + return {}; + + if (KeysField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_STRING) + return R->GetRepeatedString(*M, KeysField, static_cast(Index)); + + if (KeysField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_INT32) + return std::to_string(R->GetRepeatedInt32(*M, KeysField, static_cast(Index))); + return {}; + } - if (Index >= static_cast(R->FieldSize(*M, KeysField))) + return {}; + } + + + // ============================================================ + // GetMapValueAsStruct + // ------------------------------------------------------------ + // Returns a node view for the map value at the specified index. + // Supports both native protobuf maps and FVTXMapContainer. + // ============================================================ + FProtobufViewAdapter FProtobufViewAdapter::GetMapValueAsStruct(const VtxDiff::FieldDesc& Field, + size_t Index) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); + if (!M || !D || !R) + return {}; + const auto* F = D->FindFieldByName(Field.name); + if (!F) return {}; - if (KeysField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_STRING) - return R->GetRepeatedString(*M, KeysField, static_cast(Index)); + if (F->is_map()) { + const google::protobuf::Message& Entry = R->GetRepeatedMessage(*M, F, static_cast(Index)); + const auto* EntryRefl = Entry.GetReflection(); + const auto* ValueField = F->message_type()->map_value(); + if (!EntryRefl || !ValueField || + ValueField->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) + return {}; - if (KeysField->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_INT32) - return std::to_string(R->GetRepeatedInt32(*M, KeysField, static_cast(Index))); + const google::protobuf::Message& ValueMsg = EntryRefl->GetMessage(Entry, ValueField); + return FProtobufViewAdapter(&ValueMsg, false); + } + + if (Field.type == EVTXContainerType::MapProperties) { + const auto* ValuesField = D->FindFieldByName("Values"); + if (!ValuesField || !ValuesField->is_repeated() || + Index >= static_cast(R->FieldSize(*M, ValuesField))) + return {}; + const google::protobuf::Message& SubMsg = R->GetRepeatedMessage(*M, ValuesField, static_cast(Index)); + return FProtobufViewAdapter(&SubMsg, false); + } return {}; } - return {}; -} - - -// ============================================================ -// GetMapValueAsStruct -// ------------------------------------------------------------ -// Returns a node view for the map value at the specified index. -// Supports both native protobuf maps and FVTXMapContainer. -// ============================================================ -FProtobufViewAdapter FProtobufViewAdapter::GetMapValueAsStruct(const VtxDiff::FieldDesc& Field, size_t Index) const -{ - const auto* M = GetMsg(); const auto* D = GetDesc(); const auto* R = GetRefl(); - if (!M || !D || !R) return {}; - const auto* F = D->FindFieldByName(Field.name); - if (!F) return {}; - - if (F->is_map()) { - const google::protobuf::Message& Entry = R->GetRepeatedMessage(*M, F, static_cast(Index)); - const auto* EntryRefl = Entry.GetReflection(); - const auto* ValueField = F->message_type()->map_value(); - if (!EntryRefl || !ValueField || ValueField->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) return {}; - - const google::protobuf::Message& ValueMsg = EntryRefl->GetMessage(Entry, ValueField); - return FProtobufViewAdapter(&ValueMsg, false); + // ============================================================ + // GetArrayElementAsStruct + // ------------------------------------------------------------ + // Returns a nested IBinaryNodeView for an element inside a + // repeated message field (e.g., array of structs). + // ============================================================ + FProtobufViewAdapter FProtobufViewAdapter::GetArrayElementAsStruct(const VtxDiff::FieldDesc& Field, + size_t Index) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); + if (!M || !D || !R) + return {}; + const auto* F = D->FindFieldByName(Field.name); + if (!F || !F->is_repeated() || F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) + return {}; + + const int Count = R->FieldSize(*M, F); + if (Index >= static_cast(Count)) + return {}; + + const google::protobuf::Message& NestedMsg = R->GetRepeatedMessage(*M, F, static_cast(Index)); + return FProtobufViewAdapter(&NestedMsg, false); } - if (Field.type == EVTXContainerType::MapProperties) { - const auto* ValuesField = D->FindFieldByName("Values"); - if (!ValuesField || !ValuesField->is_repeated() || Index >= static_cast(R->FieldSize(*M, ValuesField))) return {}; - const google::protobuf::Message& SubMsg = R->GetRepeatedMessage(*M, ValuesField, static_cast(Index)); - return FProtobufViewAdapter(&SubMsg, false); + // ============================================================ + // IsValid + // ------------------------------------------------------------ + // Returns true if the current adapter contains a valid message, + // descriptor, and reflection instance. + // ============================================================ + bool FProtobufViewAdapter::IsValid() const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); + + return (M != nullptr && D != nullptr && R != nullptr); + } + + + // ============================================================ + // GetNestedStruct + // ------------------------------------------------------------ + // Returns a nested IBinaryNodeView for a singular message field + // (non-repeated). Used for nested structures or sub-messages. + // ============================================================ + FProtobufViewAdapter FProtobufViewAdapter::GetNestedStruct(const VtxDiff::FieldDesc& Field) const { + const auto* M = GetMsg(); + const auto* D = GetDesc(); + const auto* R = GetRefl(); + if (!M || !D || !R) + return {}; + const auto* F = D->FindFieldByName(Field.name); + if (!F || F->is_repeated() || F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) + return {}; + if (!R->HasField(*M, F)) + return {}; + + const google::protobuf::Message& Nested = R->GetMessage(*M, F); + return FProtobufViewAdapter(&Nested, false); } - return {}; -} - -// ============================================================ -// GetArrayElementAsStruct -// ------------------------------------------------------------ -// Returns a nested IBinaryNodeView for an element inside a -// repeated message field (e.g., array of structs). -// ============================================================ -FProtobufViewAdapter FProtobufViewAdapter::GetArrayElementAsStruct(const VtxDiff::FieldDesc& Field, size_t Index) const -{ - const auto* M = GetMsg(); const auto* D = GetDesc(); const auto* R = GetRefl(); - if (!M || !D || !R) return {}; - const auto* F = D->FindFieldByName(Field.name); - if (!F || !F->is_repeated() || F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) return {}; - - const int Count = R->FieldSize(*M, F); - if (Index >= static_cast(Count)) return {}; - - const google::protobuf::Message& NestedMsg = R->GetRepeatedMessage(*M, F, static_cast(Index)); - return FProtobufViewAdapter(&NestedMsg, false); -} - - -// ============================================================ -// IsValid -// ------------------------------------------------------------ -// Returns true if the current adapter contains a valid message, -// descriptor, and reflection instance. -// ============================================================ -bool FProtobufViewAdapter::IsValid() const -{ - const auto* M = GetMsg(); - const auto* D = GetDesc(); - const auto* R = GetRefl(); - - return (M != nullptr && D != nullptr && R != nullptr); -} - - -// ============================================================ -// GetNestedStruct -// ------------------------------------------------------------ -// Returns a nested IBinaryNodeView for a singular message field -// (non-repeated). Used for nested structures or sub-messages. -// ============================================================ -FProtobufViewAdapter FProtobufViewAdapter::GetNestedStruct(const VtxDiff::FieldDesc& Field) const -{ - const auto* M = GetMsg(); const auto* D = GetDesc(); const auto* R = GetRefl(); - if (!M || !D || !R) return {}; - const auto* F = D->FindFieldByName(Field.name); - if (!F || F->is_repeated() || F->cpp_type() != google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) return {}; - if (!R->HasField(*M, F)) return {}; - - const google::protobuf::Message& Nested = R->GetMessage(*M, F); - return FProtobufViewAdapter(&Nested, false); -} - - - // ============================================================================ + + + // ============================================================================ // PbViewFactory implementation // ============================================================================ - void PbViewFactory::SetSubArrayNames(const std::unordered_map& Map) - { + void PbViewFactory::SetSubArrayNames(const std::unordered_map& Map) { SubNames = std::move(Map); } - std::optional PbViewFactory::CreateRoot(std::span Buffer) const - { - if (RootType.empty() || Buffer.empty()) return std::nullopt; + std::optional PbViewFactory::CreateRoot(std::span Buffer) const { + if (RootType.empty() || Buffer.empty()) + return std::nullopt; const DescriptorPool* Pool = UsePool ? UsePool : DescriptorPool::generated_pool(); const Descriptor* RootDesc = Pool->FindMessageTypeByName(RootType); - if (!RootDesc) return std::nullopt; + if (!RootDesc) + return std::nullopt; const uint8_t* DataPtr = reinterpret_cast(Buffer.data()); const size_t DataSize = Buffer.size(); auto View = FProtobufViewAdapter::CreateRoot(Pool, RootType, DataPtr, DataSize); - if (!View) return std::nullopt; + if (!View) + return std::nullopt; - if (!SubNames.empty()) View->SetSubArrayNames(SubNames); + if (!SubNames.empty()) + View->SetSubArrayNames(SubNames); return View; } - bool PbViewFactory::InitFromFile(const std::string& Path, const std::string& InRootType) - { + bool PbViewFactory::InitFromFile(const std::string& Path, const std::string& InRootType) { RootType = InRootType; // File-based descriptor loading not supported; use InitFromMemory instead return false; } - bool PbViewFactory::InitFromMemory(const uint8_t* Data, size_t Size, const std::string& InRootType) - { + bool PbViewFactory::InitFromMemory(const uint8_t* Data, size_t Size, const std::string& InRootType) { RootType = InRootType; // If no data provided, use the default generated pool - if (!Data || Size == 0) - { + if (!Data || Size == 0) { UsePool = DescriptorPool::generated_pool(); return true; } // Attempt to parse a serialized FileDescriptorSet from memory FileDescriptorSet DescriptorSet; - if (!DescriptorSet.ParseFromArray(Data, static_cast(Size))) - { + if (!DescriptorSet.ParseFromArray(Data, static_cast(Size))) { VTX_ERROR("PbViewFactory: Failed to parse FileDescriptorSet; falling back to generated_pool()."); UsePool = DescriptorPool::generated_pool(); return true; @@ -1159,11 +1181,9 @@ FProtobufViewAdapter FProtobufViewAdapter::GetNestedStruct(const VtxDiff::FieldD // Build a custom DescriptorPool dynamically from the FileDescriptorSet auto NewPool = std::make_unique(); - for (int I = 0; I < DescriptorSet.file_size(); ++I) - { + for (int I = 0; I < DescriptorSet.file_size(); ++I) { const FileDescriptor* FileDesc = NewPool->BuildFile(DescriptorSet.file(I)); - if (!FileDesc) - { + if (!FileDesc) { VTX_ERROR("PbViewFactory: Failed to build FileDescriptor (index {}); using generated_pool().", I); UsePool = DescriptorPool::generated_pool(); return true; @@ -1177,4 +1197,3 @@ FProtobufViewAdapter FProtobufViewAdapter::GetNestedStruct(const VtxDiff::FieldD } } // namespace VtxDiff::Protobuf - diff --git a/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.h b/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.h index 2be4f54..0dc184f 100644 --- a/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.h +++ b/sdk/src/vtx_differ/src/vtx/differ/adapters/protobuff/vtx_protobuff_view_adapter.h @@ -16,123 +16,120 @@ namespace VtxDiff::Protobuf { -// ================================================================ -// Struct: SubArrayNames -// --------------------------------------------------------------- -// Describes paired fields for subarray handling, e.g.: -// "vertices_arrays" + "vertices_offsets" -// ================================================================ - struct SubArrayNames - { + // ================================================================ + // Struct: SubArrayNames + // --------------------------------------------------------------- + // Describes paired fields for subarray handling, e.g.: + // "vertices_arrays" + "vertices_offsets" + // ================================================================ + struct SubArrayNames { std::string FlatDataField; std::string OffsetsField; }; -// ================================================================ -// Class: ProtobufViewAdapter -// --------------------------------------------------------------- -// A reflection-based adapter that exposes a protobuf message as a -// hierarchical binary tree for diff computation. -// ================================================================ -class FProtobufViewAdapter final : public IBinaryNodeView -{ -public: - FProtobufViewAdapter(const google::protobuf::Message* Msg); - virtual ~FProtobufViewAdapter() override; - - static std::unique_ptr CreateRoot( - const google::protobuf::DescriptorPool* Pool, - const std::string& RootType, - const uint8_t* Buffer, - size_t Size); - - static std::unique_ptr FromMessage( - const google::protobuf::Message& Message); - - void SetSubArrayNames(std::unordered_map Map); - - // ============================================================ - // IBinaryNodeView interface - // ============================================================ - virtual std::vector EnumerateFields() const override; - virtual std::span GetFieldBytes(const VtxDiff::FieldDesc& Field) const override; - virtual size_t GetArraySize(const VtxDiff::FieldDesc& Field) const override; - virtual std::span GetArrayElementBytes(const VtxDiff::FieldDesc& Field, size_t Index) const override; - virtual std::span GetSubArrayBytes(const VtxDiff::FieldDesc& Field, size_t SubIndex) const override; - virtual std::unique_ptr GetNestedStruct(const VtxDiff::FieldDesc& Field) const override; - virtual std::unique_ptr GetArrayElementAsStruct(const VtxDiff::FieldDesc& Field, size_t Index) const override; - virtual bool IsValid() const override; - virtual size_t GetMapSize(const VtxDiff::FieldDesc& Field) const override; - virtual std::string GetMapKey(const VtxDiff::FieldDesc& Field, size_t Index) const override; - virtual std::unique_ptr GetMapValueAsStruct(const VtxDiff::FieldDesc& Field, size_t Index) const override; - virtual std::string GetScalarFieldString(const std::string& FieldName) const override; - virtual std::unique_ptr GetFieldByName(const std::string& FieldName) const override; -private: - std::unique_ptr Owned; - const google::protobuf::Message* External = nullptr; - - mutable std::vector> Arena; - mutable std::vector Scratch; - mutable std::string TempString; - std::unordered_map SubNames; - -private: - // Helpers internos - const google::protobuf::FieldDescriptor* FindField(std::string_view Name) const; - - bool IsScalar(const google::protobuf::FieldDescriptor* Field) const; - - EVTXContainerType InferContainerType(const google::protobuf::FieldDescriptor* Field) const; - std::span Publish(const void* Data, size_t Size) const; - std::span Publish(const std::vector& Buffer) const; - std::span Publish(std::vector&& Buffer) const; - void ResetArena() const; - const google::protobuf::Descriptor* ActiveDesc() const; - static std::unique_ptr CloneMessage(const google::protobuf::Message& Src); - -public: - virtual void Reset() override; - - inline const google::protobuf::Message* GetMsg() const noexcept { - return Owned ? Owned.get() : External; - } - - inline const google::protobuf::Descriptor* GetDesc() const noexcept { - auto* m = GetMsg(); - return m ? m->GetDescriptor() : nullptr; - } - - inline const google::protobuf::Reflection* GetRefl() const noexcept { - auto* m = GetMsg(); - return m ? m->GetReflection() : nullptr; - } - -}; - -// ================================================================ -// Class: PbViewFactory -// --------------------------------------------------------------- -// Factory responsible for loading descriptor pools and creating -// root-level ProtobufViewAdapters. -// ================================================================ -class PbViewFactory final : public IBinaryViewFactory { -public: - PbViewFactory() = default; - ~PbViewFactory() override = default; - - bool InitFromMemory(const uint8_t* Data, size_t Size, const std::string& RootType); - bool InitFromFile(const std::string& Path, const std::string& RootType); - - void SetSubArrayNames(const std::unordered_map& Map); - std::unique_ptr CreateRoot(std::span Buffer) const override; - - const std::string& GetRootType() const { return RootType; } - -private: - std::string RootType; - std::unordered_map SubNames; - const google::protobuf::DescriptorPool* UsePool = nullptr; - std::unique_ptr OwnedPool; -}; + // ================================================================ + // Class: ProtobufViewAdapter + // --------------------------------------------------------------- + // A reflection-based adapter that exposes a protobuf message as a + // hierarchical binary tree for diff computation. + // ================================================================ + class FProtobufViewAdapter final : public IBinaryNodeView { + public: + FProtobufViewAdapter(const google::protobuf::Message* Msg); + virtual ~FProtobufViewAdapter() override; + + static std::unique_ptr CreateRoot(const google::protobuf::DescriptorPool* Pool, + const std::string& RootType, const uint8_t* Buffer, + size_t Size); + + static std::unique_ptr FromMessage(const google::protobuf::Message& Message); + + void SetSubArrayNames(std::unordered_map Map); + + // ============================================================ + // IBinaryNodeView interface + // ============================================================ + virtual std::vector EnumerateFields() const override; + virtual std::span GetFieldBytes(const VtxDiff::FieldDesc& Field) const override; + virtual size_t GetArraySize(const VtxDiff::FieldDesc& Field) const override; + virtual std::span GetArrayElementBytes(const VtxDiff::FieldDesc& Field, + size_t Index) const override; + virtual std::span GetSubArrayBytes(const VtxDiff::FieldDesc& Field, + size_t SubIndex) const override; + virtual std::unique_ptr GetNestedStruct(const VtxDiff::FieldDesc& Field) const override; + virtual std::unique_ptr GetArrayElementAsStruct(const VtxDiff::FieldDesc& Field, + size_t Index) const override; + virtual bool IsValid() const override; + virtual size_t GetMapSize(const VtxDiff::FieldDesc& Field) const override; + virtual std::string GetMapKey(const VtxDiff::FieldDesc& Field, size_t Index) const override; + virtual std::unique_ptr GetMapValueAsStruct(const VtxDiff::FieldDesc& Field, + size_t Index) const override; + virtual std::string GetScalarFieldString(const std::string& FieldName) const override; + virtual std::unique_ptr GetFieldByName(const std::string& FieldName) const override; + + private: + std::unique_ptr Owned; + const google::protobuf::Message* External = nullptr; + + mutable std::vector> Arena; + mutable std::vector Scratch; + mutable std::string TempString; + std::unordered_map SubNames; + + private: + // Helpers internos + const google::protobuf::FieldDescriptor* FindField(std::string_view Name) const; + + bool IsScalar(const google::protobuf::FieldDescriptor* Field) const; + + EVTXContainerType InferContainerType(const google::protobuf::FieldDescriptor* Field) const; + std::span Publish(const void* Data, size_t Size) const; + std::span Publish(const std::vector& Buffer) const; + std::span Publish(std::vector&& Buffer) const; + void ResetArena() const; + const google::protobuf::Descriptor* ActiveDesc() const; + static std::unique_ptr CloneMessage(const google::protobuf::Message& Src); + + public: + virtual void Reset() override; + + inline const google::protobuf::Message* GetMsg() const noexcept { return Owned ? Owned.get() : External; } + + inline const google::protobuf::Descriptor* GetDesc() const noexcept { + auto* m = GetMsg(); + return m ? m->GetDescriptor() : nullptr; + } + + inline const google::protobuf::Reflection* GetRefl() const noexcept { + auto* m = GetMsg(); + return m ? m->GetReflection() : nullptr; + } + }; + + // ================================================================ + // Class: PbViewFactory + // --------------------------------------------------------------- + // Factory responsible for loading descriptor pools and creating + // root-level ProtobufViewAdapters. + // ================================================================ + class PbViewFactory final : public IBinaryViewFactory { + public: + PbViewFactory() = default; + ~PbViewFactory() override = default; + + bool InitFromMemory(const uint8_t* Data, size_t Size, const std::string& RootType); + bool InitFromFile(const std::string& Path, const std::string& RootType); + + void SetSubArrayNames(const std::unordered_map& Map); + std::unique_ptr CreateRoot(std::span Buffer) const override; + + const std::string& GetRootType() const { return RootType; } + + private: + std::string RootType; + std::unordered_map SubNames; + const google::protobuf::DescriptorPool* UsePool = nullptr; + std::unique_ptr OwnedPool; + }; } // namespace VtxDiff::Protobuf diff --git a/sdk/src/vtx_differ/src/vtx/differ/core/vtx_differ_facade.cpp b/sdk/src/vtx_differ/src/vtx/differ/core/vtx_differ_facade.cpp index b4b4863..0209c02 100644 --- a/sdk/src/vtx_differ/src/vtx/differ/core/vtx_differ_facade.cpp +++ b/sdk/src/vtx_differ/src/vtx/differ/core/vtx_differ_facade.cpp @@ -7,63 +7,55 @@ namespace VtxDiff { -class FlatBuffersDifferFacade final : public IVtxDifferFacade { -public: - FlatBuffersDifferFacade() { - factory_.InitFromMemory(nullptr, 0, "fbsvtx.Bucket"); - } + class FlatBuffersDifferFacade final : public IVtxDifferFacade { + public: + FlatBuffersDifferFacade() { factory_.InitFromMemory(nullptr, 0, "fbsvtx.Bucket"); } - PatchIndex DiffRawFrames( - std::span frame_a, - std::span frame_b, - const DiffOptions& options) override - { - auto node_a = factory_.CreateRoot(frame_a); - auto node_b = factory_.CreateRoot(frame_b); - if (!node_a || !node_b) return {}; + PatchIndex DiffRawFrames(std::span frame_a, std::span frame_b, + const DiffOptions& options) override { + auto node_a = factory_.CreateRoot(frame_a); + auto node_b = factory_.CreateRoot(frame_b); + if (!node_a || !node_b) + return {}; - DefaultTreeDiff engine; - return engine.ComputeDiff(*node_a, *node_b, options); - } + DefaultTreeDiff engine; + return engine.ComputeDiff(*node_a, *node_b, options); + } -private: - Flatbuffers::FbViewFactory factory_; -}; + private: + Flatbuffers::FbViewFactory factory_; + }; -class ProtobufDifferFacade final : public IVtxDifferFacade { -public: - ProtobufDifferFacade() { - factory_.InitFromMemory(nullptr, 0, "cppvtx.Bucket"); - } + class ProtobufDifferFacade final : public IVtxDifferFacade { + public: + ProtobufDifferFacade() { factory_.InitFromMemory(nullptr, 0, "cppvtx.Bucket"); } - PatchIndex DiffRawFrames( - std::span frame_a, - std::span frame_b, - const DiffOptions& options) override - { - auto node_a = factory_.CreateRoot(frame_a); - auto node_b = factory_.CreateRoot(frame_b); - if (!node_a || !node_b) return {}; + PatchIndex DiffRawFrames(std::span frame_a, std::span frame_b, + const DiffOptions& options) override { + auto node_a = factory_.CreateRoot(frame_a); + auto node_b = factory_.CreateRoot(frame_b); + if (!node_a || !node_b) + return {}; - DefaultTreeDiff engine; - return engine.ComputeDiff(*node_a, *node_b, options); - } + DefaultTreeDiff engine; + return engine.ComputeDiff(*node_a, *node_b, options); + } -private: - Protobuf::PbViewFactory factory_; -}; + private: + Protobuf::PbViewFactory factory_; + }; -std::unique_ptr CreateDifferFacade(VTX::VtxFormat format) { - switch (format) { + std::unique_ptr CreateDifferFacade(VTX::VtxFormat format) { + switch (format) { case VTX::VtxFormat::FlatBuffers: return std::make_unique(); case VTX::VtxFormat::Protobuf: return std::make_unique(); default: return nullptr; + } } -} } // namespace VtxDiff diff --git a/sdk/src/vtx_reader/src/vtx/reader/core/vtx_deserializer_service.cpp b/sdk/src/vtx_reader/src/vtx/reader/core/vtx_deserializer_service.cpp index 43c4782..fe952e1 100644 --- a/sdk/src/vtx_reader/src/vtx/reader/core/vtx_deserializer_service.cpp +++ b/sdk/src/vtx_reader/src/vtx/reader/core/vtx_deserializer_service.cpp @@ -6,8 +6,7 @@ #include "vtx_schema.pb.h" using namespace VTX; -std::string VTX::ReplayUnpacker::Decompress(const std::string& compressed_data) -{ +std::string VTX::ReplayUnpacker::Decompress(const std::string& compressed_data) { //zstd includes a magic number of 4 bytes, if is smaller than that is not a valid zstd buffer if (compressed_data.size() < 4) { return compressed_data; @@ -16,13 +15,13 @@ std::string VTX::ReplayUnpacker::Decompress(const std::string& compressed_data) //get magic number const uint8_t* bytes = reinterpret_cast(compressed_data.data()); bool is_zstd = (bytes[0] == 0x28 && bytes[1] == 0xB5 && bytes[2] == 0x2F && bytes[3] == 0xFD); - + if (!is_zstd) { return compressed_data; } - + unsigned long long const rSize = ZSTD_getFrameContentSize(compressed_data.data(), compressed_data.size()); - + if (rSize == ZSTD_CONTENTSIZE_ERROR) { throw std::runtime_error("ZSTD: It's not a standard ZSTD compressed block"); } @@ -33,10 +32,8 @@ std::string VTX::ReplayUnpacker::Decompress(const std::string& compressed_data) std::string decompressedBuffer; decompressedBuffer.resize(rSize); - size_t const dSize = ZSTD_decompress( - decompressedBuffer.data(), rSize, - compressed_data.data(), compressed_data.size() - ); + size_t const dSize = + ZSTD_decompress(decompressedBuffer.data(), rSize, compressed_data.data(), compressed_data.size()); if (ZSTD_isError(dSize)) { throw std::runtime_error("ZSTD Decompress Error: " + std::string(ZSTD_getErrorName(dSize))); @@ -50,21 +47,20 @@ std::vector> VTX::ReplayUnpacker::Unpack(const cp if (chunk.is_compressed() && !chunk.compressed_data().empty()) { const std::string& compressed = chunk.compressed_data(); - + unsigned long long const c_size = ZSTD_getFrameContentSize(compressed.data(), compressed.size()); if (c_size == ZSTD_CONTENTSIZE_ERROR || c_size == ZSTD_CONTENTSIZE_UNKNOWN) { throw std::runtime_error("VTX Reader: Unknown ZSTD original size"); } uncompressed_data.resize(c_size); - size_t const dSize = ZSTD_decompress(uncompressed_data.data(), c_size, - compressed.data(), compressed.size()); + size_t const dSize = ZSTD_decompress(uncompressed_data.data(), c_size, compressed.data(), compressed.size()); if (ZSTD_isError(dSize)) { throw std::runtime_error("VTX Reader: Error decompressing ZSTD"); } } else { - uncompressed_data = chunk.compressed_data(); + uncompressed_data = chunk.compressed_data(); } cppvtx::FrameChunk innerContainer; diff --git a/sdk/src/vtx_reader/src/vtx/reader/core/vtx_reader_facade.cpp b/sdk/src/vtx_reader/src/vtx/reader/core/vtx_reader_facade.cpp index 546b8ac..b00f6d3 100644 --- a/sdk/src/vtx_reader/src/vtx/reader/core/vtx_reader_facade.cpp +++ b/sdk/src/vtx_reader/src/vtx/reader/core/vtx_reader_facade.cpp @@ -12,280 +12,238 @@ namespace VTX { -void ReaderChunkState::OnChunkLoadStarted(int32_t chunk_idx) { - std::lock_guard lock(state_mutex_); - loading_chunks_.push_back(chunk_idx); - VTX_DEBUG("Chunk {} load started.", chunk_idx); -} - -void ReaderChunkState::OnChunkLoadFinished(int32_t chunk_idx) { - std::lock_guard lock(state_mutex_); - auto it = std::remove(loading_chunks_.begin(), loading_chunks_.end(), chunk_idx); - if (it != loading_chunks_.end()) { - loading_chunks_.erase(it, loading_chunks_.end()); + void ReaderChunkState::OnChunkLoadStarted(int32_t chunk_idx) { + std::lock_guard lock(state_mutex_); + loading_chunks_.push_back(chunk_idx); + VTX_DEBUG("Chunk {} load started.", chunk_idx); } - loaded_chunks_.push_back(chunk_idx); - std::sort(loaded_chunks_.begin(), loaded_chunks_.end()); - VTX_DEBUG("Chunk {} loaded into RAM.", chunk_idx); -} - -void ReaderChunkState::OnChunkEvicted(int32_t chunk_idx) { - std::lock_guard lock(state_mutex_); - auto it = std::remove(loaded_chunks_.begin(), loaded_chunks_.end(), chunk_idx); - if (it != loaded_chunks_.end()) { - loaded_chunks_.erase(it, loaded_chunks_.end()); - } - VTX_DEBUG("Chunk {} evicted from RAM.", chunk_idx); -} - -ReaderChunkSnapshot ReaderChunkState::GetSnapshot() const { - std::lock_guard lock(state_mutex_); - return ReaderChunkSnapshot{ - .loaded_chunks = loaded_chunks_, - .loading_chunks = loading_chunks_, - }; -} - -void ReaderChunkState::Reset() { - std::lock_guard lock(state_mutex_); - loaded_chunks_.clear(); - loading_chunks_.clear(); -} - -class FlatBuffersFacadeImpl : public IVtxReaderFacade { -public: - FlatBuffersFacadeImpl(const std::string& filepath) - : InternalReader(filepath) {} - - void SetEvents(const ReplayReaderEvents& events) override { - InternalReader.SetEvents(events); - } - - VTX::FileHeader GetHeader() override { - return InternalReader.GetFileHeader(); + void ReaderChunkState::OnChunkLoadFinished(int32_t chunk_idx) { + std::lock_guard lock(state_mutex_); + auto it = std::remove(loading_chunks_.begin(), loading_chunks_.end(), chunk_idx); + if (it != loading_chunks_.end()) { + loading_chunks_.erase(it, loading_chunks_.end()); + } + loaded_chunks_.push_back(chunk_idx); + std::sort(loaded_chunks_.begin(), loaded_chunks_.end()); + VTX_DEBUG("Chunk {} loaded into RAM.", chunk_idx); } - VTX::FileFooter GetFooter() override { - return InternalReader.GetFileFooter(); + void ReaderChunkState::OnChunkEvicted(int32_t chunk_idx) { + std::lock_guard lock(state_mutex_); + auto it = std::remove(loaded_chunks_.begin(), loaded_chunks_.end(), chunk_idx); + if (it != loaded_chunks_.end()) { + loaded_chunks_.erase(it, loaded_chunks_.end()); + } + VTX_DEBUG("Chunk {} evicted from RAM.", chunk_idx); } - VTX::ContextualSchema GetContextualSchema() override { - return InternalReader.GetContextualSchema(); + ReaderChunkSnapshot ReaderChunkState::GetSnapshot() const { + std::lock_guard lock(state_mutex_); + return ReaderChunkSnapshot { + .loaded_chunks = loaded_chunks_, + .loading_chunks = loading_chunks_, + }; } - VTX::PropertyAddressCache GetPropertyAddressCache() override { - return InternalReader.GetPropertyAddressCache(); + void ReaderChunkState::Reset() { + std::lock_guard lock(state_mutex_); + loaded_chunks_.clear(); + loading_chunks_.clear(); } - int32_t GetTotalFrames() const override { - return InternalReader.GetTotalFrames(); - } - const std::vector& GetSeekTable() const override { - return InternalReader.GetSeekTable(); - } + class FlatBuffersFacadeImpl : public IVtxReaderFacade { + public: + FlatBuffersFacadeImpl(const std::string& filepath) + : InternalReader(filepath) {} - void SetCacheWindow(uint32_t backward, uint32_t forward) override { - InternalReader.SetCacheWindow(backward, forward); - } + void SetEvents(const ReplayReaderEvents& events) override { InternalReader.SetEvents(events); } - void WarmAt(int32_t frame_index) override { - InternalReader.WarmAt(frame_index); - } + VTX::FileHeader GetHeader() override { return InternalReader.GetFileHeader(); } - bool GetFrame(int32_t frame_index, VTX::Frame& out_frame) override { - return InternalReader.GetFrame(frame_index, out_frame); - } + VTX::FileFooter GetFooter() override { return InternalReader.GetFileFooter(); } - const VTX::Frame* GetFrame(int32_t frame_index) override { - return InternalReader.GetFramePtr(frame_index); - } + VTX::ContextualSchema GetContextualSchema() override { return InternalReader.GetContextualSchema(); } - const VTX::Frame* GetFrameSync(int frame_index) override { - return InternalReader.GetFramePtrSync(frame_index); - } + VTX::PropertyAddressCache GetPropertyAddressCache() override { + return InternalReader.GetPropertyAddressCache(); + } - void GetFrameRange(int32_t start_frame, int32_t range, std::vector& out_frames) override { - InternalReader.GetFrameRange(start_frame, range, out_frames); - } + int32_t GetTotalFrames() const override { return InternalReader.GetTotalFrames(); } - std::vector GetFrameContext(int32_t center_frame, int32_t back_range, int32_t forward_range) override { - return InternalReader.GetFrameContext(center_frame, back_range, forward_range); - } + const std::vector& GetSeekTable() const override { return InternalReader.GetSeekTable(); } - int32_t GetChunkFrameCountSafe(int32_t chunk_index) override { - return InternalReader.GetChunkFrameCountSafe(chunk_index); - } + void SetCacheWindow(uint32_t backward, uint32_t forward) override { + InternalReader.SetCacheWindow(backward, forward); + } - void InspectChunkHeader(int32_t index) const override { - InternalReader.InspectChunkHeader(index); - } + void WarmAt(int32_t frame_index) override { InternalReader.WarmAt(frame_index); } - FrameAccessor CreateAccessor() const override { - return InternalReader.CreateAccessor(); - } + bool GetFrame(int32_t frame_index, VTX::Frame& out_frame) override { + return InternalReader.GetFrame(frame_index, out_frame); + } - std::span GetRawFrameBytes(int32_t frame_index) override { - return InternalReader.GetRawFrameBytes(frame_index); - } + const VTX::Frame* GetFrame(int32_t frame_index) override { return InternalReader.GetFramePtr(frame_index); } -private: - VTX::ReplayReader InternalReader; -}; + const VTX::Frame* GetFrameSync(int frame_index) override { return InternalReader.GetFramePtrSync(frame_index); } + void GetFrameRange(int32_t start_frame, int32_t range, std::vector& out_frames) override { + InternalReader.GetFrameRange(start_frame, range, out_frames); + } -class ProtobuffFacadeImpl : public IVtxReaderFacade { -public: - ProtobuffFacadeImpl(const std::string& filepath) - : InternalReader(filepath) {} + std::vector GetFrameContext(int32_t center_frame, int32_t back_range, + int32_t forward_range) override { + return InternalReader.GetFrameContext(center_frame, back_range, forward_range); + } - void SetEvents(const ReplayReaderEvents& events) override { - InternalReader.SetEvents(events); - } + int32_t GetChunkFrameCountSafe(int32_t chunk_index) override { + return InternalReader.GetChunkFrameCountSafe(chunk_index); + } - VTX::FileHeader GetHeader() override { - return InternalReader.GetFileHeader(); - } + void InspectChunkHeader(int32_t index) const override { InternalReader.InspectChunkHeader(index); } - VTX::FileFooter GetFooter() override { - return InternalReader.GetFileFooter(); - } + FrameAccessor CreateAccessor() const override { return InternalReader.CreateAccessor(); } - VTX::ContextualSchema GetContextualSchema() override { - return InternalReader.GetContextualSchema(); - } + std::span GetRawFrameBytes(int32_t frame_index) override { + return InternalReader.GetRawFrameBytes(frame_index); + } - VTX::PropertyAddressCache GetPropertyAddressCache() override { - return InternalReader.GetPropertyAddressCache(); - } + private: + VTX::ReplayReader InternalReader; + }; - int32_t GetTotalFrames() const override { - return InternalReader.GetTotalFrames(); - } - const std::vector& GetSeekTable() const override { - return InternalReader.GetSeekTable(); - } + class ProtobuffFacadeImpl : public IVtxReaderFacade { + public: + ProtobuffFacadeImpl(const std::string& filepath) + : InternalReader(filepath) {} - void SetCacheWindow(uint32_t backward, uint32_t forward) override { - InternalReader.SetCacheWindow(backward, forward); - } + void SetEvents(const ReplayReaderEvents& events) override { InternalReader.SetEvents(events); } - void WarmAt(int32_t frame_index) override { - InternalReader.WarmAt(frame_index); - } + VTX::FileHeader GetHeader() override { return InternalReader.GetFileHeader(); } - bool GetFrame(int32_t frame_index, VTX::Frame& out_frame) override { - return InternalReader.GetFrame(frame_index, out_frame); - } + VTX::FileFooter GetFooter() override { return InternalReader.GetFileFooter(); } - const VTX::Frame* GetFrame(int32_t frame_index) override { - return InternalReader.GetFramePtr(frame_index); - } + VTX::ContextualSchema GetContextualSchema() override { return InternalReader.GetContextualSchema(); } - const VTX::Frame* GetFrameSync(int frame_index) override { - return InternalReader.GetFramePtrSync(frame_index); - } + VTX::PropertyAddressCache GetPropertyAddressCache() override { + return InternalReader.GetPropertyAddressCache(); + } - void GetFrameRange(int32_t start_frame, int32_t range, std::vector& out_frames) override { - InternalReader.GetFrameRange(start_frame, range, out_frames); - } + int32_t GetTotalFrames() const override { return InternalReader.GetTotalFrames(); } - std::vector GetFrameContext(int32_t center_frame, int32_t back_range, int32_t forward_range) override { - return InternalReader.GetFrameContext(center_frame, back_range, forward_range); - } + const std::vector& GetSeekTable() const override { return InternalReader.GetSeekTable(); } - int32_t GetChunkFrameCountSafe(int32_t chunk_index) override { - return InternalReader.GetChunkFrameCountSafe(chunk_index); - } + void SetCacheWindow(uint32_t backward, uint32_t forward) override { + InternalReader.SetCacheWindow(backward, forward); + } - void InspectChunkHeader(int32_t index) const override { - InternalReader.InspectChunkHeader(index); - } + void WarmAt(int32_t frame_index) override { InternalReader.WarmAt(frame_index); } - FrameAccessor CreateAccessor() const override { - return InternalReader.CreateAccessor(); - } + bool GetFrame(int32_t frame_index, VTX::Frame& out_frame) override { + return InternalReader.GetFrame(frame_index, out_frame); + } - std::span GetRawFrameBytes(int32_t frame_index) override { - return InternalReader.GetRawFrameBytes(frame_index); - } + const VTX::Frame* GetFrame(int32_t frame_index) override { return InternalReader.GetFramePtr(frame_index); } -private: - VTX::ReplayReader InternalReader; -}; + const VTX::Frame* GetFrameSync(int frame_index) override { return InternalReader.GetFramePtrSync(frame_index); } + void GetFrameRange(int32_t start_frame, int32_t range, std::vector& out_frames) override { + InternalReader.GetFrameRange(start_frame, range, out_frames); + } -std::unique_ptr CreateFlatBuffersFacade(const std::string& filepath) { - return std::make_unique(filepath); -} + std::vector GetFrameContext(int32_t center_frame, int32_t back_range, + int32_t forward_range) override { + return InternalReader.GetFrameContext(center_frame, back_range, forward_range); + } -std::unique_ptr CreateProtobuffFacade(const std::string& filepath) { - return std::make_unique(filepath); -} + int32_t GetChunkFrameCountSafe(int32_t chunk_index) override { + return InternalReader.GetChunkFrameCountSafe(chunk_index); + } + void InspectChunkHeader(int32_t index) const override { InternalReader.InspectChunkHeader(index); } -ReaderContext OpenReplayFile(const std::string& filepath) { - ReaderContext result; - try { - std::ifstream file(filepath, std::ios::binary | std::ios::in); - if (!file.is_open()) { - result.SetError("Failed to open file: " + filepath); - return result; - } + FrameAccessor CreateAccessor() const override { return InternalReader.CreateAccessor(); } - // Calculate file size - file.seekg(0, std::ios::end); - const auto file_size = file.tellg(); - result.size_in_mb = static_cast(file_size) / (1024.0f * 1024.0f); - file.seekg(0); - - // Read magic bytes - std::string magic(4, '\0'); - file.read(magic.data(), 4); - file.close(); - - if (magic == "VTXF") { - result.format = VtxFormat::FlatBuffers; - result.reader = std::make_unique(filepath); - } else if (magic == "VTXP") { - result.format = VtxFormat::Protobuf; - result.reader = std::make_unique(filepath); - } else { - result.SetError("Unknown file format. Magic: " + magic); - return result; + std::span GetRawFrameBytes(int32_t frame_index) override { + return InternalReader.GetRawFrameBytes(frame_index); } - if (!result.reader) { - result.SetError("Reader creation failed."); - return result; - } + private: + VTX::ReplayReader InternalReader; + }; - // Wire chunk events to the built-in state tracker - auto* cs = result.chunk_state.get(); - cs->Reset(); - ReplayReaderEvents events; - events.OnChunkLoadStarted = [cs](int32_t chunk_idx) { - cs->OnChunkLoadStarted(chunk_idx); - }; - events.OnChunkLoadFinished = [cs](int32_t chunk_idx) { - cs->OnChunkLoadFinished(chunk_idx); - }; - events.OnChunkEvicted = [cs](int32_t chunk_idx) { - cs->OnChunkEvicted(chunk_idx); - }; - result.reader->SetEvents(events); - - } catch (const std::exception& e) { - result.SetError(std::string("Error opening replay: ") + e.what()); - result.reader.reset(); - } catch (...) { - result.SetError("Unknown error while opening replay."); - result.reader.reset(); + std::unique_ptr CreateFlatBuffersFacade(const std::string& filepath) { + return std::make_unique(filepath); + } + + std::unique_ptr CreateProtobuffFacade(const std::string& filepath) { + return std::make_unique(filepath); + } + + + ReaderContext OpenReplayFile(const std::string& filepath) { + ReaderContext result; + try { + std::ifstream file(filepath, std::ios::binary | std::ios::in); + if (!file.is_open()) { + result.SetError("Failed to open file: " + filepath); + return result; + } + + // Calculate file size + file.seekg(0, std::ios::end); + const auto file_size = file.tellg(); + result.size_in_mb = static_cast(file_size) / (1024.0f * 1024.0f); + file.seekg(0); + + // Read magic bytes + std::string magic(4, '\0'); + file.read(magic.data(), 4); + file.close(); + + if (magic == "VTXF") { + result.format = VtxFormat::FlatBuffers; + result.reader = std::make_unique(filepath); + } else if (magic == "VTXP") { + result.format = VtxFormat::Protobuf; + result.reader = std::make_unique(filepath); + } else { + result.SetError("Unknown file format. Magic: " + magic); + return result; + } + + if (!result.reader) { + result.SetError("Reader creation failed."); + return result; + } + + // Wire chunk events to the built-in state tracker + auto* cs = result.chunk_state.get(); + cs->Reset(); + + ReplayReaderEvents events; + events.OnChunkLoadStarted = [cs](int32_t chunk_idx) { + cs->OnChunkLoadStarted(chunk_idx); + }; + events.OnChunkLoadFinished = [cs](int32_t chunk_idx) { + cs->OnChunkLoadFinished(chunk_idx); + }; + events.OnChunkEvicted = [cs](int32_t chunk_idx) { + cs->OnChunkEvicted(chunk_idx); + }; + result.reader->SetEvents(events); + + } catch (const std::exception& e) { + result.SetError(std::string("Error opening replay: ") + e.what()); + result.reader.reset(); + } catch (...) { + result.SetError("Unknown error while opening replay."); + result.reader.reset(); + } + return result; } - return result; -} } // namespace VTX diff --git a/sdk/src/vtx_reader/src/vtx/reader/core/vtx_schema_adapter.cpp b/sdk/src/vtx_reader/src/vtx/reader/core/vtx_schema_adapter.cpp index 84fef40..9d54644 100644 --- a/sdk/src/vtx_reader/src/vtx/reader/core/vtx_schema_adapter.cpp +++ b/sdk/src/vtx_reader/src/vtx/reader/core/vtx_schema_adapter.cpp @@ -6,8 +6,7 @@ namespace VTX { - void PopulateCacheFromJsonString(const std::string& json_str, PropertyAddressCache& cache) - { + void PopulateCacheFromJsonString(const std::string& json_str, PropertyAddressCache& cache) { cache.Clear(); if (json_str.empty()) { VTX_WARN("JSON string is empty in the .vtx file"); @@ -23,12 +22,12 @@ namespace VTX { for (const auto& [struct_name, struct_def] : temp_registry.GetDefinitions()) { int32_t type_id = temp_registry.GetStructTypeId(struct_name); if (type_id == -1) { - VTX_WARN("Struct '{}' does not have a TypeID registered in the enum.", struct_name); - continue; + VTX_WARN("Struct '{}' does not have a TypeID registered in the enum.", struct_name); + continue; } cache.name_to_id[struct_name] = type_id; - - auto& struct_cache = cache.structs[type_id]; + + auto& struct_cache = cache.structs[type_id]; struct_cache.name = struct_name; for (const auto& field : struct_def.fields) { if (field.type_id != VTX::FieldType::None) { @@ -38,7 +37,8 @@ namespace VTX { addr.container_type = field.container_type; addr.child_type_name = field.struct_type; struct_cache.properties[field.name] = addr; - struct_cache.names_by_lookup_key[VTX::MakePropertyLookupKey(field.index, field.type_id, field.container_type)] = field.name; + struct_cache.names_by_lookup_key[VTX::MakePropertyLookupKey(field.index, field.type_id, + field.container_type)] = field.name; struct_cache.property_order.push_back(field.name); } } @@ -47,17 +47,16 @@ namespace VTX { // ========================================================================================= // PROTOBUF (cppvtx::PropertySchema) // ========================================================================================= - void SchemaAdapter::BuildCache(const cppvtx::ContextualSchema& src, PropertyAddressCache& cache) - { + void SchemaAdapter::BuildCache(const cppvtx::ContextualSchema& src, + PropertyAddressCache& cache) { PopulateCacheFromJsonString(src.schema(), cache); } // ========================================================================================= // FLATBUFFERS (fbsvtx::PropertySchemaT) // ========================================================================================= - void SchemaAdapter::BuildCache(const fbsvtx::ContextualSchemaT& src, PropertyAddressCache& cache) - { + void SchemaAdapter::BuildCache(const fbsvtx::ContextualSchemaT& src, + PropertyAddressCache& cache) { PopulateCacheFromJsonString(src.schema, cache); - } -} +} // namespace VTX diff --git a/sdk/src/vtx_reader/src/vtx/reader/formatters/flatbuffer_reader_policy.cpp b/sdk/src/vtx_reader/src/vtx/reader/formatters/flatbuffer_reader_policy.cpp index ffb7697..c0879d3 100644 --- a/sdk/src/vtx_reader/src/vtx/reader/formatters/flatbuffer_reader_policy.cpp +++ b/sdk/src/vtx_reader/src/vtx/reader/formatters/flatbuffer_reader_policy.cpp @@ -19,7 +19,7 @@ VTX::FlatBuffersReaderPolicy::HeaderType VTX::FlatBuffersReaderPolicy::ParseHead } std::string VTX::FlatBuffersReaderPolicy::GetMagicBytes() { - return "VTXF"; + return "VTXF"; } VTX::FlatBuffersReaderPolicy::FooterType VTX::FlatBuffersReaderPolicy::ParseFooter(const std::string& buffer) { @@ -31,35 +31,38 @@ VTX::FlatBuffersReaderPolicy::FooterType VTX::FlatBuffersReaderPolicy::ParseFoot return footer; } -void VTX::FlatBuffersReaderPolicy::ProcessChunkData(int chunk_index,const std::string& compressed, std::stop_token st,std::vector& out_native_frames,std::vector& out_decompressed_blob,std::vector>& out_raw_frames_spans) - { +void VTX::FlatBuffersReaderPolicy::ProcessChunkData(int chunk_index, const std::string& compressed, std::stop_token st, + std::vector& out_native_frames, + std::vector& out_decompressed_blob, + std::vector>& out_raw_frames_spans) { std::string decompressed = VTX::ReplayUnpacker::Decompress(compressed); auto* chunk = flatbuffers::GetRoot(decompressed.data()); - - if (!chunk || !chunk->frames()) return; + + if (!chunk || !chunk->frames()) + return; size_t num_frames = chunk->frames()->size(); out_native_frames.resize(num_frames); out_raw_frames_spans.reserve(num_frames); - + flatbuffers::FlatBufferBuilder fbb; std::vector frame_sizes; frame_sizes.reserve(num_frames); //serialize and concatenate frmaes in a single block for (size_t i = 0; i < num_frames; ++i) { - if (st.stop_requested()) return; + if (st.stop_requested()) + return; VTX::Serialization::FromFlat(chunk->frames()->Get(i), out_native_frames[i]); - + //pack for differ auto* data_obj = chunk->frames()->Get(i)->data()->Get(0); std::unique_ptr dataT(data_obj->UnPack()); - + fbb.Clear(); fbb.Finish(fbsvtx::Bucket::Pack(fbb, dataT.get())); - - out_decompressed_blob.insert(out_decompressed_blob.end(), - fbb.GetBufferPointer(), + + out_decompressed_blob.insert(out_decompressed_blob.end(), fbb.GetBufferPointer(), fbb.GetBufferPointer() + fbb.GetSize()); frame_sizes.push_back(fbb.GetSize()); } @@ -67,34 +70,33 @@ void VTX::FlatBuffersReaderPolicy::ProcessChunkData(int chunk_index,const std::s //genrate spans for each frame, zero copy size_t offset = 0; for (size_t size : frame_sizes) { - out_raw_frames_spans.emplace_back( - reinterpret_cast(out_decompressed_blob.data() + offset), - size - ); + out_raw_frames_spans.emplace_back(reinterpret_cast(out_decompressed_blob.data() + offset), + size); offset += size; } } -void VTX::FlatBuffersReaderPolicy::PopulateIndexTable(const FooterType& footer,std::vector& chunk_index_table) -{ +void VTX::FlatBuffersReaderPolicy::PopulateIndexTable(const FooterType& footer, + std::vector& chunk_index_table) { chunk_index_table.reserve(footer.chunk_index.size()); - + for (const auto& e : footer.chunk_index) { - if(!e) continue; + if (!e) + continue; ChunkIndexEntry ce; - ce.chunk_index = e->chunk_index; - ce.start_frame = e->start_frame; - ce.end_frame = e->end_frame; - ce.file_offset = e->file_offset; + ce.chunk_index = e->chunk_index; + ce.start_frame = e->start_frame; + ce.end_frame = e->end_frame; + ce.file_offset = e->file_offset; ce.chunk_size_bytes = e->chunk_size_bytes; - + chunk_index_table.push_back(ce); } } -void VTX::FlatBuffersReaderPolicy::PopulateGameTimes(const FooterType& footer, VTX::GameTime::VTXGameTimes& game_times) -{ +void VTX::FlatBuffersReaderPolicy::PopulateGameTimes(const FooterType& footer, + VTX::GameTime::VTXGameTimes& game_times) { if (footer.times) { const auto& t = *footer.times; game_times.SetGameTime(t.game_time); @@ -112,12 +114,11 @@ const VTX::FlatBuffersReaderPolicy::SchemaType& VTX::FlatBuffersReaderPolicy::Ge if (header.contextual_schema) { return *header.contextual_schema; } - static SchemaType dummy; - return dummy; + static SchemaType dummy; + return dummy; } - VTX::ContextualSchema VTX::FlatBuffersReaderPolicy::GetVTXContextualSchema(const HeaderType& header) -{ +VTX::ContextualSchema VTX::FlatBuffersReaderPolicy::GetVTXContextualSchema(const HeaderType& header) { VTX::ContextualSchema contextual_schema; contextual_schema.data_identifier = header.contextual_schema->data_identifier; contextual_schema.data_version = header.contextual_schema->data_version; @@ -126,14 +127,13 @@ const VTX::FlatBuffersReaderPolicy::SchemaType& VTX::FlatBuffersReaderPolicy::Ge return contextual_schema; } -VTX::FileHeader VTX::FlatBuffersReaderPolicy::GetVTXHeader(const HeaderType& fbs_header) -{ +VTX::FileHeader VTX::FlatBuffersReaderPolicy::GetVTXHeader(const HeaderType& fbs_header) { VTX::FileHeader out; out.replay_name = fbs_header.replay_name; out.replay_uuid = fbs_header.replay_uuid; out.recorded_utc_timestamp = fbs_header.recorded_utc_timestamp; out.custom_json_metadata = fbs_header.custom_json_metadata; - + if (fbs_header.version) { out.version.format_major = fbs_header.version->format_major; out.version.format_minor = fbs_header.version->format_minor; @@ -142,13 +142,12 @@ VTX::FileHeader VTX::FlatBuffersReaderPolicy::GetVTXHeader(const HeaderType& fbs return out; } -VTX::FileFooter VTX::FlatBuffersReaderPolicy::GetVTXFooter(const FooterType& fbs_footer) -{ +VTX::FileFooter VTX::FlatBuffersReaderPolicy::GetVTXFooter(const FooterType& fbs_footer) { VTX::FileFooter out; out.total_frames = fbs_footer.total_frames; out.duration_seconds = fbs_footer.duration_seconds; out.payload_checksum = fbs_footer.payload_checksum; - + out.chunk_index.reserve(fbs_footer.chunk_index.size()); for (const auto& chunk : fbs_footer.chunk_index) { if (chunk) { @@ -157,12 +156,12 @@ VTX::FileFooter VTX::FlatBuffersReaderPolicy::GetVTXFooter(const FooterType& fbs entry.chunk_size_bytes = chunk->chunk_size_bytes; entry.start_frame = chunk->start_frame; entry.end_frame = chunk->end_frame; - + entry.file_offset = chunk->file_offset; out.chunk_index.push_back(entry); } } - + out.events.reserve(fbs_footer.events.size()); for (const auto& ev : fbs_footer.events) { if (ev) { @@ -173,13 +172,13 @@ VTX::FileFooter VTX::FlatBuffersReaderPolicy::GetVTXFooter(const FooterType& fbs out.events.push_back(out_ev); } } - + if (fbs_footer.times) { const auto& fbs_times = fbs_footer.times; out.times.game_time.assign(fbs_times->game_time.begin(), fbs_times->game_time.end()); out.times.created_utc.assign(fbs_times->created_utc.begin(), fbs_times->created_utc.end()); out.times.gaps.assign(fbs_times->gaps.begin(), fbs_times->gaps.end()); out.times.segments.assign(fbs_times->segments.begin(), fbs_times->segments.end()); - } + } return out; } diff --git a/sdk/src/vtx_reader/src/vtx/reader/formatters/protobuff_reader_policy.cpp b/sdk/src/vtx_reader/src/vtx/reader/formatters/protobuff_reader_policy.cpp index 780b45f..2af17ee 100644 --- a/sdk/src/vtx_reader/src/vtx/reader/formatters/protobuff_reader_policy.cpp +++ b/sdk/src/vtx_reader/src/vtx/reader/formatters/protobuff_reader_policy.cpp @@ -18,7 +18,7 @@ VTX::ProtobufReaderPolicy::HeaderType VTX::ProtobufReaderPolicy::ParseHeader(con return header; } - VTX::ProtobufReaderPolicy::FooterType VTX::ProtobufReaderPolicy::ParseFooter(const std::string& buffer) { +VTX::ProtobufReaderPolicy::FooterType VTX::ProtobufReaderPolicy::ParseFooter(const std::string& buffer) { FooterType footer; if (!footer.ParseFromString(buffer)) { throw std::runtime_error("VTX Reader [Proto]: Failed to parse FileFooter."); @@ -26,8 +26,10 @@ VTX::ProtobufReaderPolicy::HeaderType VTX::ProtobufReaderPolicy::ParseHeader(con return footer; } -void VTX::ProtobufReaderPolicy::ProcessChunkData(int chunk_index,const std::string& compressed, std::stop_token st,std::vector& out_native_frames,std::vector& out_decompressed_blob,std::vector>& out_raw_frames_spans) - { +void VTX::ProtobufReaderPolicy::ProcessChunkData(int chunk_index, const std::string& compressed, std::stop_token st, + std::vector& out_native_frames, + std::vector& out_decompressed_blob, + std::vector>& out_raw_frames_spans) { std::string decompressed = VTX::ReplayUnpacker::Decompress(compressed); cppvtx::FrameChunk proto; @@ -39,7 +41,7 @@ void VTX::ProtobufReaderPolicy::ProcessChunkData(int chunk_index,const std::stri //precalculate data size size_t total_raw_size = 0; - for(int i = 0; i < num_frames; ++i) { + for (int i = 0; i < num_frames; ++i) { total_raw_size += proto.frames(i).data(0).ByteSizeLong(); } out_decompressed_blob.reserve(total_raw_size); @@ -48,8 +50,9 @@ void VTX::ProtobufReaderPolicy::ProcessChunkData(int chunk_index,const std::stri frame_sizes.reserve(num_frames); //serialize and concatenate frmaes in a single block - for(int i = 0; i < num_frames; ++i) { - if (st.stop_requested()) return; + for (int i = 0; i < num_frames; ++i) { + if (st.stop_requested()) + return; Serialization::FromProto(proto.frames(i), out_native_frames[i]); // Eextract root for the differ @@ -62,32 +65,30 @@ void VTX::ProtobufReaderPolicy::ProcessChunkData(int chunk_index,const std::stri // done here because if we do it before , if vector grows pointers are invalid size_t offset = 0; for (size_t size : frame_sizes) { - out_raw_frames_spans.emplace_back( - reinterpret_cast(out_decompressed_blob.data() + offset), - size - ); + out_raw_frames_spans.emplace_back(reinterpret_cast(out_decompressed_blob.data() + offset), + size); offset += size; } } -void VTX::ProtobufReaderPolicy::PopulateIndexTable(const FooterType& footer,std::vector& chunk_index_table) -{ +void VTX::ProtobufReaderPolicy::PopulateIndexTable(const FooterType& footer, + std::vector& chunk_index_table) { chunk_index_table.reserve(footer.chunk_index_size()); for (const auto& protoEntry : footer.chunk_index()) { ChunkIndexEntry nativeEntry; - nativeEntry.chunk_index = protoEntry.chunk_index(); - nativeEntry.start_frame = protoEntry.start_frame(); - nativeEntry.end_frame = protoEntry.end_frame(); - nativeEntry.file_offset = protoEntry.file_offset(); + nativeEntry.chunk_index = protoEntry.chunk_index(); + nativeEntry.start_frame = protoEntry.start_frame(); + nativeEntry.end_frame = protoEntry.end_frame(); + nativeEntry.file_offset = protoEntry.file_offset(); nativeEntry.chunk_size_bytes = protoEntry.chunk_size_bytes(); chunk_index_table.push_back(nativeEntry); } } -void VTX::ProtobufReaderPolicy::PopulateGameTimes(const VTX::ProtobufReaderPolicy::FooterType& footer, VTX::GameTime::VTXGameTimes& game_times) -{ +void VTX::ProtobufReaderPolicy::PopulateGameTimes(const VTX::ProtobufReaderPolicy::FooterType& footer, + VTX::GameTime::VTXGameTimes& game_times) { const auto& times_proto = footer.times(); game_times.SetGameTime({times_proto.game_time().begin(), times_proto.game_time().end()}); game_times.SetCreatedUtc({times_proto.created_utc().begin(), times_proto.created_utc().end()}); @@ -103,8 +104,7 @@ const VTX::ProtobufReaderPolicy::SchemaType& VTX::ProtobufReaderPolicy::GetSchem return header.prop_schema(); } -VTX::ContextualSchema VTX::ProtobufReaderPolicy::GetVTXContextualSchema(const HeaderType& header) -{ +VTX::ContextualSchema VTX::ProtobufReaderPolicy::GetVTXContextualSchema(const HeaderType& header) { VTX::ContextualSchema contextual_schema; contextual_schema.data_identifier = header.prop_schema().data_indentifier(); contextual_schema.data_version = header.prop_schema().data_version(); @@ -113,8 +113,7 @@ VTX::ContextualSchema VTX::ProtobufReaderPolicy::GetVTXContextualSchema(const He return contextual_schema; } -VTX::FileHeader VTX::ProtobufReaderPolicy::GetVTXHeader(const HeaderType& pb_header) -{ +VTX::FileHeader VTX::ProtobufReaderPolicy::GetVTXHeader(const HeaderType& pb_header) { VTX::FileHeader out; out.replay_name = pb_header.replay_name(); out.replay_uuid = pb_header.replay_uuid(); @@ -130,8 +129,7 @@ VTX::FileHeader VTX::ProtobufReaderPolicy::GetVTXHeader(const HeaderType& pb_hea return out; } -VTX::FileFooter VTX::ProtobufReaderPolicy::GetVTXFooter(const FooterType& pb_footer) -{ +VTX::FileFooter VTX::ProtobufReaderPolicy::GetVTXFooter(const FooterType& pb_footer) { VTX::FileFooter out; out.total_frames = pb_footer.total_frames(); out.duration_seconds = pb_footer.duration_seconds(); diff --git a/sdk/src/vtx_reader/src/vtx/reader/serialization/flatbuffers_to_vtx.cpp b/sdk/src/vtx_reader/src/vtx/reader/serialization/flatbuffers_to_vtx.cpp index a018765..42b6b53 100644 --- a/sdk/src/vtx_reader/src/vtx/reader/serialization/flatbuffers_to_vtx.cpp +++ b/sdk/src/vtx_reader/src/vtx/reader/serialization/flatbuffers_to_vtx.cpp @@ -5,41 +5,47 @@ // --- MATH TYPES --- void VTX::Serialization::FromFlat(const fbsvtx::Vector* src, VTX::Vector& dst) { - if (!src) return; - dst.x = src->x(); - dst.y = src->y(); + if (!src) + return; + dst.x = src->x(); + dst.y = src->y(); dst.z = src->z(); } void VTX::Serialization::FromFlat(const fbsvtx::Quat* src, VTX::Quat& dst) { - if (!src) return; - dst.x = src->x(); - dst.y = src->y(); - dst.z = src->z(); + if (!src) + return; + dst.x = src->x(); + dst.y = src->y(); + dst.z = src->z(); dst.w = src->w(); } void VTX::Serialization::FromFlat(const fbsvtx::Transform* src, VTX::Transform& dst) { - if (!src) return; - FromFlat(&src->translation(), dst.translation); + if (!src) + return; + FromFlat(&src->translation(), dst.translation); FromFlat(&src->rotation(), dst.rotation); FromFlat(&src->scale(), dst.scale); } void VTX::Serialization::FromFlat(const fbsvtx::FloatRange* src, VTX::FloatRange& dst) { - if (!src) return; - dst.min = src->min(); - dst.max = src->max(); + if (!src) + return; + dst.min = src->min(); + dst.max = src->max(); dst.value_normalized = src->value_normalized(); } void VTX::Serialization::FromFlat(const fbsvtx::MapContainer* src, VTX::MapContainer& dst) { - if (!src) return; + if (!src) + return; if (src->keys()) { dst.keys.reserve(src->keys()->size()); - for (auto k : *src->keys()) dst.keys.push_back(k->str()); + for (auto k : *src->keys()) + dst.keys.push_back(k->str()); } - + if (src->values()) { dst.values.resize(src->values()->size()); for (size_t i = 0; i < src->values()->size(); ++i) { @@ -50,10 +56,11 @@ void VTX::Serialization::FromFlat(const fbsvtx::MapContainer* src, VTX::MapConta // --- PROPERTY CONTAINER --- void VTX::Serialization::FromFlat(const fbsvtx::PropertyContainer* src, VTX::PropertyContainer& dst) { - if (!src) return; + if (!src) + return; dst.entity_type_id = src->type_id(); dst.content_hash = src->content_hash(); - + // 1. Scalars if (auto v = src->bool_properties()) { dst.bool_properties.clear(); @@ -62,66 +69,80 @@ void VTX::Serialization::FromFlat(const fbsvtx::PropertyContainer* src, VTX::Pro dst.bool_properties.push_back(b != 0); } } - - if (auto v = src->int32_properties()) dst.int32_properties.assign(v->begin(), v->end()); - if (auto v = src->int64_properties()) dst.int64_properties.assign(v->begin(), v->end()); - if (auto v = src->float_properties()) dst.float_properties.assign(v->begin(), v->end()); - if (auto v = src->double_properties()) dst.double_properties.assign(v->begin(), v->end()); - + + if (auto v = src->int32_properties()) + dst.int32_properties.assign(v->begin(), v->end()); + if (auto v = src->int64_properties()) + dst.int64_properties.assign(v->begin(), v->end()); + if (auto v = src->float_properties()) + dst.float_properties.assign(v->begin(), v->end()); + if (auto v = src->double_properties()) + dst.double_properties.assign(v->begin(), v->end()); + if (auto v = src->string_properties()) { dst.string_properties.reserve(v->size()); - for(auto s : *v) dst.string_properties.push_back(s->str()); + for (auto s : *v) + dst.string_properties.push_back(s->str()); } // 2. Structs (Vector of Structs) auto unpackStructs = [](auto* fbsVec, auto& nativeVec) { - if (!fbsVec) return; + if (!fbsVec) + return; nativeVec.resize(fbsVec->size()); - for (size_t i = 0; i < fbsVec->size(); ++i) FromFlat(fbsVec->Get(i), nativeVec[i]); + for (size_t i = 0; i < fbsVec->size(); ++i) + FromFlat(fbsVec->Get(i), nativeVec[i]); }; - unpackStructs(src->vector_properties(), dst.vector_properties); - unpackStructs(src->quat_properties(), dst.quat_properties); + unpackStructs(src->vector_properties(), dst.vector_properties); + unpackStructs(src->quat_properties(), dst.quat_properties); unpackStructs(src->transform_properties(), dst.transform_properties); - unpackStructs(src->range_properties(), dst.range_properties); + unpackStructs(src->range_properties(), dst.range_properties); // 3. Flat Arrays (SoA) auto unpackArray = [](auto* fbsArr, auto& nativeArr) { - if (!fbsArr) return; - if (fbsArr->data()) nativeArr.data.assign(fbsArr->data()->begin(), fbsArr->data()->end()); - if (fbsArr->offsets()) nativeArr.offsets.assign(fbsArr->offsets()->begin(), fbsArr->offsets()->end()); + if (!fbsArr) + return; + if (fbsArr->data()) + nativeArr.data.assign(fbsArr->data()->begin(), fbsArr->data()->end()); + if (fbsArr->offsets()) + nativeArr.offsets.assign(fbsArr->offsets()->begin(), fbsArr->offsets()->end()); }; // Special handling for BoolArray if it uses uint8 internally in FBS if (auto arr = src->bool_arrays()) { if (arr->data()) { // Conversion implicit uint8 -> bool - dst.bool_arrays.data.assign(arr->data()->begin(), arr->data()->end()); + dst.bool_arrays.data.assign(arr->data()->begin(), arr->data()->end()); } - if (arr->offsets()) dst.bool_arrays.offsets.assign(arr->offsets()->begin(), arr->offsets()->end()); + if (arr->offsets()) + dst.bool_arrays.offsets.assign(arr->offsets()->begin(), arr->offsets()->end()); } unpackArray(src->byte_array_properties(), dst.byte_array_properties); - unpackArray(src->int32_arrays(), dst.int32_arrays); - unpackArray(src->int64_arrays(), dst.int64_arrays); - unpackArray(src->float_arrays(), dst.float_arrays); - unpackArray(src->double_arrays(), dst.double_arrays); - + unpackArray(src->int32_arrays(), dst.int32_arrays); + unpackArray(src->int64_arrays(), dst.int64_arrays); + unpackArray(src->float_arrays(), dst.float_arrays); + unpackArray(src->double_arrays(), dst.double_arrays); + // String Array if (auto arr = src->string_arrays()) { if (auto d = arr->data()) { dst.string_arrays.data.reserve(d->size()); - for(auto s : *d) dst.string_arrays.data.push_back(s->str()); + for (auto s : *d) + dst.string_arrays.data.push_back(s->str()); } - if (auto o = arr->offsets()) dst.string_arrays.offsets.assign(o->begin(), o->end()); + if (auto o = arr->offsets()) + dst.string_arrays.offsets.assign(o->begin(), o->end()); } // Struct Arrays (Needs conversion per element) auto unpackStructArray = [](auto* fbsArr, auto& nativeArr) { - if (!fbsArr) return; + if (!fbsArr) + return; if (auto d = fbsArr->data()) { nativeArr.data.resize(d->size()); - for(size_t i = 0; i < d->size(); ++i) { + for (size_t i = 0; i < d->size(); ++i) { FromFlat(d->Get(i), nativeArr.data[i]); } } @@ -135,125 +156,125 @@ void VTX::Serialization::FromFlat(const fbsvtx::PropertyContainer* src, VTX::Pro // 4. Recursive if (auto v = src->any_struct_properties()) { dst.any_struct_properties.resize(v->size()); - for(size_t i = 0; i < v->size(); ++i) FromFlat(v->Get(i), dst.any_struct_properties[i]); + for (size_t i = 0; i < v->size(); ++i) + FromFlat(v->Get(i), dst.any_struct_properties[i]); } if (auto arr = src->any_struct_arrays()) { if (auto d = arr->data()) { dst.any_struct_arrays.data.resize(d->size()); - for(size_t i = 0; i < d->size(); ++i) FromFlat(d->Get(i), dst.any_struct_arrays.data[i]); + for (size_t i = 0; i < d->size(); ++i) + FromFlat(d->Get(i), dst.any_struct_arrays.data[i]); } - if (auto o = arr->offsets()) dst.any_struct_arrays.offsets.assign(o->begin(), o->end()); + if (auto o = arr->offsets()) + dst.any_struct_arrays.offsets.assign(o->begin(), o->end()); } // 5. Maps if (auto v = src->map_properties()) { dst.map_properties.resize(v->size()); - for(size_t i = 0; i < v->size(); ++i) FromFlat(v->Get(i), dst.map_properties[i]); + for (size_t i = 0; i < v->size(); ++i) + FromFlat(v->Get(i), dst.map_properties[i]); } - + if (auto arr = src->map_arrays()) { - if (auto d = arr->data()) { + if (auto d = arr->data()) { dst.map_arrays.data.resize(d->size()); - for(size_t i = 0; i < d->size(); ++i) FromFlat(d->Get(i), dst.map_arrays.data[i]); + for (size_t i = 0; i < d->size(); ++i) + FromFlat(d->Get(i), dst.map_arrays.data[i]); } - if (auto o = arr->offsets()) dst.map_arrays.offsets.assign(o->begin(), o->end()); + if (auto o = arr->offsets()) + dst.map_arrays.offsets.assign(o->begin(), o->end()); } } // --- DATA & FRAME --- void VTX::Serialization::FromFlat(const fbsvtx::Bucket* src, VTX::Bucket& dst) { - if(!src) return; - if(src->unique_ids()) { + if (!src) + return; + if (src->unique_ids()) { dst.unique_ids.reserve(src->unique_ids()->size()); - for(auto s : *src->unique_ids()) dst.unique_ids.push_back(s->str()); + for (auto s : *src->unique_ids()) + dst.unique_ids.push_back(s->str()); } - if(src->entities()) { + if (src->entities()) { dst.entities.resize(src->entities()->size()); - for(size_t i = 0; i < src->entities()->size(); ++i) FromFlat(src->entities()->Get(i), dst.entities[i]); + for (size_t i = 0; i < src->entities()->size(); ++i) + FromFlat(src->entities()->Get(i), dst.entities[i]); } - + if (src->type_ranges()) { const auto* ranges = src->type_ranges(); dst.type_ranges.reserve(ranges->size()); for (size_t i = 0; i < ranges->size(); ++i) { const auto* r = ranges->Get(i); - dst.type_ranges.push_back(VTX::EntityRange{ - r->start_index(), - r->count() - }); + dst.type_ranges.push_back(VTX::EntityRange {r->start_index(), r->count()}); } } } void VTX::Serialization::FromFlat(const fbsvtx::Frame* src, VTX::Frame& dst) { - if(!src || !src->data()) return; + if (!src || !src->data()) + return; auto& buckets = dst.GetMutableBuckets(); buckets.resize(src->data()->size()); - for(size_t i = 0; i < src->data()->size(); ++i) { + for (size_t i = 0; i < src->data()->size(); ++i) { FromFlat(src->data()->Get(i), buckets[i]); } } -void VTX::Serialization::FromFlat(const fbsvtx::FileFooter* src, VTX::FileFooter& dst) -{ +void VTX::Serialization::FromFlat(const fbsvtx::FileFooter* src, VTX::FileFooter& dst) { dst.total_frames = src->total_frames(); dst.duration_seconds = src->duration_seconds(); - - FromFlat(src->times(),dst.times); - + + FromFlat(src->times(), dst.times); + dst.chunk_index.reserve(src->chunk_index()->size()); - for(size_t i = 0; i < src->chunk_index()->size(); ++i) { - FromFlat(src->chunk_index()->Get(i), dst.chunk_index[i]); + for (size_t i = 0; i < src->chunk_index()->size(); ++i) { + FromFlat(src->chunk_index()->Get(i), dst.chunk_index[i]); } - + dst.events.reserve(src->events()->size()); - for(size_t i = 0; i < src->events()->size(); ++i) { - FromFlat(src->events()->Get(i), dst.events[i]); + for (size_t i = 0; i < src->events()->size(); ++i) { + FromFlat(src->events()->Get(i), dst.events[i]); } - + dst.payload_checksum = src->payload_checksum(); } -void VTX::Serialization::FromFlat(const fbsvtx::FileHeader* src, VTX::FileHeader& dst) -{ -} +void VTX::Serialization::FromFlat(const fbsvtx::FileHeader* src, VTX::FileHeader& dst) {} -void VTX::Serialization::FromFlat(const fbsvtx::ChunkIndexEntry* src, VTX::ChunkIndexEntry& dst) -{ +void VTX::Serialization::FromFlat(const fbsvtx::ChunkIndexEntry* src, VTX::ChunkIndexEntry& dst) { dst.chunk_index = src->chunk_index(); dst.start_frame = src->start_frame(); dst.end_frame = src->end_frame(); dst.file_offset = src->file_offset(); dst.chunk_size_bytes = src->chunk_size_bytes(); - } -void VTX::Serialization::FromFlat(const fbsvtx::ReplayTimeData* src, VTX::ReplayTimeData& dst) -{ - for(size_t i = 0; i < src->game_time()->size(); ++i) { +void VTX::Serialization::FromFlat(const fbsvtx::ReplayTimeData* src, VTX::ReplayTimeData& dst) { + for (size_t i = 0; i < src->game_time()->size(); ++i) { dst.game_time.push_back(src->game_time()->Get(i)); } - - for(size_t i = 0; i < src->created_utc()->size(); ++i) { + + for (size_t i = 0; i < src->created_utc()->size(); ++i) { dst.created_utc.push_back(src->created_utc()->Get(i)); } - - for(size_t i = 0; i < src->gaps()->size(); ++i) { + + for (size_t i = 0; i < src->gaps()->size(); ++i) { dst.gaps.push_back(src->gaps()->Get(i)); } - - for(size_t i = 0; i < src->segments()->size(); ++i) { + + for (size_t i = 0; i < src->segments()->size(); ++i) { dst.segments.push_back(src->segments()->Get(i)); } } -void VTX::Serialization::FromFlat(const fbsvtx::TimelineEvent* src, VTX::TimelineEvent& dst) -{ +void VTX::Serialization::FromFlat(const fbsvtx::TimelineEvent* src, VTX::TimelineEvent& dst) { dst.game_time = src->game_time(); dst.event_type = *src->event_type(); dst.label = *src->label(); - FromFlat(src->location(),dst.location); + FromFlat(src->location(), dst.location); dst.entity_unique_id = *src->entity_unique_id(); } diff --git a/sdk/src/vtx_reader/src/vtx/reader/serialization/proto_to_vtx.cpp b/sdk/src/vtx_reader/src/vtx/reader/serialization/proto_to_vtx.cpp index 1f1c995..3ec4109 100644 --- a/sdk/src/vtx_reader/src/vtx/reader/serialization/proto_to_vtx.cpp +++ b/sdk/src/vtx_reader/src/vtx/reader/serialization/proto_to_vtx.cpp @@ -3,22 +3,23 @@ #include "vtx_schema.pb.h" namespace VTX { namespace Serialization { - - template + + template void FromProtoVec(const TProtoArr& src, TNativeArr& dst) { dst.data.clear(); dst.offsets.clear(); - + dst.data.reserve(src.data_size()); dst.offsets.reserve(src.offsets_size()); for (const auto& v : src.data()) { - dst.data.push_back(v); + dst.data.push_back(v); } - for (auto o : src.offsets()) dst.offsets.push_back(o); + for (auto o : src.offsets()) + dst.offsets.push_back(o); } - template + template void FromProtoComplexVec(const TProtoArr& src, TNativeArr& dst, TFunc unpackFunc) { dst.data.resize(src.data_size()); for (int i = 0; i < src.data_size(); ++i) { @@ -26,7 +27,7 @@ namespace VTX { } dst.offsets.assign(src.offsets().begin(), src.offsets().end()); } - + void FromProto(const cppvtx::Vector& src, VTX::Vector& dst) { dst.x = src.x(); @@ -60,11 +61,11 @@ namespace VTX { out.entity_type_id = proto.type_id(); out.content_hash = proto.content_hash(); - - Copy(out.bool_properties, proto.bool_properties()); - Copy(out.int32_properties, proto.int32_properties()); - Copy(out.int64_properties, proto.int64_properties()); - Copy(out.float_properties, proto.float_properties()); + + Copy(out.bool_properties, proto.bool_properties()); + Copy(out.int32_properties, proto.int32_properties()); + Copy(out.int64_properties, proto.int64_properties()); + Copy(out.float_properties, proto.float_properties()); Copy(out.double_properties, proto.double_properties()); Copy(out.string_properties, proto.string_properties()); @@ -75,11 +76,11 @@ namespace VTX { } }; - Unpack(proto.vector_properties(), out.vector_properties); - Unpack(proto.quat_properties(), out.quat_properties); + Unpack(proto.vector_properties(), out.vector_properties); + Unpack(proto.quat_properties(), out.quat_properties); Unpack(proto.transform_properties(), out.transform_properties); - Unpack(proto.range_properties(), out.range_properties); - + Unpack(proto.range_properties(), out.range_properties); + if (proto.has_byte_array_properties()) { const auto& proto_bytes = proto.byte_array_properties(); auto& native_bytes = out.byte_array_properties; @@ -96,50 +97,60 @@ namespace VTX { native_bytes.offsets.push_back(o); } } - - if (proto.has_int32_arrays()) FromProtoVec(proto.int32_arrays(), out.int32_arrays); - if (proto.has_int64_arrays()) FromProtoVec(proto.int64_arrays(), out.int64_arrays); - if (proto.has_float_arrays()) FromProtoVec(proto.float_arrays(), out.float_arrays); - if (proto.has_double_arrays()) FromProtoVec(proto.double_arrays(), out.double_arrays); - if (proto.has_bool_arrays()) FromProtoVec(proto.bool_arrays(), out.bool_arrays); - if (proto.has_string_arrays()) FromProtoVec(proto.string_arrays(), out.string_arrays); - - if (proto.has_vector_arrays()) FromProtoComplexVec(proto.vector_arrays(), out.vector_arrays, [](auto& p, auto& n){ FromProto(p, n); }); - if (proto.has_quat_arrays()) FromProtoComplexVec(proto.quat_arrays(), out.quat_arrays, [](auto& p, auto& n){ FromProto(p, n); }); - if (proto.has_transform_arrays()) FromProtoComplexVec(proto.transform_arrays(), out.transform_arrays, [](auto& p, auto& n){ FromProto(p, n); }); - if (proto.has_range_arrays()) FromProtoComplexVec(proto.range_arrays(), out.range_arrays, [](auto& p, auto& n){ FromProto(p, n); }); + + if (proto.has_int32_arrays()) + FromProtoVec(proto.int32_arrays(), out.int32_arrays); + if (proto.has_int64_arrays()) + FromProtoVec(proto.int64_arrays(), out.int64_arrays); + if (proto.has_float_arrays()) + FromProtoVec(proto.float_arrays(), out.float_arrays); + if (proto.has_double_arrays()) + FromProtoVec(proto.double_arrays(), out.double_arrays); + if (proto.has_bool_arrays()) + FromProtoVec(proto.bool_arrays(), out.bool_arrays); + if (proto.has_string_arrays()) + FromProtoVec(proto.string_arrays(), out.string_arrays); + + if (proto.has_vector_arrays()) + FromProtoComplexVec(proto.vector_arrays(), out.vector_arrays, + [](auto& p, auto& n) { FromProto(p, n); }); + if (proto.has_quat_arrays()) + FromProtoComplexVec(proto.quat_arrays(), out.quat_arrays, [](auto& p, auto& n) { FromProto(p, n); }); + if (proto.has_transform_arrays()) + FromProtoComplexVec(proto.transform_arrays(), out.transform_arrays, + [](auto& p, auto& n) { FromProto(p, n); }); + if (proto.has_range_arrays()) + FromProtoComplexVec(proto.range_arrays(), out.range_arrays, [](auto& p, auto& n) { FromProto(p, n); }); out.any_struct_properties.resize(proto.any_struct_properties_size()); - for(int i = 0; i < proto.any_struct_properties_size(); ++i) { + for (int i = 0; i < proto.any_struct_properties_size(); ++i) { FromProto(proto.any_struct_properties(i), out.any_struct_properties[i]); } if (proto.has_any_struct_arrays()) { - FromProtoComplexVec(proto.any_struct_arrays(), out.any_struct_arrays, [](auto& p, auto& n){ FromProto(p, n); }); + FromProtoComplexVec(proto.any_struct_arrays(), out.any_struct_arrays, + [](auto& p, auto& n) { FromProto(p, n); }); } } void FromProto(const cppvtx::Bucket& proto, VTX::Bucket& out) { out.unique_ids.clear(); out.unique_ids.reserve(proto.unique_ids_size()); - for(const auto& id : proto.unique_ids()) { + for (const auto& id : proto.unique_ids()) { out.unique_ids.push_back(id); } - + out.entities.clear(); - out.entities.resize(proto.entities_size()); - + out.entities.resize(proto.entities_size()); + for (int i = 0; i < proto.entities_size(); ++i) { FromProto(proto.entities(i), out.entities[i]); } - + out.type_ranges.reserve(proto.type_ranges_size()); for (int i = 0; i < proto.type_ranges_size(); ++i) { const auto& r = proto.type_ranges(i); - out.type_ranges.push_back(VTX::EntityRange{ - r.start_index(), - r.count() - }); + out.type_ranges.push_back(VTX::EntityRange {r.start_index(), r.count()}); } } @@ -150,5 +161,5 @@ namespace VTX { FromProto(proto.data(i), buckets[i]); } } - } -} \ No newline at end of file + } // namespace Serialization +} // namespace VTX \ No newline at end of file diff --git a/sdk/src/vtx_writer/src/vtx/writer/core/vtx_record_pipeline.cpp b/sdk/src/vtx_writer/src/vtx/writer/core/vtx_record_pipeline.cpp index a4abc6e..82bf154 100644 --- a/sdk/src/vtx_writer/src/vtx/writer/core/vtx_record_pipeline.cpp +++ b/sdk/src/vtx_writer/src/vtx/writer/core/vtx_record_pipeline.cpp @@ -1,12 +1,12 @@ #include "vtx/writer/core/vtx_record_pipeline.h" -VTX::RecordPipeline::RecordPipeline(std::unique_ptr source,std::unique_ptr writer) -: source_(std::move(source)), writer_(std::move(writer)) -{ -} +VTX::RecordPipeline::RecordPipeline(std::unique_ptr source, std::unique_ptr writer) + : source_(std::move(source)) + , writer_(std::move(writer)) {} -bool VTX::RecordPipeline::Run(std::function on_progress ) { - if (!source_ || !writer_) return false; +bool VTX::RecordPipeline::Run(std::function on_progress) { + if (!source_ || !writer_) + return false; if (!source_->Initialize()) { return false; @@ -21,7 +21,6 @@ bool VTX::RecordPipeline::Run(std::function on_progres while (source_->GetNextFrame(native_frame, time_register)) { - writer_->RecordFrame(native_frame, time_register); frames_processed++; @@ -43,6 +42,7 @@ bool VTX::RecordPipeline::Run(std::function on_progres } writer_->Stop(); - if (on_progress) on_progress(1.0f,""); + if (on_progress) + on_progress(1.0f, ""); return frames_processed > 0; } diff --git a/sdk/src/vtx_writer/src/vtx/writer/core/vtx_writer_facade.cpp b/sdk/src/vtx_writer/src/vtx/writer/core/vtx_writer_facade.cpp index 3597cbc..f04fe3d 100644 --- a/sdk/src/vtx_writer/src/vtx/writer/core/vtx_writer_facade.cpp +++ b/sdk/src/vtx_writer/src/vtx/writer/core/vtx_writer_facade.cpp @@ -14,63 +14,57 @@ namespace VTX { class WriterFacadeImpl : public IVtxWriterFacade { public: WriterFacadeImpl(typename ReplayWriter::Config internal_config) - : writer_(internal_config) - { - } + : writer_(internal_config) {} // RecordFrame / Flush / Stop are no-ops after the first Stop(). The // on-disk file was finalised (header + chunks + footer) there, and // letting further writes through would rewrite/corrupt the footer. void RecordFrame(VTX::Frame& native_frame, const VTX::GameTime::GameTimeRegister& game_time_register) override { - if (stopped_) return; + if (stopped_) + return; writer_.RecordFrame(native_frame, game_time_register); } void Flush() override { - if (stopped_) return; + if (stopped_) + return; writer_.Flush(); } void Stop() override { - if (stopped_) return; // idempotent: second Stop() is a no-op + if (stopped_) + return; // idempotent: second Stop() is a no-op writer_.Stop(); stopped_ = true; } - VTX::SchemaRegistry& GetSchema()override - { - return writer_.GetRegistry(); - } + VTX::SchemaRegistry& GetSchema() override { return writer_.GetRegistry(); } private: ReplayWriter writer_; bool stopped_ = false; }; - - - std::unique_ptr CreateFlatBuffersWriterFacade( - const WriterFacadeConfig& config) - { + + + std::unique_ptr CreateFlatBuffersWriterFacade(const WriterFacadeConfig& config) { using SinkType = ChunkedFileSink; - + ReplayWriter::Config internal_cfg; internal_cfg.default_fps = config.default_fps; internal_cfg.is_increasing = config.is_increasing; internal_cfg.chunker_config.max_frames = config.chunk_max_frames; internal_cfg.chunker_config.max_bytes = config.chunk_max_bytes; - + internal_cfg.sink_config.filename = config.output_filepath; internal_cfg.schema_json_path = config.schema_json_path; - internal_cfg.sink_config.header_config.replay_name = config.replay_name; + internal_cfg.sink_config.header_config.replay_name = config.replay_name; internal_cfg.sink_config.header_config.replay_uuid = config.replay_uuid; internal_cfg.sink_config.b_use_compression = config.use_compression; return std::make_unique>(internal_cfg); } - std::unique_ptr CreateProtobuffWriterFacade( - const WriterFacadeConfig& config) - { + std::unique_ptr CreateProtobuffWriterFacade(const WriterFacadeConfig& config) { using SinkType = VTX::ChunkedFileSink; ReplayWriter::Config internal_cfg; @@ -80,11 +74,11 @@ namespace VTX { internal_cfg.is_increasing = config.is_increasing; internal_cfg.chunker_config.max_frames = config.chunk_max_frames; internal_cfg.chunker_config.max_bytes = config.chunk_max_bytes; - + internal_cfg.sink_config.filename = config.output_filepath; internal_cfg.schema_json_path = config.schema_json_path; internal_cfg.sink_config.b_use_compression = config.use_compression; return std::make_unique>(internal_cfg); } -} \ No newline at end of file +} // namespace VTX \ No newline at end of file diff --git a/sdk/src/vtx_writer/src/vtx/writer/formatters/flatbuffers_vtx_policy.cpp b/sdk/src/vtx_writer/src/vtx/writer/formatters/flatbuffers_vtx_policy.cpp index a378c52..ddf103c 100644 --- a/sdk/src/vtx_writer/src/vtx/writer/formatters/flatbuffers_vtx_policy.cpp +++ b/sdk/src/vtx_writer/src/vtx/writer/formatters/flatbuffers_vtx_policy.cpp @@ -9,14 +9,15 @@ #include "vtx/writer/policies/formatters/flatbuffers_vtx_policy.h" std::string VTX::FlatBuffersVtxPolicy::GetMagicBytes() { - return "VTXF"; + return "VTXF"; } std::unique_ptr VTX::FlatBuffersVtxPolicy::FromNative(VTX::Frame&& native) { auto sorted_frame = std::make_unique(); - + const auto& native_buckets = native.GetBuckets(); - if (native_buckets.empty()) return sorted_frame; + if (native_buckets.empty()) + return sorted_frame; const auto& native_data = native_buckets[0]; auto& sorted_data = sorted_frame->GetBucket("data"); @@ -33,7 +34,7 @@ std::unique_ptr VTX::FlatBuffersVtxPolicy: for (const auto& ent : entities) { max_type = std::max(ent.entity_type_id, max_type); } - + if (max_type < 0) { sorted_data = native_data; return sorted_frame; @@ -55,7 +56,7 @@ std::unique_ptr VTX::FlatBuffersVtxPolicy: for (int32_t type_id = 0; type_id <= max_type; ++type_id) { const auto& indices = indices_by_type[type_id]; - + sorted_data.type_ranges[type_id].start_index = current_index; sorted_data.type_ranges[type_id].count = static_cast(indices.size()); @@ -72,13 +73,13 @@ std::unique_ptr VTX::FlatBuffersVtxPolicy: auto& sorted_bones = sorted_frame->GetBucket("bone_data"); sorted_bones = native_buckets[1]; } - + return sorted_frame; } size_t VTX::FlatBuffersVtxPolicy::GetFrameSize(const FrameType& /*frame*/) { // FlatBuffers doesnt know the size until is serialized - return 0; + return 0; } VTX::FlatBuffersVtxPolicy::SchemaType VTX::FlatBuffersVtxPolicy::CreateSchema(const SchemaRegistry& registry) { @@ -95,100 +96,83 @@ VTX::FlatBuffersVtxPolicy::SchemaType VTX::FlatBuffersVtxPolicy::CreateSchema(co std::string VTX::FlatBuffersVtxPolicy::SerializeHeader(const VTX::SessionConfig& config, const SchemaType& schema) { flatbuffers::FlatBufferBuilder builder(1024); - + auto replay_name = builder.CreateString(config.replay_name); - auto uuid = builder.CreateString(config.replay_uuid.empty() ? "uuid_placeholder" : config.replay_uuid); - auto meta = builder.CreateString(config.custom_json_metadata); - - auto release = builder.CreateString("1.0.0"); - auto hash = builder.CreateString("UNKNOWN_HASH"); + auto uuid = builder.CreateString(config.replay_uuid.empty() ? "uuid_placeholder" : config.replay_uuid); + auto meta = builder.CreateString(config.custom_json_metadata); + + auto release = builder.CreateString("1.0.0"); + auto hash = builder.CreateString("UNKNOWN_HASH"); - auto version = fbsvtx::CreateVersionInfo(builder, - 1, 0, // Major, Minor - config.schema_version); + auto version = fbsvtx::CreateVersionInfo(builder, 1, 0, // Major, Minor + config.schema_version); auto now = std::chrono::system_clock::now(); int64_t timestamp = std::chrono::duration_cast(now.time_since_epoch()).count(); - + auto schema_off = fbsvtx::ContextualSchema::Pack(builder, schema.get()); - - auto header_off = fbsvtx::CreateFileHeader(builder, - version, - schema_off, - uuid, - replay_name, - timestamp, - meta); + + auto header_off = fbsvtx::CreateFileHeader(builder, version, schema_off, uuid, replay_name, timestamp, meta); builder.Finish(header_off); return {reinterpret_cast(builder.GetBufferPointer()), builder.GetSize()}; } -std::string VTX::FlatBuffersVtxPolicy::SerializeChunk(const std::vector>& frames, int32_t chunk_idx, bool is_compressed) { - if (frames.empty()) return ""; +std::string VTX::FlatBuffersVtxPolicy::SerializeChunk(const std::vector>& frames, + int32_t chunk_idx, bool is_compressed) { + if (frames.empty()) + return ""; - flatbuffers::FlatBufferBuilder builder(1024 * 1024); + flatbuffers::FlatBufferBuilder builder(1024 * 1024); std::vector> frame_offsets; frame_offsets.reserve(frames.size()); - for(const auto& f : frames) { - if(f) { + for (const auto& f : frames) { + if (f) { frame_offsets.push_back(VTX::Serialization::ToFlat(builder, *f)); } } auto frames_vector = builder.CreateVector(frame_offsets); - - auto chunk_off = fbsvtx::CreateChunk(builder, - chunk_idx, - is_compressed, - 0, - frames_vector, - 0 - ); + + auto chunk_off = fbsvtx::CreateChunk(builder, chunk_idx, is_compressed, 0, frames_vector, 0); builder.Finish(chunk_off); return {reinterpret_cast(builder.GetBufferPointer()), builder.GetSize()}; } -std::string VTX::FlatBuffersVtxPolicy::SerializeFooter(const std::vector& seek_table, const SessionFooter& footer_data) { +std::string VTX::FlatBuffersVtxPolicy::SerializeFooter(const std::vector& seek_table, + const SessionFooter& footer_data) { flatbuffers::FlatBufferBuilder builder(2048); flatbuffers::Offset time_data_offsets = 0; - - if(footer_data.game_times || footer_data.created_utc || footer_data.gaps || footer_data.segments) { - auto gt = (footer_data.game_times) ? builder.CreateVector(*footer_data.game_times) : 0; - auto cu = (footer_data.created_utc) ? builder.CreateVector(*footer_data.created_utc) : 0; - auto gaps = (footer_data.gaps) ? builder.CreateVector(*footer_data.gaps) : 0; - auto segs = (footer_data.segments) ? builder.CreateVector(*footer_data.segments) : 0; - + + if (footer_data.game_times || footer_data.created_utc || footer_data.gaps || footer_data.segments) { + auto gt = (footer_data.game_times) ? builder.CreateVector(*footer_data.game_times) : 0; + auto cu = (footer_data.created_utc) ? builder.CreateVector(*footer_data.created_utc) : 0; + auto gaps = (footer_data.gaps) ? builder.CreateVector(*footer_data.gaps) : 0; + auto segs = (footer_data.segments) ? builder.CreateVector(*footer_data.segments) : 0; + time_data_offsets = fbsvtx::CreateReplayTimeData(builder, gt, cu, gaps, segs); } std::vector> index_offsets; index_offsets.reserve(seek_table.size()); - - for(const auto& e : seek_table) { - index_offsets.push_back(fbsvtx::CreateChunkIndexEntry(builder, - e.chunk_index, - e.start_frame, - e.end_frame, - static_cast(e.file_offset), - e.chunk_size_bytes)); + + for (const auto& e : seek_table) { + index_offsets.push_back(fbsvtx::CreateChunkIndexEntry(builder, e.chunk_index, e.start_frame, e.end_frame, + static_cast(e.file_offset), + e.chunk_size_bytes)); } auto index_vector = builder.CreateVector(index_offsets); - auto events_vec = builder.CreateVector(std::vector>{}); + auto events_vec = builder.CreateVector(std::vector> {}); - auto footer_offset = fbsvtx::CreateFileFooter(builder, - footer_data.total_frames, - static_cast(footer_data.duration_seconds), - time_data_offsets, - index_vector, - events_vec, - 0); + auto footer_offset = + fbsvtx::CreateFileFooter(builder, footer_data.total_frames, static_cast(footer_data.duration_seconds), + time_data_offsets, index_vector, events_vec, 0); builder.Finish(footer_offset); return {reinterpret_cast(builder.GetBufferPointer()), builder.GetSize()}; diff --git a/sdk/src/vtx_writer/src/vtx/writer/formatters/protobuff_vtx_policy.cpp b/sdk/src/vtx_writer/src/vtx/writer/formatters/protobuff_vtx_policy.cpp index 9d45bae..50fe6d3 100644 --- a/sdk/src/vtx_writer/src/vtx/writer/formatters/protobuff_vtx_policy.cpp +++ b/sdk/src/vtx_writer/src/vtx/writer/formatters/protobuff_vtx_policy.cpp @@ -5,20 +5,19 @@ #include "vtx_schema.pb.h" std::string VTX::ProtobufVtxPolicy::GetMagicBytes() { - return "VTXP"; + return "VTXP"; } std::unique_ptr VTX::ProtobufVtxPolicy::FromNative(VTX::Frame&& native) { const auto& native_buckets = native.GetBuckets(); VTX::Frame sorted_native; sorted_native.GetMutableBuckets().resize(native_buckets.size()); - + for (size_t b_idx = 0; b_idx < native_buckets.size(); ++b_idx) { const auto& src_bucket = native_buckets[b_idx]; auto& dst_bucket = sorted_native.GetBucket(static_cast(b_idx)); - if (b_idx == 0 && !src_bucket.entities.empty()) - { + if (b_idx == 0 && !src_bucket.entities.empty()) { const auto& entities = src_bucket.entities; const auto& ids = src_bucket.unique_ids; int32_t max_type = -1; @@ -57,24 +56,21 @@ std::unique_ptr VTX::ProtobufVtxPolicy::FromN } else { dst_bucket = src_bucket; } - } - else - { + } else { dst_bucket = src_bucket; } } - + auto proto = std::make_unique(); VTX::Serialization::ToProto(sorted_native, proto.get()); return proto; } - + size_t VTX::ProtobufVtxPolicy::GetFrameSize(const VTX::ProtobufVtxPolicy::FrameType& frame) { return frame.ByteSizeLong(); } VTX::ProtobufVtxPolicy::SchemaType VTX::ProtobufVtxPolicy::CreateSchema(const SchemaRegistry& registry) { - SchemaType proto_schema; proto_schema.set_data_indentifier("VTX_GameData"); // Default VTX data format identifier proto_schema.set_data_version(1); @@ -89,15 +85,15 @@ std::string VTX::ProtobufVtxPolicy::SerializeHeader(const VTX::SessionConfig& co proto_header.set_replay_name(config.replay_name); proto_header.set_replay_uuid(config.replay_uuid.empty() ? "uuid_placeholder" : config.replay_uuid); proto_header.set_custom_json_metadata(config.custom_json_metadata); - + auto* v = proto_header.mutable_version(); - v->set_format_major(1); v->set_format_minor(0); + v->set_format_major(1); + v->set_format_minor(0); v->set_schema_version(config.schema_version); auto now = std::chrono::system_clock::now(); proto_header.set_recorded_utc_timestamp( - std::chrono::duration_cast(now.time_since_epoch()).count() - ); + std::chrono::duration_cast(now.time_since_epoch()).count()); proto_header.mutable_prop_schema()->CopyFrom(schema); @@ -106,15 +102,17 @@ std::string VTX::ProtobufVtxPolicy::SerializeHeader(const VTX::SessionConfig& co return payload; } -std::string VTX::ProtobufVtxPolicy::SerializeChunk(const std::vector>& frames, int32_t chunkIdx, bool is_compressed) { - if (frames.empty()) return ""; +std::string VTX::ProtobufVtxPolicy::SerializeChunk(const std::vector>& frames, + int32_t chunkIdx, bool is_compressed) { + if (frames.empty()) + return ""; cppvtx::FrameChunk chunk_msg; chunk_msg.set_chunk_index(chunkIdx); - chunk_msg.set_is_compressed(is_compressed); - + chunk_msg.set_is_compressed(is_compressed); + for (const auto& frame_ptr : frames) { - if(frame_ptr) { + if (frame_ptr) { chunk_msg.add_frames()->Swap(frame_ptr.get()); } } @@ -124,19 +122,17 @@ std::string VTX::ProtobufVtxPolicy::SerializeChunk(const std::vector& seekTable, - const SessionFooter& footerData) -{ +std::string VTX::ProtobufVtxPolicy::SerializeFooter(const std::vector& seekTable, + const SessionFooter& footerData) { cppvtx::FileFooter footer_msg; - + footer_msg.set_total_frames(footerData.total_frames); footer_msg.set_duration_seconds(static_cast(footerData.duration_seconds)); - - if (footerData.game_times || footerData.created_utc || footerData.gaps || footerData.segments) { - + + if (footerData.game_times || footerData.created_utc || footerData.gaps || footerData.segments) { auto* times_msg = footer_msg.mutable_times(); if (footerData.game_times && !footerData.game_times->empty()) { @@ -171,9 +167,9 @@ std::string VTX::ProtobufVtxPolicy::SerializeFooter(const std::vectorReserve(static_cast(seekTable.size())); - + for (const auto& entry : seekTable) { auto* proto_entry = footer_msg.add_chunk_index(); proto_entry->set_chunk_index(entry.chunk_index); @@ -185,8 +181,8 @@ std::string VTX::ProtobufVtxPolicy::SerializeFooter(const std::vector - auto CreateScalarArray(flatbuffers::FlatBufferBuilder& builder, const std::vector& data, const std::vector& offsets, CreateFunc func) - { - if (data.empty()) return decltype(func(builder, 0, 0))(0); - + auto CreateScalarArray(flatbuffers::FlatBufferBuilder& builder, const std::vector& data, + const std::vector& offsets, CreateFunc func) { + if (data.empty()) + return decltype(func(builder, 0, 0))(0); + auto data_vec = builder.CreateVector(data); auto offs_vec = builder.CreateVector(offsets); return func(builder, data_vec, offs_vec); } template - auto CreateStructArray(flatbuffers::FlatBufferBuilder& builder, std::vector& data, const std::vector& offsets, CreateFunc func) - { - if (data.empty()) return decltype(func(builder, 0, 0))(0); + auto CreateStructArray(flatbuffers::FlatBufferBuilder& builder, std::vector& data, + const std::vector& offsets, CreateFunc func) { + if (data.empty()) + return decltype(func(builder, 0, 0))(0); - std::vector temp; + std::vector temp; temp.reserve(data.size()); - for(auto& x : data) temp.push_back(ToFlat(x)); + for (auto& x : data) + temp.push_back(ToFlat(x)); auto data_vec = builder.CreateVectorOfStructs(temp); auto offs_vec = builder.CreateVector(offsets); @@ -34,13 +35,15 @@ namespace VTX } template - static auto CreateStringArray(flatbuffers::FlatBufferBuilder& builder, const std::vector& data, const std::vector& offsets, CreateFunc func) - { - if (data.empty()) return decltype(func(builder, 0, 0))(0); + static auto CreateStringArray(flatbuffers::FlatBufferBuilder& builder, const std::vector& data, + const std::vector& offsets, CreateFunc func) { + if (data.empty()) + return decltype(func(builder, 0, 0))(0); std::vector> str_offsets; str_offsets.reserve(data.size()); - for(const auto& s : data) str_offsets.push_back(builder.CreateString(s)); + for (const auto& s : data) + str_offsets.push_back(builder.CreateString(s)); auto data_vec = builder.CreateVector(str_offsets); auto offs_vec = builder.CreateVector(offsets); @@ -48,17 +51,18 @@ namespace VTX } template - static auto CreateBoolFromBitArray(flatbuffers::FlatBufferBuilder& builder, const std::vector& data, const std::vector& offsets, CreateFunc func) - { - if (data.empty()) return decltype(func(builder, 0, 0))(0); - + static auto CreateBoolFromBitArray(flatbuffers::FlatBufferBuilder& builder, const std::vector& data, + const std::vector& offsets, CreateFunc func) { + if (data.empty()) + return decltype(func(builder, 0, 0))(0); + std::vector temp_bool(data.begin(), data.end()); auto dataVec = builder.CreateVector(temp_bool); auto offsVec = builder.CreateVector(offsets); return func(builder, dataVec, offsVec); } - } -}; + } // namespace Serialization +}; // namespace VTX // ========================================================================= // IMPLEMENTACIÓN DE LA API (Sin inlines) // ========================================================================= @@ -79,29 +83,33 @@ fbsvtx::FloatRange VTX::Serialization::ToFlat(VTX::FloatRange& r) { return {r.min, r.max, r.value_normalized}; } -flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::MapContainer& src) { - if (src.keys.empty()) return 0; - +flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::FlatBufferBuilder& builder, + VTX::MapContainer& src) { + if (src.keys.empty()) + return 0; + std::vector> k; k.reserve(src.keys.size()); - for(const auto& s : src.keys) k.push_back(builder.CreateString(s)); - + for (const auto& s : src.keys) + k.push_back(builder.CreateString(s)); + std::vector> v; v.reserve(src.values.size()); - for(auto& p : src.values) v.push_back(ToFlat(builder, p)); + for (auto& p : src.values) + v.push_back(ToFlat(builder, p)); return fbsvtx::CreateMapContainer(builder, builder.CreateVector(k), builder.CreateVector(v)); } -flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::PropertyContainer& src) { - +flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::FlatBufferBuilder& builder, + VTX::PropertyContainer& src) { src.content_hash = VTX::Helpers::CalculateContainerHash(src); // --- SCALARS --- - auto int32Off = src.int32_properties.empty() ? 0 : builder.CreateVector(src.int32_properties); - auto int64Off = src.int64_properties.empty() ? 0 : builder.CreateVector(src.int64_properties); - auto floatOff = src.float_properties.empty() ? 0 : builder.CreateVector(src.float_properties); - auto doubleOff = src.double_properties.empty() ? 0 : builder.CreateVector(src.double_properties); - + auto int32Off = src.int32_properties.empty() ? 0 : builder.CreateVector(src.int32_properties); + auto int64Off = src.int64_properties.empty() ? 0 : builder.CreateVector(src.int64_properties); + auto floatOff = src.float_properties.empty() ? 0 : builder.CreateVector(src.float_properties); + auto doubleOff = src.double_properties.empty() ? 0 : builder.CreateVector(src.double_properties); + flatbuffers::Offset> boolOff = 0; if (!src.bool_properties.empty()) { std::vector temp(src.bool_properties.begin(), src.bool_properties.end()); @@ -111,164 +119,209 @@ flatbuffers::Offset VTX::Serialization::ToFlat(flatbu flatbuffers::Offset>> stringOff = 0; if (!src.string_properties.empty()) { std::vector> strs; - for(const auto& s : src.string_properties) strs.push_back(builder.CreateString(s)); + for (const auto& s : src.string_properties) + strs.push_back(builder.CreateString(s)); stringOff = builder.CreateVector(strs); } // --- MATH PROPS --- flatbuffers::Offset> vecPropOff = 0; - if(!src.vector_properties.empty()){ - std::vector temp; temp.reserve(src.vector_properties.size()); - for(auto& v : src.vector_properties) temp.push_back(ToFlat(v)); + if (!src.vector_properties.empty()) { + std::vector temp; + temp.reserve(src.vector_properties.size()); + for (auto& v : src.vector_properties) + temp.push_back(ToFlat(v)); vecPropOff = builder.CreateVectorOfStructs(temp); } - + flatbuffers::Offset> quatPropOff = 0; - if(!src.quat_properties.empty()){ - std::vector temp; temp.reserve(src.quat_properties.size()); - for(auto& v : src.quat_properties) temp.push_back(ToFlat(v)); + if (!src.quat_properties.empty()) { + std::vector temp; + temp.reserve(src.quat_properties.size()); + for (auto& v : src.quat_properties) + temp.push_back(ToFlat(v)); quatPropOff = builder.CreateVectorOfStructs(temp); } flatbuffers::Offset> transPropOff = 0; - if(!src.transform_properties.empty()){ - std::vector temp; temp.reserve(src.transform_properties.size()); - for(auto& v : src.transform_properties) temp.push_back(ToFlat(v)); + if (!src.transform_properties.empty()) { + std::vector temp; + temp.reserve(src.transform_properties.size()); + for (auto& v : src.transform_properties) + temp.push_back(ToFlat(v)); transPropOff = builder.CreateVectorOfStructs(temp); } flatbuffers::Offset> rangePropOff = 0; - if(!src.range_properties.empty()){ - std::vector temp; temp.reserve(src.range_properties.size()); - for(auto& v : src.range_properties) temp.push_back(ToFlat(v)); + if (!src.range_properties.empty()) { + std::vector temp; + temp.reserve(src.range_properties.size()); + for (auto& v : src.range_properties) + temp.push_back(ToFlat(v)); rangePropOff = builder.CreateVectorOfStructs(temp); } // --- FLAT ARRAYS (SoA) --- - auto flatBytes = CreateScalarArray(builder, src.byte_array_properties.data, src.byte_array_properties.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatBytesArray(b, d, o); }); + auto flatBytes = CreateScalarArray(builder, src.byte_array_properties.data, src.byte_array_properties.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatBytesArray(b, d, o); }); - auto flatInt32 = CreateScalarArray(builder, src.int32_arrays.data, src.int32_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatIntArray(b, d, o); }); + auto flatInt32 = CreateScalarArray(builder, src.int32_arrays.data, src.int32_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatIntArray(b, d, o); }); - auto flatInt64 = CreateScalarArray(builder, src.int64_arrays.data, src.int64_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatInt64Array(b, d, o); }); + auto flatInt64 = CreateScalarArray(builder, src.int64_arrays.data, src.int64_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatInt64Array(b, d, o); }); - auto flatFloat = CreateScalarArray(builder, src.float_arrays.data, src.float_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatFloatArray(b, d, o); }); + auto flatFloat = CreateScalarArray(builder, src.float_arrays.data, src.float_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatFloatArray(b, d, o); }); - auto flatDouble = CreateScalarArray(builder, src.double_arrays.data, src.double_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatDoubleArray(b, d, o); }); - - auto flatString = CreateStringArray(builder, src.string_arrays.data, src.string_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatStringArray(b, d, o); }); - - auto flatBool = CreateScalarArray(builder, src.bool_arrays.data, src.bool_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatBoolArray(b, d, o); }); + auto flatDouble = CreateScalarArray(builder, src.double_arrays.data, src.double_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatDoubleArray(b, d, o); }); + + auto flatString = CreateStringArray(builder, src.string_arrays.data, src.string_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatStringArray(b, d, o); }); + + auto flatBool = CreateScalarArray(builder, src.bool_arrays.data, src.bool_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatBoolArray(b, d, o); }); // Struct Arrays - auto flatVec = CreateStructArray(builder, src.vector_arrays.data, src.vector_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatVectorArray(b, d, o); }); + auto flatVec = CreateStructArray( + builder, src.vector_arrays.data, src.vector_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatVectorArray(b, d, o); }); - auto flatQuat = CreateStructArray(builder, src.quat_arrays.data, src.quat_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatQuatArray(b, d, o); }); + auto flatQuat = CreateStructArray( + builder, src.quat_arrays.data, src.quat_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatQuatArray(b, d, o); }); - auto flatTrans = CreateStructArray(builder, src.transform_arrays.data, src.transform_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatTransformArray(b, d, o); }); + auto flatTrans = CreateStructArray( + builder, src.transform_arrays.data, src.transform_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatTransformArray(b, d, o); }); - auto flatRange = CreateStructArray(builder, src.range_arrays.data, src.range_arrays.offsets, - [](auto& b, auto d, auto o){ return fbsvtx::CreateFlatRangeArray(b, d, o); }); + auto flatRange = CreateStructArray( + builder, src.range_arrays.data, src.range_arrays.offsets, + [](auto& b, auto d, auto o) { return fbsvtx::CreateFlatRangeArray(b, d, o); }); // --- RECURSIVE --- flatbuffers::Offset>> anyStructOff = 0; if (!src.any_struct_properties.empty()) { std::vector> offs; - for (auto& child : src.any_struct_properties) offs.push_back(ToFlat(builder, child)); + for (auto& child : src.any_struct_properties) + offs.push_back(ToFlat(builder, child)); anyStructOff = builder.CreateVector(offs); } flatbuffers::Offset anyStructArrOff = 0; - if(!src.any_struct_arrays.data.empty()) { + if (!src.any_struct_arrays.data.empty()) { std::vector> d; - for(auto& item : src.any_struct_arrays.data) d.push_back(ToFlat(builder, item)); - - anyStructArrOff = fbsvtx::CreateFlatPropertyContainerArray( - builder, builder.CreateVector(d), builder.CreateVector(src.any_struct_arrays.offsets)); + for (auto& item : src.any_struct_arrays.data) + d.push_back(ToFlat(builder, item)); + + anyStructArrOff = fbsvtx::CreateFlatPropertyContainerArray(builder, builder.CreateVector(d), + builder.CreateVector(src.any_struct_arrays.offsets)); } // --- MAPS --- flatbuffers::Offset>> mapPropOff = 0; - if(!src.map_properties.empty()) { + if (!src.map_properties.empty()) { std::vector> m; - for(auto& item : src.map_properties) m.push_back(ToFlat(builder, item)); + for (auto& item : src.map_properties) + m.push_back(ToFlat(builder, item)); mapPropOff = builder.CreateVector(m); } flatbuffers::Offset mapArrOff = 0; - if(!src.map_arrays.data.empty()) { + if (!src.map_arrays.data.empty()) { std::vector> d; - for(auto& item : src.map_arrays.data) d.push_back(ToFlat(builder, item)); - mapArrOff = fbsvtx::CreateFlatMapArray( - builder, builder.CreateVector(d), builder.CreateVector(src.map_arrays.offsets)); + for (auto& item : src.map_arrays.data) + d.push_back(ToFlat(builder, item)); + mapArrOff = + fbsvtx::CreateFlatMapArray(builder, builder.CreateVector(d), builder.CreateVector(src.map_arrays.offsets)); } // --- FINAL BUILD --- fbsvtx::PropertyContainerBuilder containerBuilder(builder); - + containerBuilder.add_type_id(src.entity_type_id); containerBuilder.add_content_hash(src.content_hash); - - if(!boolOff.IsNull()) containerBuilder.add_bool_properties(boolOff); - if(!int32Off.IsNull()) containerBuilder.add_int32_properties(int32Off); - if(!int64Off.IsNull()) containerBuilder.add_int64_properties(int64Off); - if(!floatOff.IsNull()) containerBuilder.add_float_properties(floatOff); - if(!doubleOff.IsNull()) containerBuilder.add_double_properties(doubleOff); - if(!stringOff.IsNull()) containerBuilder.add_string_properties(stringOff); - - if(!vecPropOff.IsNull()) containerBuilder.add_vector_properties(vecPropOff); - if(!quatPropOff.IsNull()) containerBuilder.add_quat_properties(quatPropOff); - if(!transPropOff.IsNull()) containerBuilder.add_transform_properties(transPropOff); - if(!rangePropOff.IsNull()) containerBuilder.add_range_properties(rangePropOff); - - if(!flatBytes.IsNull()) containerBuilder.add_byte_array_properties(flatBytes); - if(!flatInt32.IsNull()) containerBuilder.add_int32_arrays(flatInt32); - if(!flatInt64.IsNull()) containerBuilder.add_int64_arrays(flatInt64); - if(!flatFloat.IsNull()) containerBuilder.add_float_arrays(flatFloat); - if(!flatDouble.IsNull()) containerBuilder.add_double_arrays(flatDouble); - if(!flatString.IsNull()) containerBuilder.add_string_arrays(flatString); - if(!flatBool.IsNull()) containerBuilder.add_bool_arrays(flatBool); - - if(!flatVec.IsNull()) containerBuilder.add_vector_arrays(flatVec); - if(!flatQuat.IsNull()) containerBuilder.add_quat_arrays(flatQuat); - if(!flatTrans.IsNull()) containerBuilder.add_transform_arrays(flatTrans); - if(!flatRange.IsNull()) containerBuilder.add_range_arrays(flatRange); - - if(!anyStructOff.IsNull()) containerBuilder.add_any_struct_properties(anyStructOff); - if(!anyStructArrOff.IsNull()) containerBuilder.add_any_struct_arrays(anyStructArrOff); - if(!mapPropOff.IsNull()) containerBuilder.add_map_properties(mapPropOff); - if(!mapArrOff.IsNull()) containerBuilder.add_map_arrays(mapArrOff); + + if (!boolOff.IsNull()) + containerBuilder.add_bool_properties(boolOff); + if (!int32Off.IsNull()) + containerBuilder.add_int32_properties(int32Off); + if (!int64Off.IsNull()) + containerBuilder.add_int64_properties(int64Off); + if (!floatOff.IsNull()) + containerBuilder.add_float_properties(floatOff); + if (!doubleOff.IsNull()) + containerBuilder.add_double_properties(doubleOff); + if (!stringOff.IsNull()) + containerBuilder.add_string_properties(stringOff); + + if (!vecPropOff.IsNull()) + containerBuilder.add_vector_properties(vecPropOff); + if (!quatPropOff.IsNull()) + containerBuilder.add_quat_properties(quatPropOff); + if (!transPropOff.IsNull()) + containerBuilder.add_transform_properties(transPropOff); + if (!rangePropOff.IsNull()) + containerBuilder.add_range_properties(rangePropOff); + + if (!flatBytes.IsNull()) + containerBuilder.add_byte_array_properties(flatBytes); + if (!flatInt32.IsNull()) + containerBuilder.add_int32_arrays(flatInt32); + if (!flatInt64.IsNull()) + containerBuilder.add_int64_arrays(flatInt64); + if (!flatFloat.IsNull()) + containerBuilder.add_float_arrays(flatFloat); + if (!flatDouble.IsNull()) + containerBuilder.add_double_arrays(flatDouble); + if (!flatString.IsNull()) + containerBuilder.add_string_arrays(flatString); + if (!flatBool.IsNull()) + containerBuilder.add_bool_arrays(flatBool); + + if (!flatVec.IsNull()) + containerBuilder.add_vector_arrays(flatVec); + if (!flatQuat.IsNull()) + containerBuilder.add_quat_arrays(flatQuat); + if (!flatTrans.IsNull()) + containerBuilder.add_transform_arrays(flatTrans); + if (!flatRange.IsNull()) + containerBuilder.add_range_arrays(flatRange); + + if (!anyStructOff.IsNull()) + containerBuilder.add_any_struct_properties(anyStructOff); + if (!anyStructArrOff.IsNull()) + containerBuilder.add_any_struct_arrays(anyStructArrOff); + if (!mapPropOff.IsNull()) + containerBuilder.add_map_properties(mapPropOff); + if (!mapArrOff.IsNull()) + containerBuilder.add_map_arrays(mapArrOff); return containerBuilder.Finish(); } -flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::Bucket& src) { +flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::FlatBufferBuilder& builder, + VTX::Bucket& src) { flatbuffers::Offset>> idsOff = 0; - if(!src.unique_ids.empty()) { + if (!src.unique_ids.empty()) { std::vector> offsets; - for(const auto& s : src.unique_ids) offsets.push_back(builder.CreateString(s)); + for (const auto& s : src.unique_ids) + offsets.push_back(builder.CreateString(s)); idsOff = builder.CreateVector(offsets); } std::vector> entOffsets; - for(auto& e : src.entities) entOffsets.push_back(ToFlat(builder, e)); + for (auto& e : src.entities) + entOffsets.push_back(ToFlat(builder, e)); auto entitiesOff = builder.CreateVector(entOffsets); - + flatbuffers::Offset> rangesOff = 0; if (!src.type_ranges.empty()) { std::vector fbRanges; fbRanges.reserve(src.type_ranges.size()); for (const auto& r : src.type_ranges) { - fbRanges.emplace_back(r.start_index, r.count); + fbRanges.emplace_back(r.start_index, r.count); } rangesOff = builder.CreateVectorOfStructs(fbRanges); } @@ -276,9 +329,10 @@ flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::Flat return fbsvtx::CreateBucket(builder, idsOff, entitiesOff, rangesOff); } -flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::FlatBufferBuilder& builder, VTX::Frame& src) { +flatbuffers::Offset VTX::Serialization::ToFlat(flatbuffers::FlatBufferBuilder& builder, + VTX::Frame& src) { auto& src_buckets = src.GetMutableBuckets(); - + std::vector> fbs_buckets; fbs_buckets.reserve(src_buckets.size()); diff --git a/tests/common/test_bucket_frame.cpp b/tests/common/test_bucket_frame.cpp index 8e1a73b..e8d4d84 100644 --- a/tests/common/test_bucket_frame.cpp +++ b/tests/common/test_bucket_frame.cpp @@ -25,9 +25,9 @@ TEST(Frame, CreateBucketIsIdempotent) { auto& b2 = frame.CreateBucket("entity"); b2.unique_ids.push_back("id_1"); - EXPECT_EQ(frame.GetBuckets().size(), 1u); // same bucket + EXPECT_EQ(frame.GetBuckets().size(), 1u); // same bucket EXPECT_EQ(frame.GetBuckets()[0].unique_ids.size(), 2u); - EXPECT_EQ(&b1, &b2); // same reference + EXPECT_EQ(&b1, &b2); // same reference } TEST(Frame, MultipleBucketsKeepStableIndices) { @@ -61,22 +61,24 @@ TEST(Bucket, GetEntitiesOfTypeReturnsCorrectSpan) { bucket.unique_ids.push_back("proj_" + std::to_string(i)); } bucket.type_ranges = { - {0, 3}, // type 0: indices 0..2 - {3, 2}, // type 1: indices 3..4 + {0, 3}, // type 0: indices 0..2 + {3, 2}, // type 1: indices 3..4 }; auto players = bucket.GetEntitiesOfType(0); - auto projs = bucket.GetEntitiesOfType(1); + auto projs = bucket.GetEntitiesOfType(1); EXPECT_EQ(players.size(), 3u); - EXPECT_EQ(projs.size(), 2u); - for (const auto& p : players) EXPECT_EQ(p.entity_type_id, 0); - for (const auto& p : projs) EXPECT_EQ(p.entity_type_id, 1); + EXPECT_EQ(projs.size(), 2u); + for (const auto& p : players) + EXPECT_EQ(p.entity_type_id, 0); + for (const auto& p : projs) + EXPECT_EQ(p.entity_type_id, 1); } TEST(Bucket, GetEntitiesOfTypeReturnsEmptyForUnknownId) { VTX::Bucket bucket; - bucket.type_ranges = { {0, 1} }; // only type 0 registered + bucket.type_ranges = {{0, 1}}; // only type 0 registered auto span = bucket.GetEntitiesOfType(5); EXPECT_TRUE(span.empty()); } @@ -87,7 +89,7 @@ TEST(Bucket, GetEntitiesOfTypeWithEnumOverloadCompiles) { enum class ArenaEntity : int32_t { Player = 0, Projectile = 1 }; VTX::Bucket bucket; bucket.entities.emplace_back(); - bucket.type_ranges = { {0, 1} }; + bucket.type_ranges = {{0, 1}}; auto players = bucket.GetEntitiesOfType(ArenaEntity::Player); EXPECT_EQ(players.size(), 1u); diff --git a/tests/common/test_content_hash_edges.cpp b/tests/common/test_content_hash_edges.cpp index 56173de..e7e0bdc 100644 --- a/tests/common/test_content_hash_edges.cpp +++ b/tests/common/test_content_hash_edges.cpp @@ -42,7 +42,7 @@ TEST(ContentHashEdges, HashDistinguishesDifferentNaNBitPatterns) { // diffs between containers that happened to land on different NaNs. float qnan = std::nanf(""); float snan = 0.0f; - const uint32_t snan_bits = 0x7FA00000u; // quiet bit clear, payload set + const uint32_t snan_bits = 0x7FA00000u; // quiet bit clear, payload set std::memcpy(&snan, &snan_bits, sizeof(snan)); PropertyContainer a, b; @@ -54,7 +54,7 @@ TEST(ContentHashEdges, HashDistinguishesDifferentNaNBitPatterns) { // long as it's consistent, but we expect a bitwise hasher to differ. (void)CalculateContainerHash(a); (void)CalculateContainerHash(b); - SUCCEED(); // the real invariant: no crash on NaN of any kind + SUCCEED(); // the real invariant: no crash on NaN of any kind } // --------------------------------------------------------------------------- @@ -68,7 +68,7 @@ TEST(ContentHashEdges, HashDistinguishesEmptyVsDefault_DocumentsInvariant) { // change makes them differ, the test surfaces the contract shift. PropertyContainer a; PropertyContainer b; - b.float_properties = {}; // explicitly empty + b.float_properties = {}; // explicitly empty EXPECT_EQ(CalculateContainerHash(a), CalculateContainerHash(b)); } @@ -121,11 +121,11 @@ TEST(ContentHashEdges, HashLargeStringVectorsDoesntCrashAndIsStable) { TEST(ContentHashEdges, HashIsStableAcrossMove) { PropertyContainer original; - original.entity_type_id = 7; + original.entity_type_id = 7; original.int32_properties = {1, 2, 3}; original.string_properties = {"alpha", "bravo"}; original.float_properties = {3.14f, -2.71f}; - original.vector_properties = {VTX::Vector{1.0, 2.0, 3.0}}; + original.vector_properties = {VTX::Vector {1.0, 2.0, 3.0}}; const uint64_t before = CalculateContainerHash(original); diff --git a/tests/common/test_flat_array.cpp b/tests/common/test_flat_array.cpp index 9079d08..3d84482 100644 --- a/tests/common/test_flat_array.cpp +++ b/tests/common/test_flat_array.cpp @@ -16,7 +16,7 @@ TEST(FlatArray, DefaultConstructsEmpty) { FlatArray arr; EXPECT_EQ(arr.SubArrayCount(), 0u); EXPECT_EQ(arr.TotalElementCount(), 0u); - EXPECT_TRUE(arr.GetSubArray(0).empty()); // OOB read is silent-empty + EXPECT_TRUE(arr.GetSubArray(0).empty()); // OOB read is silent-empty } TEST(FlatArray, ClearResetsEverything) { @@ -45,8 +45,11 @@ TEST(FlatArray, AppendSubArrayStoresItemsContiguously) { auto sub1 = arr.GetSubArray(1); ASSERT_EQ(sub0.size(), 3u); ASSERT_EQ(sub1.size(), 2u); - EXPECT_EQ(sub0[0], 10); EXPECT_EQ(sub0[1], 20); EXPECT_EQ(sub0[2], 30); - EXPECT_EQ(sub1[0], 40); EXPECT_EQ(sub1[1], 50); + EXPECT_EQ(sub0[0], 10); + EXPECT_EQ(sub0[1], 20); + EXPECT_EQ(sub0[2], 30); + EXPECT_EQ(sub1[0], 40); + EXPECT_EQ(sub1[1], 50); } TEST(FlatArray, AppendEmptySubArrayStillCountsAsOne) { @@ -107,7 +110,7 @@ TEST(FlatArray, EraseSubArrayRemovesElementsAndShifts) { EXPECT_EQ(arr.GetSubArray(0).size(), 2u); EXPECT_EQ(arr.GetSubArray(1).size(), 1u); EXPECT_EQ(arr.GetSubArray(1)[0], 6); - EXPECT_EQ(arr.TotalElementCount(), 3u); // 2 + 1 + EXPECT_EQ(arr.TotalElementCount(), 3u); // 2 + 1 } TEST(FlatArray, EraseSubArrayOutOfBoundsReturnsFalse) { @@ -190,7 +193,7 @@ TEST(FlatArray, ReplaceAtPositionUpdatesInPlace) { TEST(FlatArray, EraseRangeRemovesContiguousRun) { FlatArray arr; arr.AppendSubArray({1, 2, 3, 4, 5}); - ASSERT_TRUE(arr.EraseRange(0, 1, 4)); // remove indexes [1,4) = 2,3,4 + ASSERT_TRUE(arr.EraseRange(0, 1, 4)); // remove indexes [1,4) = 2,3,4 auto sub = arr.GetSubArray(0); ASSERT_EQ(sub.size(), 2u); EXPECT_EQ(sub[0], 1); diff --git a/tests/common/test_flat_array_edges.cpp b/tests/common/test_flat_array_edges.cpp index 2a27302..96a0a74 100644 --- a/tests/common/test_flat_array_edges.cpp +++ b/tests/common/test_flat_array_edges.cpp @@ -71,7 +71,7 @@ TEST(FlatArrayEdges, InsertSubArrayAtSizeEqualsAppend) { FlatArray a, b; a.AppendSubArray({1, 2}); a.AppendSubArray({3}); - a.AppendSubArray({4, 5, 6}); // append via AppendSubArray + a.AppendSubArray({4, 5, 6}); // append via AppendSubArray b.AppendSubArray({1, 2}); b.AppendSubArray({3}); @@ -142,7 +142,7 @@ TEST(FlatArrayEdges, EraseLastRemainingSubArray) { EXPECT_EQ(arr.SubArrayCount(), 0u); EXPECT_EQ(arr.TotalElementCount(), 0u); - EXPECT_TRUE(arr.GetSubArray(0).empty()); // OOB -> empty silently + EXPECT_TRUE(arr.GetSubArray(0).empty()); // OOB -> empty silently } TEST(FlatArrayEdges, EraseRangeZeroLengthIsNoOp) { @@ -156,7 +156,9 @@ TEST(FlatArrayEdges, EraseRangeZeroLengthIsNoOp) { auto sub = arr.GetSubArray(0); ASSERT_EQ(sub.size(), 5u); - EXPECT_EQ(sub[0], 1); EXPECT_EQ(sub[2], 3); EXPECT_EQ(sub[4], 5); + EXPECT_EQ(sub[0], 1); + EXPECT_EQ(sub[2], 3); + EXPECT_EQ(sub[4], 5); } // --------------------------------------------------------------------------- @@ -168,13 +170,14 @@ TEST(FlatArrayEdges, InsertAtEndEqualsPushBack) { a.AppendSubArray({10, 20, 30}); b.AppendSubArray({10, 20, 30}); - ASSERT_TRUE(a.Insert(0, a.GetSubArray(0).size(), 99)); // Insert at end - ASSERT_TRUE(b.PushBack(0, 99)); // PushBack + ASSERT_TRUE(a.Insert(0, a.GetSubArray(0).size(), 99)); // Insert at end + ASSERT_TRUE(b.PushBack(0, 99)); // PushBack auto sa = a.GetSubArray(0); auto sb = b.GetSubArray(0); ASSERT_EQ(sa.size(), sb.size()); - for (size_t i = 0; i < sa.size(); ++i) EXPECT_EQ(sa[i], sb[i]); + for (size_t i = 0; i < sa.size(); ++i) + EXPECT_EQ(sa[i], sb[i]); } // --------------------------------------------------------------------------- @@ -183,8 +186,8 @@ TEST(FlatArrayEdges, InsertAtEndEqualsPushBack) { TEST(FlatArrayEdges, DoubleCreateEmptyThenPushBackOnLast) { FlatArray arr; - arr.CreateEmptySubArray(); // sub 0 - arr.CreateEmptySubArray(); // sub 1 + arr.CreateEmptySubArray(); // sub 0 + arr.CreateEmptySubArray(); // sub 1 ASSERT_TRUE(arr.PushBack(1, 42)); EXPECT_EQ(arr.SubArrayCount(), 2u); @@ -204,12 +207,10 @@ TEST(FlatArrayEdges, ReplaceSubArrayWithLargerSpanContainingMoves) { arr.AppendSubArray({std::string("alpha"), std::string("beta")}); arr.AppendSubArray({std::string("gamma")}); - const std::vector replacement = { - std::string(128, 'x'), // large string (no SSO) - std::string("short"), - std::string(""), // empty - std::string(64, 'y') - }; + const std::vector replacement = {std::string(128, 'x'), // large string (no SSO) + std::string("short"), + std::string(""), // empty + std::string(64, 'y')}; ASSERT_TRUE(arr.ReplaceSubArray(0, std::span(replacement))); ASSERT_EQ(arr.GetSubArray(0).size(), 4u); @@ -225,13 +226,8 @@ TEST(FlatArrayEdges, OpsOnStringFlatArrayAcrossSsoBoundary) { // SSO (small string optimization) typically crosses around 15-22 chars. // Mix short and long strings in the same sub-array, then erase/insert. FlatArray arr; - arr.AppendSubArray({ - std::string("a"), - std::string(""), - std::string("short-ish"), - std::string(100, 'A'), - std::string(1, 'Z') - }); + arr.AppendSubArray( + {std::string("a"), std::string(""), std::string("short-ish"), std::string(100, 'A'), std::string(1, 'Z')}); // Delete the middle (long) element. ASSERT_TRUE(arr.Erase(0, 3)); @@ -263,7 +259,7 @@ TEST(FlatArrayEdges, FlatBoolArrayIsUint8Based_Regression) { "FlatBoolArray must remain FlatArray for SoA safety"); VTX::FlatBoolArray arr; - arr.AppendSubArray({uint8_t{1}, uint8_t{0}, uint8_t{1}}); + arr.AppendSubArray({uint8_t {1}, uint8_t {0}, uint8_t {1}}); ASSERT_EQ(arr.GetSubArray(0).size(), 3u); EXPECT_EQ(arr.GetSubArray(0)[0], 1); EXPECT_EQ(arr.GetSubArray(0)[1], 0); diff --git a/tests/common/test_frame_accessor.cpp b/tests/common/test_frame_accessor.cpp index fd62788..d10c470 100644 --- a/tests/common/test_frame_accessor.cpp +++ b/tests/common/test_frame_accessor.cpp @@ -9,91 +9,63 @@ namespace { -VTX::PropertyContainer MakeCompanion(int32_t level) -{ - VTX::PropertyContainer pc; - pc.int32_properties = {level}; - return pc; -} - -VTX::PropertyAddressCache BuildCache() -{ - VTX::PropertyAddressCache cache; - - cache.name_to_id["Player"] = 7; - auto& player = cache.structs[7]; - player.name = "Player"; - player.properties["Name"] = { - .index = 0, - .type_id = VTX::FieldType::String, - .container_type = VTX::FieldContainerType::None - }; - player.properties["Score"] = { - .index = 0, - .type_id = VTX::FieldType::Int32, - .container_type = VTX::FieldContainerType::None - }; - player.properties["Health"] = { - .index = 0, - .type_id = VTX::FieldType::Float, - .container_type = VTX::FieldContainerType::None - }; - player.properties["IsAlive"] = { - .index = 0, - .type_id = VTX::FieldType::Bool, - .container_type = VTX::FieldContainerType::None - }; - player.properties["Inventory"] = { - .index = 0, - .type_id = VTX::FieldType::Int32, - .container_type = VTX::FieldContainerType::Array - }; - player.properties["Companion"] = { - .index = 0, - .type_id = VTX::FieldType::Struct, - .container_type = VTX::FieldContainerType::None, - .child_type_name = "Companion" - }; - player.properties["History"] = { - .index = 0, - .type_id = VTX::FieldType::Struct, - .container_type = VTX::FieldContainerType::Array, - .child_type_name = "Companion" - }; - player.property_order = { - "Name", "Score", "Health", "IsAlive", "Inventory", "Companion", "History" - }; - - cache.name_to_id["Companion"] = 8; - auto& companion = cache.structs[8]; - companion.name = "Companion"; - companion.properties["Level"] = { - .index = 0, - .type_id = VTX::FieldType::Int32, - .container_type = VTX::FieldContainerType::None - }; - companion.property_order = {"Level"}; - - return cache; -} - -VTX::PropertyContainer BuildEntity() -{ - VTX::PropertyContainer pc; - pc.string_properties = {"Alpha"}; - pc.int32_properties = {7}; - pc.float_properties = {75.0f}; - pc.bool_properties = {true}; - pc.int32_arrays.AppendSubArray({10, 20, 30}); - pc.any_struct_properties = {MakeCompanion(42)}; - pc.any_struct_arrays.AppendSubArray({MakeCompanion(1), MakeCompanion(2)}); - return pc; -} + VTX::PropertyContainer MakeCompanion(int32_t level) { + VTX::PropertyContainer pc; + pc.int32_properties = {level}; + return pc; + } + + VTX::PropertyAddressCache BuildCache() { + VTX::PropertyAddressCache cache; + + cache.name_to_id["Player"] = 7; + auto& player = cache.structs[7]; + player.name = "Player"; + player.properties["Name"] = { + .index = 0, .type_id = VTX::FieldType::String, .container_type = VTX::FieldContainerType::None}; + player.properties["Score"] = { + .index = 0, .type_id = VTX::FieldType::Int32, .container_type = VTX::FieldContainerType::None}; + player.properties["Health"] = { + .index = 0, .type_id = VTX::FieldType::Float, .container_type = VTX::FieldContainerType::None}; + player.properties["IsAlive"] = { + .index = 0, .type_id = VTX::FieldType::Bool, .container_type = VTX::FieldContainerType::None}; + player.properties["Inventory"] = { + .index = 0, .type_id = VTX::FieldType::Int32, .container_type = VTX::FieldContainerType::Array}; + player.properties["Companion"] = {.index = 0, + .type_id = VTX::FieldType::Struct, + .container_type = VTX::FieldContainerType::None, + .child_type_name = "Companion"}; + player.properties["History"] = {.index = 0, + .type_id = VTX::FieldType::Struct, + .container_type = VTX::FieldContainerType::Array, + .child_type_name = "Companion"}; + player.property_order = {"Name", "Score", "Health", "IsAlive", "Inventory", "Companion", "History"}; + + cache.name_to_id["Companion"] = 8; + auto& companion = cache.structs[8]; + companion.name = "Companion"; + companion.properties["Level"] = { + .index = 0, .type_id = VTX::FieldType::Int32, .container_type = VTX::FieldContainerType::None}; + companion.property_order = {"Level"}; + + return cache; + } + + VTX::PropertyContainer BuildEntity() { + VTX::PropertyContainer pc; + pc.string_properties = {"Alpha"}; + pc.int32_properties = {7}; + pc.float_properties = {75.0f}; + pc.bool_properties = {true}; + pc.int32_arrays.AppendSubArray({10, 20, 30}); + pc.any_struct_properties = {MakeCompanion(42)}; + pc.any_struct_arrays.AppendSubArray({MakeCompanion(1), MakeCompanion(2)}); + return pc; + } } // namespace -TEST(FrameAccessor, ResolvesKeysAndReportsAvailableMetadata) -{ +TEST(FrameAccessor, ResolvesKeysAndReportsAvailableMetadata) { VTX::FrameAccessor accessor; accessor.InitializeFromCache(BuildCache()); @@ -119,8 +91,7 @@ TEST(FrameAccessor, ResolvesKeysAndReportsAvailableMetadata) EXPECT_TRUE(accessor.GetPropertiesForStruct("Ghost").empty()); } -TEST(FrameAccessor, EntityViewReadsScalarArrayAndNestedProperties) -{ +TEST(FrameAccessor, EntityViewReadsScalarArrayAndNestedProperties) { VTX::FrameAccessor accessor; accessor.InitializeFromCache(BuildCache()); @@ -155,8 +126,7 @@ TEST(FrameAccessor, EntityViewReadsScalarArrayAndNestedProperties) EXPECT_EQ(VTX::EntityView(history[1]).Get(level_key), 2); } -TEST(FrameAccessor, InvalidKeysReturnDefaultsOrEmptyViews) -{ +TEST(FrameAccessor, InvalidKeysReturnDefaultsOrEmptyViews) { VTX::FrameAccessor accessor; accessor.InitializeFromCache(BuildCache()); diff --git a/tests/common/test_property_address_cache.cpp b/tests/common/test_property_address_cache.cpp index 87990be..567220d 100644 --- a/tests/common/test_property_address_cache.cpp +++ b/tests/common/test_property_address_cache.cpp @@ -8,8 +8,10 @@ #include "util/test_fixtures.h" namespace { - std::string SchemaPath() { return VtxTest::FixturePath("test_schema.json"); } -} + std::string SchemaPath() { + return VtxTest::FixturePath("test_schema.json"); + } +} // namespace TEST(PropertyAddressCache, PopulatedAfterSchemaLoad) { VTX::SchemaRegistry schema; @@ -41,10 +43,11 @@ TEST(PropertyAddressCache, PlayerPropertiesResolveToValidAddresses) { const auto& cache = schema.GetPropertyCache(); const int32_t player_id = cache.name_to_id.at("Player"); - const auto& player = cache.structs.at(player_id); + const auto& player = cache.structs.at(player_id); EXPECT_EQ(player.name, "Player"); - for (const char* field : {"UniqueID","Name","Team","Health","Armor","Position","Rotation","Velocity","IsAlive","Score","Deaths"}) { + for (const char* field : {"UniqueID", "Name", "Team", "Health", "Armor", "Position", "Rotation", "Velocity", + "IsAlive", "Score", "Deaths"}) { ASSERT_TRUE(player.properties.contains(field)) << field; const auto& addr = player.properties.at(field); EXPECT_TRUE(addr.IsValid()) << field; @@ -57,17 +60,17 @@ TEST(PropertyAddressCache, PropertyOrderMatchesSchemaFieldOrder) { const auto& cache = schema.GetPropertyCache(); const int32_t player_id = cache.name_to_id.at("Player"); - const auto& player = cache.structs.at(player_id); + const auto& player = cache.structs.at(player_id); auto ordered = player.GetPropertiesInOrder(); ASSERT_EQ(ordered.size(), 11u); - EXPECT_EQ(ordered[0].name, "UniqueID"); + EXPECT_EQ(ordered[0].name, "UniqueID"); EXPECT_EQ(ordered[10].name, "Deaths"); } TEST(PropertyAddressCache, MakeLookupKeyIsDeterministic) { - using VTX::MakePropertyLookupKey; - using VTX::FieldType; using VTX::FieldContainerType; + using VTX::FieldType; + using VTX::MakePropertyLookupKey; EXPECT_EQ(MakePropertyLookupKey(0, FieldType::Float, FieldContainerType::None), MakePropertyLookupKey(0, FieldType::Float, FieldContainerType::None)); EXPECT_NE(MakePropertyLookupKey(0, FieldType::Float, FieldContainerType::None), diff --git a/tests/common/test_property_container.cpp b/tests/common/test_property_container.cpp index cf69938..509047c 100644 --- a/tests/common/test_property_container.cpp +++ b/tests/common/test_property_container.cpp @@ -18,10 +18,10 @@ using VTX::Helpers::CalculateContainerHash; TEST(PropertyContainer, HashIsDeterministicAcrossCalls) { PropertyContainer a; - a.entity_type_id = 0; + a.entity_type_id = 0; a.string_properties = {"player_1"}; - a.float_properties = {100.0f}; - a.vector_properties = {VTX::Vector{1.0, 2.0, 3.0}}; + a.float_properties = {100.0f}; + a.vector_properties = {VTX::Vector {1.0, 2.0, 3.0}}; const uint64_t h1 = CalculateContainerHash(a); const uint64_t h2 = CalculateContainerHash(a); @@ -31,7 +31,7 @@ TEST(PropertyContainer, HashIsDeterministicAcrossCalls) { TEST(PropertyContainer, HashDiffersWhenFloatValueChanges) { PropertyContainer a; - a.entity_type_id = 0; + a.entity_type_id = 0; a.float_properties = {100.0f}; PropertyContainer b = a; @@ -87,7 +87,7 @@ TEST(PropertyContainer, HashIncludesNestedAnyStruct) { TEST(PropertyContainer, DefaultConstructedHasExpectedDefaults) { PropertyContainer pc; EXPECT_EQ(pc.entity_type_id, -1); - EXPECT_EQ(pc.content_hash, 0u); + EXPECT_EQ(pc.content_hash, 0u); EXPECT_TRUE(pc.bool_properties.empty()); EXPECT_TRUE(pc.float_properties.empty()); EXPECT_TRUE(pc.string_properties.empty()); diff --git a/tests/common/test_schema_registry.cpp b/tests/common/test_schema_registry.cpp index ca0a333..7832f7f 100644 --- a/tests/common/test_schema_registry.cpp +++ b/tests/common/test_schema_registry.cpp @@ -10,8 +10,10 @@ #include "util/test_fixtures.h" namespace { - std::string SchemaPath() { return VtxTest::FixturePath("test_schema.json"); } -} + std::string SchemaPath() { + return VtxTest::FixturePath("test_schema.json"); + } +} // namespace // --------------------------------------------------------------------------- // LoadFromJson @@ -80,7 +82,7 @@ TEST(SchemaRegistry, GetFieldResolvesKnownFields) { ASSERT_TRUE(schema.LoadFromJson(SchemaPath())); EXPECT_NE(schema.GetField("Player", "UniqueID"), nullptr); - EXPECT_NE(schema.GetField("Player", "Health"), nullptr); + EXPECT_NE(schema.GetField("Player", "Health"), nullptr); EXPECT_NE(schema.GetField("Player", "Position"), nullptr); } @@ -89,7 +91,7 @@ TEST(SchemaRegistry, GetFieldReturnsNullForUnknown) { ASSERT_TRUE(schema.LoadFromJson(SchemaPath())); EXPECT_EQ(schema.GetField("Player", "DoesNotExist"), nullptr); - EXPECT_EQ(schema.GetField("Ghost", "Health"), nullptr); + EXPECT_EQ(schema.GetField("Ghost", "Health"), nullptr); } TEST(SchemaRegistry, GetStructTypeIdReturnsStableId) { diff --git a/tests/common/test_schema_registry_errors.cpp b/tests/common/test_schema_registry_errors.cpp index 637bf31..2e754f6 100644 --- a/tests/common/test_schema_registry_errors.cpp +++ b/tests/common/test_schema_registry_errors.cpp @@ -86,7 +86,7 @@ TEST(SchemaRegistryErrors, LoadFromRawStringDuplicateStructIsDeterministic) { (void)ok2; // Both registries must agree on Player existence or absence. - const bool player1_exists = (schema.GetStruct("Player") != nullptr); + const bool player1_exists = (schema.GetStruct("Player") != nullptr); const bool player2_exists = (schema2.GetStruct("Player") != nullptr); EXPECT_EQ(player1_exists, player2_exists); } @@ -117,7 +117,7 @@ TEST(SchemaRegistryErrors, LoadFromRawStringUnknownTypeIdDoesntCrash) { // If the struct was accepted, looking up the bogus field must not crash. if (const auto* s = schema.GetStruct("Ghost")) { - (void)s; // just don't crash + (void)s; // just don't crash } // If not accepted, that's also fine -- both outcomes are valid. SUCCEED(); diff --git a/tests/common/test_time_utils.cpp b/tests/common/test_time_utils.cpp index f6da65f..4a77381 100644 --- a/tests/common/test_time_utils.cpp +++ b/tests/common/test_time_utils.cpp @@ -8,15 +8,13 @@ namespace { -size_t FieldIndex(VTX::FieldType type) -{ - return static_cast(type); -} + size_t FieldIndex(VTX::FieldType type) { + return static_cast(type); + } } // namespace -TEST(TimeUtils, PreparePropertyContainerSizesVectorsFromSchema) -{ +TEST(TimeUtils, PreparePropertyContainerSizesVectorsFromSchema) { VTX::SchemaStruct schema; schema.type_max_indices.resize(FieldIndex(VTX::FieldType::Struct) + 1, 0); schema.type_max_indices[FieldIndex(VTX::FieldType::Bool)] = 2; @@ -43,8 +41,7 @@ TEST(TimeUtils, PreparePropertyContainerSizesVectorsFromSchema) EXPECT_EQ(pc.any_struct_properties.size(), 1u); } -TEST(TimeUtils, ConvertToUeTicksSupportsNumericFormats) -{ +TEST(TimeUtils, ConvertToUeTicksSupportsNumericFormats) { using namespace VTX::TimeUtils; // Use int64{N} (not N##LL) to pick the int64 overload unambiguously. @@ -59,25 +56,19 @@ TEST(TimeUtils, ConvertToUeTicksSupportsNumericFormats) EXPECT_EQ(ConvertToUeTicks(TimeFormat::Seconds, int64 {7}), 0LL); } -TEST(TimeUtils, ConvertToUeTicksParsesIso8601AndRejectsInvalidStrings) -{ +TEST(TimeUtils, ConvertToUeTicksParsesIso8601AndRejectsInvalidStrings) { using namespace VTX::TimeUtils; - EXPECT_EQ(ConvertToUeTicks(TimeFormat::ISO8601, std::string("1970-01-01T00:00:00")), - TICKS_AT_UNIX_EPOCH); + EXPECT_EQ(ConvertToUeTicks(TimeFormat::ISO8601, std::string("1970-01-01T00:00:00")), TICKS_AT_UNIX_EPOCH); EXPECT_EQ(ConvertToUeTicks(TimeFormat::ISO8601, std::string("invalid")), 0LL); EXPECT_EQ(ConvertToUeTicks(TimeFormat::UnixUTC, std::string("1970-01-01T00:00:00")), 0LL); } -TEST(TimeUtils, DurationHelpersFormatPredictably) -{ +TEST(TimeUtils, DurationHelpersFormatPredictably) { using namespace VTX::TimeUtils; - const int64 ticks = - (3LL * 3600 * TICKS_PER_SECOND) + - (2LL * 60 * TICKS_PER_SECOND) + - (5LL * TICKS_PER_SECOND) + - (123LL * TICKS_PER_MILLISECOND); + const int64 ticks = (3LL * 3600 * TICKS_PER_SECOND) + (2LL * 60 * TICKS_PER_SECOND) + (5LL * TICKS_PER_SECOND) + + (123LL * TICKS_PER_MILLISECOND); const auto duration = TicksToDuration(ticks); EXPECT_EQ(duration.hours, 3); @@ -89,8 +80,7 @@ TEST(TimeUtils, DurationHelpersFormatPredictably) EXPECT_NEAR(TicksToSeconds(ticks), 10'925.123, 1e-9); } -TEST(TimeUtils, FormatUtcTicksAcceptsUnixAndUeEpochTicks) -{ +TEST(TimeUtils, FormatUtcTicksAcceptsUnixAndUeEpochTicks) { using namespace VTX::TimeUtils; const std::string unix_epoch = FormatUtcTicks(0); @@ -101,8 +91,7 @@ TEST(TimeUtils, FormatUtcTicksAcceptsUnixAndUeEpochTicks) EXPECT_TRUE(unix_epoch.ends_with("00:00:00.000 UTC")); } -TEST(TimeUtils, CalculateContainerHashIncludesFlatArrayOffsets) -{ +TEST(TimeUtils, CalculateContainerHashIncludesFlatArrayOffsets) { VTX::PropertyContainer a; a.int32_arrays.AppendSubArray({1, 2}); a.int32_arrays.AppendSubArray({3}); @@ -111,6 +100,5 @@ TEST(TimeUtils, CalculateContainerHashIncludesFlatArrayOffsets) b.int32_arrays.AppendSubArray({1}); b.int32_arrays.AppendSubArray({2, 3}); - EXPECT_NE(VTX::Helpers::CalculateContainerHash(a), - VTX::Helpers::CalculateContainerHash(b)); + EXPECT_NE(VTX::Helpers::CalculateContainerHash(a), VTX::Helpers::CalculateContainerHash(b)); } diff --git a/tests/common/test_vtx_game_times.cpp b/tests/common/test_vtx_game_times.cpp index 9f06e36..e1d12f5 100644 --- a/tests/common/test_vtx_game_times.cpp +++ b/tests/common/test_vtx_game_times.cpp @@ -14,9 +14,9 @@ #include #include "vtx/common/vtx_types.h" -using VTX::GameTime::VTXGameTimes; -using VTX::GameTime::GameTimeRegister; using VTX::GameTime::EFilterType; +using VTX::GameTime::GameTimeRegister; +using VTX::GameTime::VTXGameTimes; // --------------------------------------------------------------------------- // Regression: historical UTC must be accepted @@ -32,9 +32,9 @@ TEST(VTXGameTimes, AcceptsHistoricalUtcOnFirstFrame_Regression) { const int64_t historical_utc = 1'745'000'000LL * 10'000'000LL; GameTimeRegister reg; - reg.game_time = 0.0f; + reg.game_time = 0.0f; reg.created_utc_time = historical_utc; - reg.FrameFilterType = EFilterType::OnlyIncreasing; + reg.FrameFilterType = EFilterType::OnlyIncreasing; EXPECT_TRUE(times.AddTimeRegistry(reg)); EXPECT_EQ(times.LastCreatedUtc(), historical_utc); @@ -47,7 +47,7 @@ TEST(VTXGameTimes, AcceptsFutureUtcOnFirstFrame) { const int64_t future_utc = VTXGameTimes::GetUtcNowTicks() + 1'000'000'000LL; GameTimeRegister reg; - reg.game_time = 0.0f; + reg.game_time = 0.0f; reg.created_utc_time = future_utc; EXPECT_TRUE(times.AddTimeRegistry(reg)); @@ -59,16 +59,16 @@ TEST(VTXGameTimes, RejectsUtcRegressionAfterFirstFrame) { VTXGameTimes times; GameTimeRegister first; - first.game_time = 0.0f; - first.created_utc_time = 1'745'000'000'000'000'0LL; // 2025 + first.game_time = 0.0f; + first.created_utc_time = 1'745'000'000'000'000'0LL; // 2025 ASSERT_TRUE(times.AddTimeRegistry(first)); // Simulate the "per-frame" finalize so add_used_ clears. times.ResolveGameTimes(1); GameTimeRegister regressing; - regressing.game_time = 0.016f; - regressing.created_utc_time = *first.created_utc_time - 1; // goes BACKWARD + regressing.game_time = 0.016f; + regressing.created_utc_time = *first.created_utc_time - 1; // goes BACKWARD EXPECT_FALSE(times.AddTimeRegistry(regressing)); } @@ -83,7 +83,7 @@ TEST(VTXGameTimes, OnlyIncreasingAcceptsGameTimeZeroOnFirstFrame_Regression) { VTXGameTimes times; GameTimeRegister reg; - reg.game_time = 0.0f; + reg.game_time = 0.0f; reg.FrameFilterType = EFilterType::OnlyIncreasing; EXPECT_TRUE(times.AddTimeRegistry(reg)); @@ -93,7 +93,7 @@ TEST(VTXGameTimes, OnlyDecreasingAcceptsGameTimeZeroOnFirstFrame_Regression) { VTXGameTimes times; GameTimeRegister reg; - reg.game_time = 0.0f; + reg.game_time = 0.0f; reg.FrameFilterType = EFilterType::OnlyDecreasing; EXPECT_TRUE(times.AddTimeRegistry(reg)); @@ -104,13 +104,13 @@ TEST(VTXGameTimes, OnlyIncreasingRejectsRegressionOnLaterFrame) { VTXGameTimes times; GameTimeRegister a; - a.game_time = 1.0f; + a.game_time = 1.0f; a.FrameFilterType = EFilterType::OnlyIncreasing; ASSERT_TRUE(times.AddTimeRegistry(a)); times.ResolveGameTimes(1); GameTimeRegister b; - b.game_time = 0.5f; // less than previous + b.game_time = 0.5f; // less than previous b.FrameFilterType = EFilterType::OnlyIncreasing; EXPECT_FALSE(times.AddTimeRegistry(b)); @@ -120,13 +120,13 @@ TEST(VTXGameTimes, OnlyDecreasingRejectsAscentOnLaterFrame) { VTXGameTimes times; GameTimeRegister a; - a.game_time = 10.0f; + a.game_time = 10.0f; a.FrameFilterType = EFilterType::OnlyDecreasing; ASSERT_TRUE(times.AddTimeRegistry(a)); times.ResolveGameTimes(1); GameTimeRegister b; - b.game_time = 20.0f; // greater than previous + b.game_time = 20.0f; // greater than previous b.FrameFilterType = EFilterType::OnlyDecreasing; EXPECT_FALSE(times.AddTimeRegistry(b)); @@ -147,7 +147,7 @@ TEST(VTXGameTimes, ClearResetsStartUtcToZero_Regression) { VTXGameTimes times; GameTimeRegister reg; - reg.game_time = 0.0f; + reg.game_time = 0.0f; reg.created_utc_time = 1'745'000'000'000'000'0LL; ASSERT_TRUE(times.AddTimeRegistry(reg)); @@ -167,9 +167,9 @@ TEST(VTXGameTimes, AcceptsManyMonotonicFrames) { for (int i = 0; i < kFrames; ++i) { GameTimeRegister reg; - reg.game_time = float(i) / 60.0f; + reg.game_time = float(i) / 60.0f; reg.created_utc_time = base_utc + int64_t(i) * 166'666LL; - reg.FrameFilterType = EFilterType::OnlyIncreasing; + reg.FrameFilterType = EFilterType::OnlyIncreasing; EXPECT_TRUE(times.AddTimeRegistry(reg)) << "frame " << i; times.ResolveGameTimes(i + 1); @@ -197,9 +197,9 @@ TEST(VTXGameTimes, RejectsSecondAddInSameFrame) { // --------------------------------------------------------------------------- TEST(VTXGameTimes, SecondsToTicksMatchesConventional) { - EXPECT_EQ(VTXGameTimes::SecondsToTicks(0.0f), 0); - EXPECT_EQ(VTXGameTimes::SecondsToTicks(1.0f), 10'000'000); - EXPECT_EQ(VTXGameTimes::SecondsToTicks(0.5f), 5'000'000); + EXPECT_EQ(VTXGameTimes::SecondsToTicks(0.0f), 0); + EXPECT_EQ(VTXGameTimes::SecondsToTicks(1.0f), 10'000'000); + EXPECT_EQ(VTXGameTimes::SecondsToTicks(0.5f), 5'000'000); } TEST(VTXGameTimes, GetUtcNowTicksIsMonotonic) { diff --git a/tests/common/test_vtx_game_times_extended.cpp b/tests/common/test_vtx_game_times_extended.cpp index 64c6d6f..2cd42e4 100644 --- a/tests/common/test_vtx_game_times_extended.cpp +++ b/tests/common/test_vtx_game_times_extended.cpp @@ -10,18 +10,16 @@ using VTX::GameTime::VTXGameTimes; namespace { -GameTimeRegister MakeBoth(float game_time, int64_t created_utc) -{ - GameTimeRegister reg; - reg.game_time = game_time; - reg.created_utc_time = created_utc; - return reg; -} + GameTimeRegister MakeBoth(float game_time, int64_t created_utc) { + GameTimeRegister reg; + reg.game_time = game_time; + reg.created_utc_time = created_utc; + return reg; + } } // namespace -TEST(VTXGameTimesExtended, SetupUsesExplicitStartUtcAndImplicitStartUtc) -{ +TEST(VTXGameTimesExtended, SetupUsesExplicitStartUtcAndImplicitStartUtc) { VTXGameTimes explicit_start; explicit_start.Setup(60.0f, true, 123456789LL); EXPECT_EQ(explicit_start.StartUtc(), 123456789LL); @@ -31,8 +29,7 @@ TEST(VTXGameTimesExtended, SetupUsesExplicitStartUtcAndImplicitStartUtc) EXPECT_GT(implicit_start.StartUtc(), 0); } -TEST(VTXGameTimesExtended, ResolveWithOnlyGameTimeSynthesizesIncreasingUtc) -{ +TEST(VTXGameTimesExtended, ResolveWithOnlyGameTimeSynthesizesIncreasingUtc) { VTXGameTimes times; times.Setup(2.0f, true, 1000LL); @@ -53,8 +50,7 @@ TEST(VTXGameTimesExtended, ResolveWithOnlyGameTimeSynthesizesIncreasingUtc) EXPECT_EQ(times.GetCreatedUtc()[1], times.GetCreatedUtc()[0] + 5'000'000LL); } -TEST(VTXGameTimesExtended, ResolveWithOnlyCreatedUtcSynthesizesRelativeGameTime) -{ +TEST(VTXGameTimesExtended, ResolveWithOnlyCreatedUtcSynthesizesRelativeGameTime) { VTXGameTimes times; times.Setup(60.0f, true, 0); @@ -73,8 +69,7 @@ TEST(VTXGameTimesExtended, ResolveWithOnlyCreatedUtcSynthesizesRelativeGameTime) EXPECT_EQ(times.GetGameTime()[1], 2250LL); } -TEST(VTXGameTimesExtended, ResolveWithoutInputsSynthesizesBothTimesFromFps) -{ +TEST(VTXGameTimesExtended, ResolveWithoutInputsSynthesizesBothTimesFromFps) { VTXGameTimes times; times.Setup(2.0f, true, 1000LL); @@ -90,8 +85,7 @@ TEST(VTXGameTimesExtended, ResolveWithoutInputsSynthesizesBothTimesFromFps) EXPECT_EQ(times.GetCreatedUtc()[1], 5'001'000LL); } -TEST(VTXGameTimesExtended, DetectsTimelineGapWhenUtcJumpExceedsThreshold) -{ +TEST(VTXGameTimesExtended, DetectsTimelineGapWhenUtcJumpExceedsThreshold) { VTXGameTimes times; times.Setup(60.0f, true, 0); @@ -105,8 +99,7 @@ TEST(VTXGameTimesExtended, DetectsTimelineGapWhenUtcJumpExceedsThreshold) EXPECT_EQ(times.GetTimelineGaps()[0], 2); } -TEST(VTXGameTimesExtended, DetectsGameSegmentWhenGameTimeDirectionReverses) -{ +TEST(VTXGameTimesExtended, DetectsGameSegmentWhenGameTimeDirectionReverses) { VTXGameTimes times; times.Setup(60.0f, true, 0); @@ -120,8 +113,7 @@ TEST(VTXGameTimesExtended, DetectsGameSegmentWhenGameTimeDirectionReverses) EXPECT_EQ(times.GetGameSegments()[0], 2); } -TEST(VTXGameTimesExtended, CopyFromKeepsOnlyCurrentChunkAndInsertLiveChunkTimesAppends) -{ +TEST(VTXGameTimesExtended, CopyFromKeepsOnlyCurrentChunkAndInsertLiveChunkTimesAppends) { VTXGameTimes source; source.Setup(60.0f, true, 1000LL); @@ -152,8 +144,7 @@ TEST(VTXGameTimesExtended, CopyFromKeepsOnlyCurrentChunkAndInsertLiveChunkTimesA EXPECT_EQ(appended.GetCreatedUtc()[0], 3000LL); } -TEST(VTXGameTimesExtended, SnapshotRollbackRestoresPreviousState) -{ +TEST(VTXGameTimesExtended, SnapshotRollbackRestoresPreviousState) { VTXGameTimes times; times.Setup(60.0f, true, 1000LL); @@ -167,8 +158,7 @@ TEST(VTXGameTimesExtended, SnapshotRollbackRestoresPreviousState) EXPECT_TRUE(times.GetCreatedUtc().empty()); } -TEST(VTXGameTimesExtended, ResolveRejectsZeroFrameCount) -{ +TEST(VTXGameTimesExtended, ResolveRejectsZeroFrameCount) { VTXGameTimes times; EXPECT_FALSE(times.ResolveGameTimes(0)); } diff --git a/tests/common/test_vtx_game_times_state.cpp b/tests/common/test_vtx_game_times_state.cpp index b388baf..7786232 100644 --- a/tests/common/test_vtx_game_times_state.cpp +++ b/tests/common/test_vtx_game_times_state.cpp @@ -7,19 +7,20 @@ #include #include "vtx/common/vtx_types.h" -using VTX::GameTime::VTXGameTimes; -using VTX::GameTime::GameTimeRegister; using VTX::GameTime::EFilterType; +using VTX::GameTime::GameTimeRegister; +using VTX::GameTime::VTXGameTimes; namespace { -GameTimeRegister MakeReg(float game_time, int64_t utc = -1) { - GameTimeRegister r; - r.game_time = game_time; - if (utc >= 0) r.created_utc_time = utc; - r.FrameFilterType = EFilterType::OnlyIncreasing; - return r; -} + GameTimeRegister MakeReg(float game_time, int64_t utc = -1) { + GameTimeRegister r; + r.game_time = game_time; + if (utc >= 0) + r.created_utc_time = utc; + r.FrameFilterType = EFilterType::OnlyIncreasing; + return r; + } } // namespace @@ -38,7 +39,7 @@ TEST(VTXGameTimesState, RollbackWithoutPriorSnapshotIsSafe) { ASSERT_TRUE(t.AddTimeRegistry(MakeReg(0.1f))); t.ResolveGameTimes(2); - t.Rollback(); // no snapshot -- must not crash + t.Rollback(); // no snapshot -- must not crash SUCCEED(); // After Rollback with no snapshot, we must still be able to Clear safely. @@ -102,7 +103,7 @@ TEST(VTXGameTimesState, ClearThenReuseReturnsToEmptyState) { t.Clear(); EXPECT_TRUE(t.IsEmpty()); - EXPECT_EQ(t.StartUtc(), 0); // regression for the 2026-04-17 fix + EXPECT_EQ(t.StartUtc(), 0); // regression for the 2026-04-17 fix // Fresh state must accept the same data again, including the first frame // being t=0 without triggering the monotonicity filter. diff --git a/tests/differ/test_diff_basic.cpp b/tests/differ/test_diff_basic.cpp index 2bcc6e1..1022ce5 100644 --- a/tests/differ/test_diff_basic.cpp +++ b/tests/differ/test_diff_basic.cpp @@ -18,75 +18,73 @@ namespace { -VTX::PropertyContainer MakePlayer( - const std::string& uid, - const std::string& name, - int team, float health, float armor, - VTX::Vector pos, - bool is_alive = true, - int score = 0, int deaths = 0) -{ - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {uid, name}; - pc.int32_properties = {team, score, deaths}; - pc.float_properties = {health, armor}; - pc.vector_properties = {pos, VTX::Vector{0.0, 0.0, 0.0}}; - pc.quat_properties = {VTX::Quat{0.0f, 0.0f, 0.0f, 1.0f}}; - pc.bool_properties = {is_alive}; - return pc; -} - -/// Builds a two-frame .vtx file from raw frame-builder callbacks. Returns the -/// path of the written file. -template -std::string WriteTwoFrameFile(const std::string& uuid, F1 build_frame_0, F2 build_frame_1) -{ - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = VtxTest::OutputPath("diff_" + uuid + ".vtx"); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "DiffTest"; - cfg.replay_uuid = uuid; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = 16; - cfg.use_compression = true; - - auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); - { - VTX::Frame f0 = build_frame_0(); - VTX::GameTime::GameTimeRegister t; - t.game_time = 0.0f; - writer->RecordFrame(f0, t); + VTX::PropertyContainer MakePlayer(const std::string& uid, const std::string& name, int team, float health, + float armor, VTX::Vector pos, bool is_alive = true, int score = 0, + int deaths = 0) { + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {uid, name}; + pc.int32_properties = {team, score, deaths}; + pc.float_properties = {health, armor}; + pc.vector_properties = {pos, VTX::Vector {0.0, 0.0, 0.0}}; + pc.quat_properties = {VTX::Quat {0.0f, 0.0f, 0.0f, 1.0f}}; + pc.bool_properties = {is_alive}; + return pc; } - { - VTX::Frame f1 = build_frame_1(); - VTX::GameTime::GameTimeRegister t; - t.game_time = 1.0f / 60.0f; - writer->RecordFrame(f1, t); + + /// Builds a two-frame .vtx file from raw frame-builder callbacks. Returns the + /// path of the written file. + template + std::string WriteTwoFrameFile(const std::string& uuid, F1 build_frame_0, F2 build_frame_1) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = VtxTest::OutputPath("diff_" + uuid + ".vtx"); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "DiffTest"; + cfg.replay_uuid = uuid; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = 16; + cfg.use_compression = true; + + auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); + { + VTX::Frame f0 = build_frame_0(); + VTX::GameTime::GameTimeRegister t; + t.game_time = 0.0f; + writer->RecordFrame(f0, t); + } + { + VTX::Frame f1 = build_frame_1(); + VTX::GameTime::GameTimeRegister t; + t.game_time = 1.0f / 60.0f; + writer->RecordFrame(f1, t); + } + writer->Flush(); + writer->Stop(); + return cfg.output_filepath; } - writer->Flush(); - writer->Stop(); - return cfg.output_filepath; -} -/// Opens a file, pulls raw bytes of frames 0 and 1, and runs the differ. -/// Returns the PatchIndex. We need to copy A's bytes because loading B may -/// evict A's chunk. -VtxDiff::PatchIndex DiffFile(const std::string& path, - const VtxDiff::DiffOptions& opts = {}) -{ - auto ctx = VTX::OpenReplayFile(path); - if (!ctx) { ADD_FAILURE() << ctx.error; return {}; } + /// Opens a file, pulls raw bytes of frames 0 and 1, and runs the differ. + /// Returns the PatchIndex. We need to copy A's bytes because loading B may + /// evict A's chunk. + VtxDiff::PatchIndex DiffFile(const std::string& path, const VtxDiff::DiffOptions& opts = {}) { + auto ctx = VTX::OpenReplayFile(path); + if (!ctx) { + ADD_FAILURE() << ctx.error; + return {}; + } - auto differ = VtxDiff::CreateDifferFacade(ctx.format); - if (!differ) { ADD_FAILURE() << "CreateDifferFacade returned nullptr"; return {}; } + auto differ = VtxDiff::CreateDifferFacade(ctx.format); + if (!differ) { + ADD_FAILURE() << "CreateDifferFacade returned nullptr"; + return {}; + } - auto raw_a = ctx.reader->GetRawFrameBytes(0); - std::vector bytes_a(raw_a.begin(), raw_a.end()); + auto raw_a = ctx.reader->GetRawFrameBytes(0); + std::vector bytes_a(raw_a.begin(), raw_a.end()); - auto raw_b = ctx.reader->GetRawFrameBytes(1); - return differ->DiffRawFrames(bytes_a, raw_b, opts); -} + auto raw_b = ctx.reader->GetRawFrameBytes(1); + return differ->DiffRawFrames(bytes_a, raw_b, opts); + } } // namespace @@ -115,8 +113,7 @@ TEST(Differ, IdenticalFramesProduceZeroOps) { VTX::Frame f; auto& bucket = f.CreateBucket("entity"); bucket.unique_ids.push_back("player_0"); - bucket.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, - VTX::Vector{0.0, 0.0, 0.0})); + bucket.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {0.0, 0.0, 0.0})); return f; }; const auto path = WriteTwoFrameFile("identical", build, build); @@ -134,8 +131,7 @@ TEST(Differ, FloatPropertyChangeShowsUpAsReplace) { VTX::Frame f; auto& bucket = f.CreateBucket("entity"); bucket.unique_ids.push_back("player_0"); - bucket.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, - VTX::Vector{0.0, 0.0, 0.0})); + bucket.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {0.0, 0.0, 0.0})); return f; }; const auto build1 = [] { @@ -143,8 +139,7 @@ TEST(Differ, FloatPropertyChangeShowsUpAsReplace) { auto& bucket = f.CreateBucket("entity"); bucket.unique_ids.push_back("player_0"); // Health dropped from 100 -> 75. Everything else identical. - bucket.entities.push_back(MakePlayer("player_0", "Alpha", 1, 75.0f, 50.0f, - VTX::Vector{0.0, 0.0, 0.0})); + bucket.entities.push_back(MakePlayer("player_0", "Alpha", 1, 75.0f, 50.0f, VTX::Vector {0.0, 0.0, 0.0})); return f; }; const auto path = WriteTwoFrameFile("replace_float", build0, build1); @@ -155,9 +150,7 @@ TEST(Differ, FloatPropertyChangeShowsUpAsReplace) { bool saw_float_replace = false; for (const auto& op : patch.operations) { if (op.ContainerType == VtxDiff::EVTXContainerType::FloatProperties && - (op.Operation == VtxDiff::DiffOperation::Replace || - op.Operation == VtxDiff::DiffOperation::ReplaceRange)) - { + (op.Operation == VtxDiff::DiffOperation::Replace || op.Operation == VtxDiff::DiffOperation::ReplaceRange)) { saw_float_replace = true; EXPECT_EQ(op.ActorId, "player_0"); } @@ -170,16 +163,14 @@ TEST(Differ, VectorPropertyChangeShowsUpAsReplace) { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids.push_back("player_0"); - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, - VTX::Vector{0.0, 0.0, 0.0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {0.0, 0.0, 0.0})); return f; }; const auto build1 = [] { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids.push_back("player_0"); - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, - VTX::Vector{10.0, 0.0, 0.0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {10.0, 0.0, 0.0})); return f; }; const auto path = WriteTwoFrameFile("replace_vec", build0, build1); @@ -206,23 +197,21 @@ TEST(Differ, FloatWithinEpsilonProducesNoOp) { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids.push_back("player_0"); - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.000001f, 50.0f, - VTX::Vector{0.0, 0.0, 0.0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.000001f, 50.0f, VTX::Vector {0.0, 0.0, 0.0})); return f; }; const auto build1 = [] { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids.push_back("player_0"); - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.000002f, 50.0f, - VTX::Vector{0.0, 0.0, 0.0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.000002f, 50.0f, VTX::Vector {0.0, 0.0, 0.0})); return f; }; const auto path = WriteTwoFrameFile("epsilon_ignored", build0, build1); VtxDiff::DiffOptions opts; opts.compare_floats_with_epsilon = true; - opts.float_epsilon = 1e-3f; + opts.float_epsilon = 1e-3f; auto patch = DiffFile(path, opts); // Any float-containing op would be unexpected with epsilon this large. @@ -236,23 +225,21 @@ TEST(Differ, FloatOutsideEpsilonProducesOp) { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids.push_back("player_0"); - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, - VTX::Vector{0.0, 0.0, 0.0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {0.0, 0.0, 0.0})); return f; }; const auto build1 = [] { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids.push_back("player_0"); - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 110.0f, 50.0f, - VTX::Vector{0.0, 0.0, 0.0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 110.0f, 50.0f, VTX::Vector {0.0, 0.0, 0.0})); return f; }; const auto path = WriteTwoFrameFile("epsilon_exceeded", build0, build1); VtxDiff::DiffOptions opts; opts.compare_floats_with_epsilon = true; - opts.float_epsilon = 1e-3f; + opts.float_epsilon = 1e-3f; auto patch = DiffFile(path, opts); bool saw_float = false; @@ -273,15 +260,15 @@ TEST(Differ, EntityAddedBetweenFramesYieldsAddOp) { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids.push_back("player_0"); - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector{0,0,0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {0, 0, 0})); return f; }; const auto build1 = [] { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids = {"player_0", "player_1"}; - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector{0,0,0})); - b.entities.push_back(MakePlayer("player_1", "Bravo", 2, 80.0f, 40.0f, VTX::Vector{5,0,0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {0, 0, 0})); + b.entities.push_back(MakePlayer("player_1", "Bravo", 2, 80.0f, 40.0f, VTX::Vector {5, 0, 0})); return f; }; const auto path = WriteTwoFrameFile("added_entity", build0, build1); @@ -289,7 +276,8 @@ TEST(Differ, EntityAddedBetweenFramesYieldsAddOp) { bool saw_add = false; for (const auto& op : patch.operations) { - if (op.Operation == VtxDiff::DiffOperation::Add) saw_add = true; + if (op.Operation == VtxDiff::DiffOperation::Add) + saw_add = true; } EXPECT_TRUE(saw_add); } @@ -299,15 +287,15 @@ TEST(Differ, EntityRemovedBetweenFramesYieldsRemoveOp) { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids = {"player_0", "player_1"}; - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector{0,0,0})); - b.entities.push_back(MakePlayer("player_1", "Bravo", 2, 80.0f, 40.0f, VTX::Vector{5,0,0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {0, 0, 0})); + b.entities.push_back(MakePlayer("player_1", "Bravo", 2, 80.0f, 40.0f, VTX::Vector {5, 0, 0})); return f; }; const auto build1 = [] { VTX::Frame f; auto& b = f.CreateBucket("entity"); b.unique_ids.push_back("player_0"); - b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector{0,0,0})); + b.entities.push_back(MakePlayer("player_0", "Alpha", 1, 100.0f, 50.0f, VTX::Vector {0, 0, 0})); return f; }; const auto path = WriteTwoFrameFile("removed_entity", build0, build1); @@ -315,7 +303,8 @@ TEST(Differ, EntityRemovedBetweenFramesYieldsRemoveOp) { bool saw_remove = false; for (const auto& op : patch.operations) { - if (op.Operation == VtxDiff::DiffOperation::Remove) saw_remove = true; + if (op.Operation == VtxDiff::DiffOperation::Remove) + saw_remove = true; } EXPECT_TRUE(saw_remove); } @@ -336,14 +325,15 @@ TEST(DiffIndexPath, AppendProducesExpectedSequence) { TEST(DiffIndexPath, PushBackSilentlyDropsOverflow) { VtxDiff::DiffIndexPath p; - for (int i = 0; i < 20; ++i) p.push_back(i); // capacity is 16 + for (int i = 0; i < 20; ++i) + p.push_back(i); // capacity is 16 EXPECT_EQ(p.count, 16); } TEST(DiffIndexPath, EqualityUsesCountAndContents) { - auto a = VtxDiff::DiffIndexPath{}.Append(1).Append(2); - auto b = VtxDiff::DiffIndexPath{}.Append(1).Append(2); - auto c = VtxDiff::DiffIndexPath{}.Append(1).Append(3); + auto a = VtxDiff::DiffIndexPath {}.Append(1).Append(2); + auto b = VtxDiff::DiffIndexPath {}.Append(1).Append(2); + auto c = VtxDiff::DiffIndexPath {}.Append(1).Append(3); EXPECT_EQ(a, b); EXPECT_FALSE(a == c); } diff --git a/tests/differ/test_diff_edges.cpp b/tests/differ/test_diff_edges.cpp index 704d59f..918827e 100644 --- a/tests/differ/test_diff_edges.cpp +++ b/tests/differ/test_diff_edges.cpp @@ -20,57 +20,63 @@ namespace { -template -std::string WriteTwoFrameFile(const std::string& uuid, F0 build0, F1 build1) { - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = VtxTest::OutputPath("diffedge_" + uuid + ".vtx"); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "DiffEdgeTest"; - cfg.replay_uuid = uuid; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = 16; - cfg.use_compression = true; + template + std::string WriteTwoFrameFile(const std::string& uuid, F0 build0, F1 build1) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = VtxTest::OutputPath("diffedge_" + uuid + ".vtx"); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "DiffEdgeTest"; + cfg.replay_uuid = uuid; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = 16; + cfg.use_compression = true; - { - auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); - VTX::Frame f0 = build0(); - VTX::Frame f1 = build1(); - VTX::GameTime::GameTimeRegister t0; t0.game_time = 0.0f; - VTX::GameTime::GameTimeRegister t1; t1.game_time = 1.0f / 60.0f; - writer->RecordFrame(f0, t0); - writer->RecordFrame(f1, t1); - writer->Flush(); - writer->Stop(); + { + auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); + VTX::Frame f0 = build0(); + VTX::Frame f1 = build1(); + VTX::GameTime::GameTimeRegister t0; + t0.game_time = 0.0f; + VTX::GameTime::GameTimeRegister t1; + t1.game_time = 1.0f / 60.0f; + writer->RecordFrame(f0, t0); + writer->RecordFrame(f1, t1); + writer->Flush(); + writer->Stop(); + } + return cfg.output_filepath; } - return cfg.output_filepath; -} -VtxDiff::PatchIndex DiffFile(const std::string& path, - const VtxDiff::DiffOptions& opts = {}) -{ - auto ctx = VTX::OpenReplayFile(path); - if (!ctx) { ADD_FAILURE() << ctx.error; return {}; } - auto differ = VtxDiff::CreateDifferFacade(ctx.format); - if (!differ) { ADD_FAILURE() << "no differ"; return {}; } + VtxDiff::PatchIndex DiffFile(const std::string& path, const VtxDiff::DiffOptions& opts = {}) { + auto ctx = VTX::OpenReplayFile(path); + if (!ctx) { + ADD_FAILURE() << ctx.error; + return {}; + } + auto differ = VtxDiff::CreateDifferFacade(ctx.format); + if (!differ) { + ADD_FAILURE() << "no differ"; + return {}; + } - auto raw_a = ctx.reader->GetRawFrameBytes(0); - std::vector bytes_a(raw_a.begin(), raw_a.end()); - auto raw_b = ctx.reader->GetRawFrameBytes(1); - return differ->DiffRawFrames(bytes_a, raw_b, opts); -} + auto raw_a = ctx.reader->GetRawFrameBytes(0); + std::vector bytes_a(raw_a.begin(), raw_a.end()); + auto raw_b = ctx.reader->GetRawFrameBytes(1); + return differ->DiffRawFrames(bytes_a, raw_b, opts); + } -// Minimal entity with only unique_id + entity_type_id so tests stay readable. -VTX::PropertyContainer MakeMinimalPlayer(const std::string& uid) { - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {uid, "name"}; - pc.int32_properties = {1, 0, 0}; - pc.float_properties = {100.0f, 50.0f}; - pc.vector_properties = {VTX::Vector{}, VTX::Vector{}}; - pc.quat_properties = {VTX::Quat{}}; - pc.bool_properties = {true}; - return pc; -} + // Minimal entity with only unique_id + entity_type_id so tests stay readable. + VTX::PropertyContainer MakeMinimalPlayer(const std::string& uid) { + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {uid, "name"}; + pc.int32_properties = {1, 0, 0}; + pc.float_properties = {100.0f, 50.0f}; + pc.vector_properties = {VTX::Vector {}, VTX::Vector {}}; + pc.quat_properties = {VTX::Quat {}}; + pc.bool_properties = {true}; + return pc; + } } // namespace @@ -90,7 +96,7 @@ TEST(DiffEdges, DiffBetweenFramesWithEmptyBuckets) { const auto build = [] { VTX::Frame f; auto& b = f.CreateBucket("entity"); - (void)b; // empty bucket, no entities + (void)b; // empty bucket, no entities return f; }; const auto path = WriteTwoFrameFile("empty_buckets", build, build); @@ -142,8 +148,7 @@ TEST(DiffEdges, DiffWithByteArrays) { auto& b = f.CreateBucket("entity"); auto pc = MakeMinimalPlayer("bytes_owner"); pc.byte_array_properties.AppendSubArray( - std::span( - reinterpret_cast("\x01\x02\x03\x04"), 4)); + std::span(reinterpret_cast("\x01\x02\x03\x04"), 4)); b.unique_ids.push_back("bytes_owner"); b.entities.push_back(std::move(pc)); return f; @@ -153,8 +158,7 @@ TEST(DiffEdges, DiffWithByteArrays) { auto& b = f.CreateBucket("entity"); auto pc = MakeMinimalPlayer("bytes_owner"); pc.byte_array_properties.AppendSubArray( - std::span( - reinterpret_cast("\x01\x02\x99\x04"), 4)); + std::span(reinterpret_cast("\x01\x02\x99\x04"), 4)); b.unique_ids.push_back("bytes_owner"); b.entities.push_back(std::move(pc)); return f; @@ -188,7 +192,7 @@ TEST(DiffEdges, DiffWithNestedAnyStructProperties) { auto pc = MakeMinimalPlayer("nested_owner"); VTX::PropertyContainer inner; - inner.float_properties = {2.71f}; // changed + inner.float_properties = {2.71f}; // changed pc.any_struct_properties.push_back(std::move(inner)); b.unique_ids.push_back("nested_owner"); @@ -229,7 +233,7 @@ TEST(DiffEdges, DiffWithEntityReplacedUnderSameUniqueId) { VTX::Frame f; auto& b = f.CreateBucket("entity"); auto pc = MakeMinimalPlayer("same_id"); - pc.int32_properties = {1, 100, 5}; // score=100, deaths=5 + pc.int32_properties = {1, 100, 5}; // score=100, deaths=5 b.unique_ids.push_back("same_id"); b.entities.push_back(std::move(pc)); return f; @@ -238,7 +242,7 @@ TEST(DiffEdges, DiffWithEntityReplacedUnderSameUniqueId) { VTX::Frame f; auto& b = f.CreateBucket("entity"); auto pc = MakeMinimalPlayer("same_id"); - pc.int32_properties = {2, 999, 0}; // totally different stats + pc.int32_properties = {2, 999, 0}; // totally different stats pc.float_properties = {25.0f, 0.0f}; b.unique_ids.push_back("same_id"); b.entities.push_back(std::move(pc)); @@ -251,7 +255,10 @@ TEST(DiffEdges, DiffWithEntityReplacedUnderSameUniqueId) { // At least one op should reference the shared actor id. bool found = false; for (const auto& op : patch.operations) { - if (op.ActorId == "same_id") { found = true; break; } + if (op.ActorId == "same_id") { + found = true; + break; + } } EXPECT_TRUE(found); } diff --git a/tests/differ/test_diff_parametrized.cpp b/tests/differ/test_diff_parametrized.cpp index e7b2325..c76de2e 100644 --- a/tests/differ/test_diff_parametrized.cpp +++ b/tests/differ/test_diff_parametrized.cpp @@ -14,100 +14,87 @@ namespace { -const char* FormatName(VTX::VtxFormat format) -{ - return format == VTX::VtxFormat::FlatBuffers ? "FlatBuffers" : "Protobuf"; -} - -std::string UniqueOutputPath(VTX::VtxFormat format, const std::string& suffix) -{ - const auto* info = ::testing::UnitTest::GetInstance()->current_test_info(); - return VtxTest::OutputPath( - VtxTest::SanitizePathComponent(std::string(info->test_suite_name())) + "_" + - VtxTest::SanitizePathComponent(std::string(info->name())) + "_" + - FormatName(format) + "_" + suffix + ".vtx"); -} - -std::unique_ptr CreateWriter(VTX::VtxFormat format, - const VTX::WriterFacadeConfig& cfg) -{ - return format == VTX::VtxFormat::FlatBuffers - ? VTX::CreateFlatBuffersWriterFacade(cfg) - : VTX::CreateProtobuffWriterFacade(cfg); -} - -VTX::PropertyContainer MakePlayer(float health) -{ - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {"player_0", "Alpha"}; - pc.int32_properties = {1, 0, 0}; - pc.float_properties = {health, 50.0f}; - pc.vector_properties = {VTX::Vector{0.0, 0.0, 0.0}, VTX::Vector{1.0, 0.0, 0.0}}; - pc.quat_properties = {VTX::Quat{0.0f, 0.0f, 0.0f, 1.0f}}; - pc.bool_properties = {true}; - return pc; -} + const char* FormatName(VTX::VtxFormat format) { + return format == VTX::VtxFormat::FlatBuffers ? "FlatBuffers" : "Protobuf"; + } -template -std::string WriteTwoFrameReplay(VTX::VtxFormat format, - const std::string& path, - F0 build0, - F1 build1) -{ - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = path; - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "DiffParametrized"; - cfg.replay_uuid = "diff-param"; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = 8; - cfg.use_compression = true; - - auto writer = CreateWriter(format, cfg); - { - auto frame = build0(); - VTX::GameTime::GameTimeRegister t; - t.game_time = 0.0f; - writer->RecordFrame(frame, t); + std::string UniqueOutputPath(VTX::VtxFormat format, const std::string& suffix) { + const auto* info = ::testing::UnitTest::GetInstance()->current_test_info(); + return VtxTest::OutputPath(VtxTest::SanitizePathComponent(std::string(info->test_suite_name())) + "_" + + VtxTest::SanitizePathComponent(std::string(info->name())) + "_" + + FormatName(format) + "_" + suffix + ".vtx"); } - { - auto frame = build1(); - VTX::GameTime::GameTimeRegister t; - t.game_time = 1.0f / 60.0f; - writer->RecordFrame(frame, t); + + std::unique_ptr CreateWriter(VTX::VtxFormat format, const VTX::WriterFacadeConfig& cfg) { + return format == VTX::VtxFormat::FlatBuffers ? VTX::CreateFlatBuffersWriterFacade(cfg) + : VTX::CreateProtobuffWriterFacade(cfg); } - writer->Stop(); - return path; -} -VtxDiff::PatchIndex DiffFrames(VTX::VtxFormat format, const std::string& path) -{ - auto ctx = VTX::OpenReplayFile(path); - if (!ctx) { - ADD_FAILURE() << ctx.error; - return {}; + VTX::PropertyContainer MakePlayer(float health) { + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {"player_0", "Alpha"}; + pc.int32_properties = {1, 0, 0}; + pc.float_properties = {health, 50.0f}; + pc.vector_properties = {VTX::Vector {0.0, 0.0, 0.0}, VTX::Vector {1.0, 0.0, 0.0}}; + pc.quat_properties = {VTX::Quat {0.0f, 0.0f, 0.0f, 1.0f}}; + pc.bool_properties = {true}; + return pc; } - EXPECT_EQ(ctx.format, format); - auto differ = VtxDiff::CreateDifferFacade(format); - if (!differ) { - ADD_FAILURE() << "CreateDifferFacade returned nullptr"; - return {}; + template + std::string WriteTwoFrameReplay(VTX::VtxFormat format, const std::string& path, F0 build0, F1 build1) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = path; + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "DiffParametrized"; + cfg.replay_uuid = "diff-param"; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = 8; + cfg.use_compression = true; + + auto writer = CreateWriter(format, cfg); + { + auto frame = build0(); + VTX::GameTime::GameTimeRegister t; + t.game_time = 0.0f; + writer->RecordFrame(frame, t); + } + { + auto frame = build1(); + VTX::GameTime::GameTimeRegister t; + t.game_time = 1.0f / 60.0f; + writer->RecordFrame(frame, t); + } + writer->Stop(); + return path; } - auto raw_a = ctx.reader->GetRawFrameBytes(0); - std::vector bytes_a(raw_a.begin(), raw_a.end()); - const auto raw_b = ctx.reader->GetRawFrameBytes(1); - return differ->DiffRawFrames(bytes_a, raw_b); -} + VtxDiff::PatchIndex DiffFrames(VTX::VtxFormat format, const std::string& path) { + auto ctx = VTX::OpenReplayFile(path); + if (!ctx) { + ADD_FAILURE() << ctx.error; + return {}; + } + EXPECT_EQ(ctx.format, format); + + auto differ = VtxDiff::CreateDifferFacade(format); + if (!differ) { + ADD_FAILURE() << "CreateDifferFacade returned nullptr"; + return {}; + } + + auto raw_a = ctx.reader->GetRawFrameBytes(0); + std::vector bytes_a(raw_a.begin(), raw_a.end()); + const auto raw_b = ctx.reader->GetRawFrameBytes(1); + return differ->DiffRawFrames(bytes_a, raw_b); + } } // namespace class DifferParametrizedTest : public ::testing::TestWithParam {}; -TEST_P(DifferParametrizedTest, IdenticalFramesProduceZeroOpsOnBothBackends) -{ +TEST_P(DifferParametrizedTest, IdenticalFramesProduceZeroOpsOnBothBackends) { const auto build = [] { VTX::Frame f; auto& bucket = f.CreateBucket("entity"); @@ -121,8 +108,7 @@ TEST_P(DifferParametrizedTest, IdenticalFramesProduceZeroOpsOnBothBackends) EXPECT_TRUE(patch.operations.empty()); } -TEST_P(DifferParametrizedTest, FloatReplaceIsDetectedOnBothBackends) -{ +TEST_P(DifferParametrizedTest, FloatReplaceIsDetectedOnBothBackends) { const auto build0 = [] { VTX::Frame f; auto& bucket = f.CreateBucket("entity"); @@ -144,8 +130,7 @@ TEST_P(DifferParametrizedTest, FloatReplaceIsDetectedOnBothBackends) bool saw_float_replace = false; for (const auto& op : patch.operations) { if (op.ContainerType == VtxDiff::EVTXContainerType::FloatProperties && - (op.Operation == VtxDiff::DiffOperation::Replace || - op.Operation == VtxDiff::DiffOperation::ReplaceRange)) { + (op.Operation == VtxDiff::DiffOperation::Replace || op.Operation == VtxDiff::DiffOperation::ReplaceRange)) { saw_float_replace = true; EXPECT_EQ(op.ActorId, "player_0"); } @@ -153,8 +138,7 @@ TEST_P(DifferParametrizedTest, FloatReplaceIsDetectedOnBothBackends) EXPECT_TRUE(saw_float_replace); } -TEST_P(DifferParametrizedTest, EmptyInputsReturnEmptyPatch) -{ +TEST_P(DifferParametrizedTest, EmptyInputsReturnEmptyPatch) { auto differ = VtxDiff::CreateDifferFacade(GetParam()); ASSERT_TRUE(differ); @@ -164,10 +148,8 @@ TEST_P(DifferParametrizedTest, EmptyInputsReturnEmptyPatch) EXPECT_TRUE(patch.operations.empty()); } -INSTANTIATE_TEST_SUITE_P( - BothBackends, - DifferParametrizedTest, - ::testing::Values(VTX::VtxFormat::FlatBuffers, VTX::VtxFormat::Protobuf), - [](const ::testing::TestParamInfo& info) { - return std::string(FormatName(info.param)); - }); +INSTANTIATE_TEST_SUITE_P(BothBackends, DifferParametrizedTest, + ::testing::Values(VTX::VtxFormat::FlatBuffers, VTX::VtxFormat::Protobuf), + [](const ::testing::TestParamInfo& info) { + return std::string(FormatName(info.param)); + }); diff --git a/tests/differ/test_diff_utils.cpp b/tests/differ/test_diff_utils.cpp index ea27699..60606b9 100644 --- a/tests/differ/test_diff_utils.cpp +++ b/tests/differ/test_diff_utils.cpp @@ -10,35 +10,28 @@ namespace { -template -std::span BytesOf(const T& value) -{ - return std::as_bytes(std::span(&value, 1)); -} + template + std::span BytesOf(const T& value) { + return std::as_bytes(std::span(&value, 1)); + } -template -std::span BytesOf(const std::array& value) -{ - return std::as_bytes(std::span(value)); -} + template + std::span BytesOf(const std::array& value) { + return std::as_bytes(std::span(value)); + } } // namespace -TEST(DiffUtils, TypeToFieldNameReturnsExpectedNames) -{ - EXPECT_EQ(VtxDiff::TypeToFieldName(VtxDiff::EVTXContainerType::FloatProperties), - "float_properties"); - EXPECT_EQ(VtxDiff::TypeToFieldName(VtxDiff::EVTXContainerType::AnyStructArrays), - "any_struct_arrays"); +TEST(DiffUtils, TypeToFieldNameReturnsExpectedNames) { + EXPECT_EQ(VtxDiff::TypeToFieldName(VtxDiff::EVTXContainerType::FloatProperties), "float_properties"); + EXPECT_EQ(VtxDiff::TypeToFieldName(VtxDiff::EVTXContainerType::AnyStructArrays), "any_struct_arrays"); EXPECT_EQ(VtxDiff::TypeToFieldName(VtxDiff::EVTXContainerType::Unknown), "Unknown"); EXPECT_EQ(VtxDiff::TypeToFieldName(static_cast(255)), ""); } -TEST(DiffUtils, PathHelpersBuildCanonicalRepresentations) -{ +TEST(DiffUtils, PathHelpersBuildCanonicalRepresentations) { VtxDiff::PathS path("Root"); - EXPECT_EQ(path.Append("Field").Index(3).Key("player_0").ToString(), - "Root.Field[3]{player_0}"); + EXPECT_EQ(path.Append("Field").Index(3).Key("player_0").ToString(), "Root.Field[3]{player_0}"); EXPECT_EQ(VtxDiff::MakeFieldPath("", "Field"), "Field"); EXPECT_EQ(VtxDiff::MakeFieldPath("Root", "Field"), "Root.Field"); @@ -48,44 +41,42 @@ TEST(DiffUtils, PathHelpersBuildCanonicalRepresentations) EXPECT_FALSE(VtxDiff::EndsWith("Root.Field", ".Other")); } -TEST(DiffUtils, IsArraysTypeRecognizesArrayContainers) -{ +TEST(DiffUtils, IsArraysTypeRecognizesArrayContainers) { EXPECT_TRUE(VtxDiff::IsArraysType(VtxDiff::EVTXContainerType::Int32Arrays)); EXPECT_TRUE(VtxDiff::IsArraysType(VtxDiff::EVTXContainerType::AnyStructArrays)); EXPECT_FALSE(VtxDiff::IsArraysType(VtxDiff::EVTXContainerType::Int32Properties)); EXPECT_FALSE(VtxDiff::IsArraysType(VtxDiff::EVTXContainerType::Unknown)); } -TEST(DiffUtils, AreScalarsEqualUsesEpsilonAndFallsBackToByteComparison) -{ +TEST(DiffUtils, AreScalarsEqualUsesEpsilonAndFallsBackToByteComparison) { VtxDiff::DiffOptions relaxed; relaxed.compare_floats_with_epsilon = true; relaxed.float_epsilon = 1e-3f; const float fa = 1.0f; const float fb = 1.0005f; - EXPECT_TRUE(VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::FloatProperties, - BytesOf(fa), BytesOf(fb), relaxed)); + EXPECT_TRUE( + VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::FloatProperties, BytesOf(fa), BytesOf(fb), relaxed)); std::array vec_a = {1.0f, 2.0f, 3.0f}; std::array vec_b = {1.0f, 2.0005f, 3.0f}; - EXPECT_TRUE(VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::VectorProperties, - BytesOf(vec_a), BytesOf(vec_b), relaxed)); + EXPECT_TRUE(VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::VectorProperties, BytesOf(vec_a), BytesOf(vec_b), + relaxed)); std::array quat_a = {0.0f, 0.0f, 0.0f, 1.0f}; std::array quat_b = {0.0f, 0.0f, 0.0f, 1.0005f}; - EXPECT_TRUE(VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::QuatProperties, - BytesOf(quat_a), BytesOf(quat_b), relaxed)); + EXPECT_TRUE(VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::QuatProperties, BytesOf(quat_a), BytesOf(quat_b), + relaxed)); VtxDiff::DiffOptions strict; strict.compare_floats_with_epsilon = false; - EXPECT_FALSE(VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::FloatProperties, - BytesOf(fa), BytesOf(fb), strict)); + EXPECT_FALSE( + VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::FloatProperties, BytesOf(fa), BytesOf(fb), strict)); const int32_t ia = 7; const int32_t ib = 8; - EXPECT_TRUE(VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::Int32Properties, - BytesOf(ia), BytesOf(ia), relaxed)); - EXPECT_FALSE(VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::Int32Properties, - BytesOf(ia), BytesOf(ib), relaxed)); + EXPECT_TRUE( + VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::Int32Properties, BytesOf(ia), BytesOf(ia), relaxed)); + EXPECT_FALSE( + VtxDiff::AreScalarsEqual(VtxDiff::EVTXContainerType::Int32Properties, BytesOf(ia), BytesOf(ib), relaxed)); } diff --git a/tests/reader/test_corrupt_files.cpp b/tests/reader/test_corrupt_files.cpp index 77f2e1b..4732d8a 100644 --- a/tests/reader/test_corrupt_files.cpp +++ b/tests/reader/test_corrupt_files.cpp @@ -22,59 +22,59 @@ namespace { -// Writes a tiny valid FlatBuffers replay so we have a baseline file we can -// then mutate for each corruption test. -std::string WriteValidFlatBuffersFile(const std::string& uuid) { - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = VtxTest::OutputPath("corrupt_" + uuid + ".vtx"); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "CorruptFileTest"; - cfg.replay_uuid = uuid; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = 10; - cfg.use_compression = true; - - { - auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); - for (int i = 0; i < 5; ++i) { - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); - - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {"p", "name"}; - pc.int32_properties = {1, 0, 0}; - pc.float_properties = {100.0f, 50.0f}; - pc.vector_properties = {VTX::Vector{}, VTX::Vector{}}; - pc.quat_properties = {VTX::Quat{}}; - pc.bool_properties = {true}; - - bucket.unique_ids.push_back("p"); - bucket.entities.push_back(std::move(pc)); - - VTX::GameTime::GameTimeRegister t; - t.game_time = float(i) / 60.0f; - writer->RecordFrame(f, t); + // Writes a tiny valid FlatBuffers replay so we have a baseline file we can + // then mutate for each corruption test. + std::string WriteValidFlatBuffersFile(const std::string& uuid) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = VtxTest::OutputPath("corrupt_" + uuid + ".vtx"); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "CorruptFileTest"; + cfg.replay_uuid = uuid; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = 10; + cfg.use_compression = true; + + { + auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); + for (int i = 0; i < 5; ++i) { + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); + + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {"p", "name"}; + pc.int32_properties = {1, 0, 0}; + pc.float_properties = {100.0f, 50.0f}; + pc.vector_properties = {VTX::Vector {}, VTX::Vector {}}; + pc.quat_properties = {VTX::Quat {}}; + pc.bool_properties = {true}; + + bucket.unique_ids.push_back("p"); + bucket.entities.push_back(std::move(pc)); + + VTX::GameTime::GameTimeRegister t; + t.game_time = float(i) / 60.0f; + writer->RecordFrame(f, t); + } + writer->Flush(); + writer->Stop(); } - writer->Flush(); - writer->Stop(); + return cfg.output_filepath; } - return cfg.output_filepath; -} -// Writes `content` verbatim to disk and returns the path. -std::string WriteRawBytes(const std::string& name, std::span content) { - const auto path = VtxTest::OutputPath(name); - std::ofstream ofs(path, std::ios::binary | std::ios::trunc); - ofs.write(reinterpret_cast(content.data()), static_cast(content.size())); - ofs.close(); - return path; -} + // Writes `content` verbatim to disk and returns the path. + std::string WriteRawBytes(const std::string& name, std::span content) { + const auto path = VtxTest::OutputPath(name); + std::ofstream ofs(path, std::ios::binary | std::ios::trunc); + ofs.write(reinterpret_cast(content.data()), static_cast(content.size())); + ofs.close(); + return path; + } -// Truncates an existing file down to `target_size` bytes. -void TruncateFile(const std::string& path, std::uintmax_t target_size) { - std::filesystem::resize_file(path, target_size); -} + // Truncates an existing file down to `target_size` bytes. + void TruncateFile(const std::string& path, std::uintmax_t target_size) { + std::filesystem::resize_file(path, target_size); + } } // namespace @@ -83,7 +83,7 @@ void TruncateFile(const std::string& path, std::uintmax_t target_size) { // =========================================================================== TEST(CorruptFile, EmptyFileReturnsError) { - const auto path = WriteRawBytes("empty.vtx", std::span{}); + const auto path = WriteRawBytes("empty.vtx", std::span {}); auto ctx = VTX::OpenReplayFile(path); EXPECT_FALSE(ctx); EXPECT_FALSE(ctx.error.empty()); @@ -104,8 +104,7 @@ TEST(CorruptFile, ValidMagicButTruncatedHeader) { // Just the magic bytes "VTXF" -- nothing else. The header reader must // detect truncation instead of reading past EOF. const uint8_t bytes[] = {'V', 'T', 'X', 'F'}; - const auto path = WriteRawBytes("truncated_magic.vtx", - std::span(bytes, sizeof(bytes))); + const auto path = WriteRawBytes("truncated_magic.vtx", std::span(bytes, sizeof(bytes))); auto ctx = VTX::OpenReplayFile(path); EXPECT_FALSE(ctx); @@ -125,13 +124,12 @@ TEST(CorruptFile, TruncatedBeforeFooter) { ASSERT_GT(file_size, 100u); const auto truncated = VtxTest::OutputPath("truncated_before_footer.mutated.vtx"); - std::filesystem::copy_file(original, truncated, - std::filesystem::copy_options::overwrite_existing); + std::filesystem::copy_file(original, truncated, std::filesystem::copy_options::overwrite_existing); TruncateFile(truncated, file_size / 2); auto ctx = VTX::OpenReplayFile(truncated); EXPECT_FALSE(ctx); - EXPECT_FALSE(ctx.error.empty()); // must surface something, not crash + EXPECT_FALSE(ctx.error.empty()); // must surface something, not crash } TEST(CorruptFile, CorruptFooterSize) { @@ -140,8 +138,7 @@ TEST(CorruptFile, CorruptFooterSize) { // instead of seeking to garbage (bug A1 variant). const auto original = WriteValidFlatBuffersFile("corrupt_footer_size"); const auto mutated = VtxTest::OutputPath("corrupt_footer_size.mutated.vtx"); - std::filesystem::copy_file(original, mutated, - std::filesystem::copy_options::overwrite_existing); + std::filesystem::copy_file(original, mutated, std::filesystem::copy_options::overwrite_existing); { std::fstream f(mutated, std::ios::in | std::ios::out | std::ios::binary); @@ -168,9 +165,8 @@ TEST(CorruptFile, ChunkOffsetBeyondEof) { // GetFrameSync must refuse to read past EOF and return nullptr rather than // returning stale bytes (bug A3). const auto original = WriteValidFlatBuffersFile("chunk_past_eof"); - const auto mutated = VtxTest::OutputPath("chunk_past_eof.mutated.vtx"); - std::filesystem::copy_file(original, mutated, - std::filesystem::copy_options::overwrite_existing); + const auto mutated = VtxTest::OutputPath("chunk_past_eof.mutated.vtx"); + std::filesystem::copy_file(original, mutated, std::filesystem::copy_options::overwrite_existing); const auto size = std::filesystem::file_size(mutated); ASSERT_GT(size, 100u); @@ -189,7 +185,7 @@ TEST(CorruptFile, ChunkOffsetBeyondEof) { // is that nothing crashes. for (int i = 0; i < ctx.reader->GetTotalFrames(); ++i) { const VTX::Frame* f = ctx.reader->GetFrameSync(i); - (void)f; // may be null, may not -- but must not crash + (void)f; // may be null, may not -- but must not crash } } @@ -202,8 +198,8 @@ TEST(CorruptFile, GetFrameSyncNegativeIndex) { auto ctx = VTX::OpenReplayFile(path); ASSERT_TRUE(ctx); - EXPECT_EQ(ctx.reader->GetFrameSync(-1), nullptr); - EXPECT_EQ(ctx.reader->GetFrameSync(-100), nullptr); + EXPECT_EQ(ctx.reader->GetFrameSync(-1), nullptr); + EXPECT_EQ(ctx.reader->GetFrameSync(-100), nullptr); } TEST(CorruptFile, GetFrameSyncIndexTooLarge) { @@ -212,7 +208,7 @@ TEST(CorruptFile, GetFrameSyncIndexTooLarge) { ASSERT_TRUE(ctx); const int total = ctx.reader->GetTotalFrames(); - EXPECT_EQ(ctx.reader->GetFrameSync(total), nullptr); - EXPECT_EQ(ctx.reader->GetFrameSync(total + 100), nullptr); - EXPECT_EQ(ctx.reader->GetFrameSync(1'000'000'000), nullptr); + EXPECT_EQ(ctx.reader->GetFrameSync(total), nullptr); + EXPECT_EQ(ctx.reader->GetFrameSync(total + 100), nullptr); + EXPECT_EQ(ctx.reader->GetFrameSync(1'000'000'000), nullptr); } diff --git a/tests/reader/test_open_replay_edges.cpp b/tests/reader/test_open_replay_edges.cpp index a2d4eb7..6177e3c 100644 --- a/tests/reader/test_open_replay_edges.cpp +++ b/tests/reader/test_open_replay_edges.cpp @@ -17,36 +17,36 @@ namespace { -std::string WriteTinyReplay(const std::string& uuid) { - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = VtxTest::OutputPath("open_edge_" + uuid + ".vtx"); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "OpenReplayEdges"; - cfg.replay_uuid = uuid; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = 10; - cfg.use_compression = true; - - auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {"p", "name"}; - pc.int32_properties = {1, 0, 0}; - pc.float_properties = {100.0f, 50.0f}; - pc.vector_properties = {VTX::Vector{}, VTX::Vector{}}; - pc.quat_properties = {VTX::Quat{}}; - pc.bool_properties = {true}; - bucket.unique_ids.push_back("p"); - bucket.entities.push_back(std::move(pc)); - VTX::GameTime::GameTimeRegister t; - t.game_time = 0.0f; - writer->RecordFrame(f, t); - writer->Flush(); - writer->Stop(); - return cfg.output_filepath; -} + std::string WriteTinyReplay(const std::string& uuid) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = VtxTest::OutputPath("open_edge_" + uuid + ".vtx"); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "OpenReplayEdges"; + cfg.replay_uuid = uuid; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = 10; + cfg.use_compression = true; + + auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {"p", "name"}; + pc.int32_properties = {1, 0, 0}; + pc.float_properties = {100.0f, 50.0f}; + pc.vector_properties = {VTX::Vector {}, VTX::Vector {}}; + pc.quat_properties = {VTX::Quat {}}; + pc.bool_properties = {true}; + bucket.unique_ids.push_back("p"); + bucket.entities.push_back(std::move(pc)); + VTX::GameTime::GameTimeRegister t; + t.game_time = 0.0f; + writer->RecordFrame(f, t); + writer->Flush(); + writer->Stop(); + return cfg.output_filepath; + } } // namespace @@ -91,8 +91,7 @@ TEST(OpenReplayEdges, RelativePathResolvesAgainstCurrentWorkingDir) { // Compute the relative path from cwd. const auto cwd = std::filesystem::current_path(); std::error_code ec; - const auto relative_path = - std::filesystem::relative(absolute_path, cwd, ec); + const auto relative_path = std::filesystem::relative(absolute_path, cwd, ec); ASSERT_FALSE(ec) << ec.message(); ASSERT_FALSE(relative_path.empty()); @@ -114,8 +113,7 @@ TEST(OpenReplayEdges, UnicodeFilenameDoesNotCrash) { const auto unicode_path = VtxTest::OutputPath("replay_\xc3\xa1\xe3\x83\x86_tests.vtx"); std::error_code ec; - std::filesystem::copy_file(good_path, unicode_path, - std::filesystem::copy_options::overwrite_existing, ec); + std::filesystem::copy_file(good_path, unicode_path, std::filesystem::copy_options::overwrite_existing, ec); if (ec) { // Some filesystems / locales reject non-ASCII filenames. In that // case we've still shown the API doesn't crash on the attempt. diff --git a/tests/reader/test_reader_api.cpp b/tests/reader/test_reader_api.cpp index 5ea29ad..7f1d558 100644 --- a/tests/reader/test_reader_api.cpp +++ b/tests/reader/test_reader_api.cpp @@ -15,100 +15,81 @@ namespace { -const char* FormatName(VTX::VtxFormat format) -{ - return format == VTX::VtxFormat::FlatBuffers ? "FlatBuffers" : "Protobuf"; -} + const char* FormatName(VTX::VtxFormat format) { + return format == VTX::VtxFormat::FlatBuffers ? "FlatBuffers" : "Protobuf"; + } -std::string UniqueOutputPath(VTX::VtxFormat format, const std::string& suffix) -{ - const auto* info = ::testing::UnitTest::GetInstance()->current_test_info(); - return VtxTest::OutputPath( - VtxTest::SanitizePathComponent(std::string(info->test_suite_name())) + "_" + - VtxTest::SanitizePathComponent(std::string(info->name())) + "_" + - FormatName(format) + "_" + suffix + ".vtx"); -} + std::string UniqueOutputPath(VTX::VtxFormat format, const std::string& suffix) { + const auto* info = ::testing::UnitTest::GetInstance()->current_test_info(); + return VtxTest::OutputPath(VtxTest::SanitizePathComponent(std::string(info->test_suite_name())) + "_" + + VtxTest::SanitizePathComponent(std::string(info->name())) + "_" + + FormatName(format) + "_" + suffix + ".vtx"); + } -VTX::WriterFacadeConfig MakeConfig(VTX::VtxFormat format, - const std::string& suffix, - int32_t chunk_max_frames) -{ - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = UniqueOutputPath(format, suffix); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "ReaderApiTest"; - cfg.replay_uuid = "reader-api"; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = chunk_max_frames; - cfg.use_compression = true; - return cfg; -} + VTX::WriterFacadeConfig MakeConfig(VTX::VtxFormat format, const std::string& suffix, int32_t chunk_max_frames) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = UniqueOutputPath(format, suffix); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "ReaderApiTest"; + cfg.replay_uuid = "reader-api"; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = chunk_max_frames; + cfg.use_compression = true; + return cfg; + } -std::unique_ptr CreateWriter(VTX::VtxFormat format, - const VTX::WriterFacadeConfig& cfg) -{ - return format == VTX::VtxFormat::FlatBuffers - ? VTX::CreateFlatBuffersWriterFacade(cfg) - : VTX::CreateProtobuffWriterFacade(cfg); -} + std::unique_ptr CreateWriter(VTX::VtxFormat format, const VTX::WriterFacadeConfig& cfg) { + return format == VTX::VtxFormat::FlatBuffers ? VTX::CreateFlatBuffersWriterFacade(cfg) + : VTX::CreateProtobuffWriterFacade(cfg); + } -VTX::Frame MakePlayerFrame(int frame_index) -{ - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); - - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {"player_0", "Alpha"}; - pc.int32_properties = {1, frame_index, 0}; - pc.float_properties = {100.0f - float(frame_index), 50.0f}; - pc.vector_properties = { - VTX::Vector{double(frame_index), 0.0, 0.0}, - VTX::Vector{1.0, 0.0, 0.0} - }; - pc.quat_properties = {VTX::Quat{0.0f, 0.0f, 0.0f, 1.0f}}; - pc.bool_properties = {true}; - - bucket.unique_ids.push_back("player_0"); - bucket.entities.push_back(std::move(pc)); - return f; -} + VTX::Frame MakePlayerFrame(int frame_index) { + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); + + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {"player_0", "Alpha"}; + pc.int32_properties = {1, frame_index, 0}; + pc.float_properties = {100.0f - float(frame_index), 50.0f}; + pc.vector_properties = {VTX::Vector {double(frame_index), 0.0, 0.0}, VTX::Vector {1.0, 0.0, 0.0}}; + pc.quat_properties = {VTX::Quat {0.0f, 0.0f, 0.0f, 1.0f}}; + pc.bool_properties = {true}; + + bucket.unique_ids.push_back("player_0"); + bucket.entities.push_back(std::move(pc)); + return f; + } -void WriteReplay(VTX::VtxFormat format, - const std::string& path, - int frames, - int32_t chunk_max_frames) -{ - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = path; - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "ReaderApiTest"; - cfg.replay_uuid = "reader-api"; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = chunk_max_frames; - cfg.use_compression = true; - - auto writer = CreateWriter(format, cfg); - for (int i = 0; i < frames; ++i) { - auto frame = MakePlayerFrame(i); - VTX::GameTime::GameTimeRegister t; - t.game_time = float(i) / 60.0f; - writer->RecordFrame(frame, t); + void WriteReplay(VTX::VtxFormat format, const std::string& path, int frames, int32_t chunk_max_frames) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = path; + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "ReaderApiTest"; + cfg.replay_uuid = "reader-api"; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = chunk_max_frames; + cfg.use_compression = true; + + auto writer = CreateWriter(format, cfg); + for (int i = 0; i < frames; ++i) { + auto frame = MakePlayerFrame(i); + VTX::GameTime::GameTimeRegister t; + t.game_time = float(i) / 60.0f; + writer->RecordFrame(frame, t); + } + writer->Stop(); } - writer->Stop(); -} -int ReadScore(const VTX::Frame& frame) -{ - return frame.GetBuckets()[0].entities[0].int32_properties[1]; -} + int ReadScore(const VTX::Frame& frame) { + return frame.GetBuckets()[0].entities[0].int32_properties[1]; + } } // namespace class ReaderApiTest : public ::testing::TestWithParam {}; -TEST_P(ReaderApiTest, CreateAccessorReadsSchemaDrivenEntityValues) -{ +TEST_P(ReaderApiTest, CreateAccessorReadsSchemaDrivenEntityValues) { const auto cfg = MakeConfig(GetParam(), "accessor", 8); WriteReplay(GetParam(), cfg.output_filepath, 1, 8); @@ -136,8 +117,7 @@ TEST_P(ReaderApiTest, CreateAccessorReadsSchemaDrivenEntityValues) EXPECT_FALSE(accessor.Get("Player", "Name").IsValid()); } -TEST_P(ReaderApiTest, FrameRangeAndContextReturnExpectedFrames) -{ +TEST_P(ReaderApiTest, FrameRangeAndContextReturnExpectedFrames) { const auto cfg = MakeConfig(GetParam(), "range_context", 8); WriteReplay(GetParam(), cfg.output_filepath, 5, 8); @@ -159,8 +139,7 @@ TEST_P(ReaderApiTest, FrameRangeAndContextReturnExpectedFrames) EXPECT_EQ(ReadScore(context[2]), 3); } -TEST_P(ReaderApiTest, OutOfBoundsQueriesReturnEmptyResults) -{ +TEST_P(ReaderApiTest, OutOfBoundsQueriesReturnEmptyResults) { const auto cfg = MakeConfig(GetParam(), "oob", 8); WriteReplay(GetParam(), cfg.output_filepath, 2, 8); @@ -175,8 +154,7 @@ TEST_P(ReaderApiTest, OutOfBoundsQueriesReturnEmptyResults) EXPECT_TRUE(range.empty()); } -TEST(ReaderApiFlatBuffers, CacheWindowZeroEvictsPreviousChunks) -{ +TEST(ReaderApiFlatBuffers, CacheWindowZeroEvictsPreviousChunks) { const auto path = VtxTest::OutputPath("ReaderApiFlatBuffers_CacheWindowZeroEvictsPreviousChunks.vtx"); WriteReplay(VTX::VtxFormat::FlatBuffers, path, 3, 1); @@ -225,10 +203,8 @@ TEST(ReaderApiFlatBuffers, CacheWindowZeroEvictsPreviousChunks) // the EWMA and still trigger laterals. This catches a regression of // the EWMA logic (hitting >= 5*N) while tolerating reasonable // variations in bootstrap behaviour. -TEST(ReaderApiFlatBuffers, RandomAccessSkipsLateralPrefetches) -{ - const auto path = VtxTest::OutputPath( - "ReaderApiFlatBuffers_RandomAccessSkipsLateralPrefetches.vtx"); +TEST(ReaderApiFlatBuffers, RandomAccessSkipsLateralPrefetches) { + const auto path = VtxTest::OutputPath("ReaderApiFlatBuffers_RandomAccessSkipsLateralPrefetches.vtx"); // 20 chunks of 5 frames each -> 100 frames total. Small enough // to run in milliseconds, big enough that jumps of 10 exceed the // window of 2. @@ -242,9 +218,7 @@ TEST(ReaderApiFlatBuffers, RandomAccessSkipsLateralPrefetches) // The indices below map to 10 chunks, each jump distance 10 chunks // (well above window=2), guaranteeing the EWMA crosses its // threshold after ~2 jumps. - const std::vector jump_frames{ - 0, 50, 10, 60, 20, 70, 30, 80, 40, 90 - }; + const std::vector jump_frames {0, 50, 10, 60, 20, 70, 30, 80, 40, 90}; for (int32_t f : jump_frames) { ASSERT_NE(ctx.reader->GetFrameSync(f), nullptr); @@ -261,8 +235,7 @@ TEST(ReaderApiFlatBuffers, RandomAccessSkipsLateralPrefetches) // this would be ~5x. EXPECT_LE(total_seen, jump_frames.size() * 2) << "Expected random access to skip lateral prefetches " - << "but saw " << total_seen << " chunks loaded vs " - << jump_frames.size() << " jumps."; + << "but saw " << total_seen << " chunks loaded vs " << jump_frames.size() << " jumps."; } // Regression for the "stale-cancelled prefetch blocks re-entry" bug. @@ -313,11 +286,9 @@ TEST(ReaderApiFlatBuffers, CancelledPrefetchReEntersWindow) { // §3.A regression coverage. WarmAt must trigger an asynchronous load // of the chunk containing `frame_index` without blocking the caller, // and without requiring a subsequent GetFrame to fire the load. -TEST(ReaderApiFlatBuffers, WarmAtTriggersAsyncLoadWithoutReading) -{ - const auto path = VtxTest::OutputPath( - "ReaderApiFlatBuffers_WarmAtTriggersAsyncLoadWithoutReading.vtx"); - WriteReplay(VTX::VtxFormat::FlatBuffers, path, 50, 10); // 5 chunks +TEST(ReaderApiFlatBuffers, WarmAtTriggersAsyncLoadWithoutReading) { + const auto path = VtxTest::OutputPath("ReaderApiFlatBuffers_WarmAtTriggersAsyncLoadWithoutReading.vtx"); + WriteReplay(VTX::VtxFormat::FlatBuffers, path, 50, 10); // 5 chunks auto ctx = VTX::OpenReplayFile(path); ASSERT_TRUE(ctx) << ctx.error; @@ -339,8 +310,7 @@ TEST(ReaderApiFlatBuffers, WarmAtTriggersAsyncLoadWithoutReading) const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (std::chrono::steady_clock::now() < deadline) { auto snap = ctx.chunk_state->GetSnapshot(); - if (std::find(snap.loaded_chunks.begin(), snap.loaded_chunks.end(), 3) - != snap.loaded_chunks.end()) { + if (std::find(snap.loaded_chunks.begin(), snap.loaded_chunks.end(), 3) != snap.loaded_chunks.end()) { break; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); @@ -350,14 +320,11 @@ TEST(ReaderApiFlatBuffers, WarmAtTriggersAsyncLoadWithoutReading) auto snap = ctx.chunk_state->GetSnapshot(); ASSERT_FALSE(snap.loaded_chunks.empty()); - EXPECT_NE(std::find(snap.loaded_chunks.begin(), snap.loaded_chunks.end(), 3), - snap.loaded_chunks.end()); + EXPECT_NE(std::find(snap.loaded_chunks.begin(), snap.loaded_chunks.end(), 3), snap.loaded_chunks.end()); } -INSTANTIATE_TEST_SUITE_P( - BothBackends, - ReaderApiTest, - ::testing::Values(VTX::VtxFormat::FlatBuffers, VTX::VtxFormat::Protobuf), - [](const ::testing::TestParamInfo& info) { - return std::string(FormatName(info.param)); - }); +INSTANTIATE_TEST_SUITE_P(BothBackends, ReaderApiTest, + ::testing::Values(VTX::VtxFormat::FlatBuffers, VTX::VtxFormat::Protobuf), + [](const ::testing::TestParamInfo& info) { + return std::string(FormatName(info.param)); + }); diff --git a/tests/reader/test_reader_context.cpp b/tests/reader/test_reader_context.cpp index 688b1ee..2b083c7 100644 --- a/tests/reader/test_reader_context.cpp +++ b/tests/reader/test_reader_context.cpp @@ -19,46 +19,46 @@ namespace { -// Writes a tiny 5-frame FlatBuffers replay and returns the path. -// uuid is used both as the filename tail and as the replay UUID so it's easy -// to identify which test produced which artefact on disk. -std::string WriteTinyFlatBuffersFile(const std::string& uuid) { - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = VtxTest::OutputPath("reader_" + uuid + ".vtx"); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "ReaderContextTest"; - cfg.replay_uuid = uuid; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = 10; - cfg.use_compression = true; - - { - auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); - for (int i = 0; i < 5; ++i) { - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); - - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {"p", "name"}; - pc.int32_properties = {1, 0, 0}; - pc.float_properties = {100.0f, 50.0f}; - pc.vector_properties = {VTX::Vector{}, VTX::Vector{}}; - pc.quat_properties = {VTX::Quat{}}; - pc.bool_properties = {true}; - - bucket.unique_ids.push_back("p"); - bucket.entities.push_back(std::move(pc)); - - VTX::GameTime::GameTimeRegister t; - t.game_time = float(i) / 60.0f; - writer->RecordFrame(f, t); - } - writer->Flush(); - writer->Stop(); - } // release -- file is fully closed before returning - return cfg.output_filepath; -} + // Writes a tiny 5-frame FlatBuffers replay and returns the path. + // uuid is used both as the filename tail and as the replay UUID so it's easy + // to identify which test produced which artefact on disk. + std::string WriteTinyFlatBuffersFile(const std::string& uuid) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = VtxTest::OutputPath("reader_" + uuid + ".vtx"); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "ReaderContextTest"; + cfg.replay_uuid = uuid; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = 10; + cfg.use_compression = true; + + { + auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); + for (int i = 0; i < 5; ++i) { + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); + + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {"p", "name"}; + pc.int32_properties = {1, 0, 0}; + pc.float_properties = {100.0f, 50.0f}; + pc.vector_properties = {VTX::Vector {}, VTX::Vector {}}; + pc.quat_properties = {VTX::Quat {}}; + pc.bool_properties = {true}; + + bucket.unique_ids.push_back("p"); + bucket.entities.push_back(std::move(pc)); + + VTX::GameTime::GameTimeRegister t; + t.game_time = float(i) / 60.0f; + writer->RecordFrame(f, t); + } + writer->Flush(); + writer->Stop(); + } // release -- file is fully closed before returning + return cfg.output_filepath; + } } // namespace @@ -73,15 +73,14 @@ class ReaderContextHappy : public ::testing::Test { // Name output after the current test so parallel ctest invocations // don't race over the same filename. const auto* info = ::testing::UnitTest::GetInstance()->current_test_info(); - const std::string uuid = - std::string(info->test_suite_name()) + "_" + info->name(); + const std::string uuid = std::string(info->test_suite_name()) + "_" + info->name(); path_ = WriteTinyFlatBuffersFile(uuid); - ctx_ = VTX::OpenReplayFile(path_); + ctx_ = VTX::OpenReplayFile(path_); ASSERT_TRUE(ctx_) << ctx_.error; } - std::string path_; + std::string path_; VTX::ReaderContext ctx_; }; diff --git a/tests/util/test_fixtures.h b/tests/util/test_fixtures.h index f363d66..cc07bef 100644 --- a/tests/util/test_fixtures.h +++ b/tests/util/test_fixtures.h @@ -20,59 +20,57 @@ namespace VtxTest { -/// Absolute path to the read-only fixtures directory. -inline std::string FixturePath(const std::string& name) { - return std::string(VTX_TEST_FIXTURES_DIR) + "/" + name; -} + /// Absolute path to the read-only fixtures directory. + inline std::string FixturePath(const std::string& name) { + return std::string(VTX_TEST_FIXTURES_DIR) + "/" + name; + } -/// Absolute path for transient test artefacts. Directory is created by the -/// build system but we recreate on demand in case the tester nuked it. -inline std::string OutputPath(const std::string& name) { - std::filesystem::path p = VTX_TEST_OUTPUT_DIR; - std::filesystem::create_directories(p); - return (p / name).string(); -} + /// Absolute path for transient test artefacts. Directory is created by the + /// build system but we recreate on demand in case the tester nuked it. + inline std::string OutputPath(const std::string& name) { + std::filesystem::path p = VTX_TEST_OUTPUT_DIR; + std::filesystem::create_directories(p); + return (p / name).string(); + } -/// Replaces path separators and other problematic filename characters so test -/// names can be embedded safely into OutputPath() results. -inline std::string SanitizePathComponent(std::string value) { - for (char& ch : value) { - const unsigned char uch = static_cast(ch); - if (std::isalnum(uch) || ch == '.' || ch == '_' || ch == '-') { - continue; + /// Replaces path separators and other problematic filename characters so test + /// names can be embedded safely into OutputPath() results. + inline std::string SanitizePathComponent(std::string value) { + for (char& ch : value) { + const unsigned char uch = static_cast(ch); + if (std::isalnum(uch) || ch == '.' || ch == '_' || ch == '-') { + continue; + } + ch = '_'; } - ch = '_'; + return value; } - return value; -} -/// Reads an entire file into memory. Returns empty on failure. -inline std::vector ReadAllBytes(const std::string& path) { - std::ifstream ifs(path, std::ios::binary | std::ios::ate); - if (!ifs) return {}; - const auto sz = ifs.tellg(); - if (sz <= 0) return {}; - ifs.seekg(0); - std::vector buf(static_cast(sz)); - ifs.read(reinterpret_cast(buf.data()), sz); - return buf; -} + /// Reads an entire file into memory. Returns empty on failure. + inline std::vector ReadAllBytes(const std::string& path) { + std::ifstream ifs(path, std::ios::binary | std::ios::ate); + if (!ifs) + return {}; + const auto sz = ifs.tellg(); + if (sz <= 0) + return {}; + ifs.seekg(0); + std::vector buf(static_cast(sz)); + ifs.read(reinterpret_cast(buf.data()), sz); + return buf; + } -/// Builds a minimal PropertyContainer with a predictable shape. Useful for -/// content_hash and diff assertions that don't need a full schema. -inline VTX::PropertyContainer MakeSimpleEntity( - int32_t type_id, - float health, - VTX::Vector position, - const std::string& name = "entity") -{ - VTX::PropertyContainer pc; - pc.entity_type_id = type_id; - pc.string_properties = { name }; - pc.float_properties = { health }; - pc.vector_properties = { position }; - pc.content_hash = VTX::Helpers::CalculateContainerHash(pc); - return pc; -} + /// Builds a minimal PropertyContainer with a predictable shape. Useful for + /// content_hash and diff assertions that don't need a full schema. + inline VTX::PropertyContainer MakeSimpleEntity(int32_t type_id, float health, VTX::Vector position, + const std::string& name = "entity") { + VTX::PropertyContainer pc; + pc.entity_type_id = type_id; + pc.string_properties = {name}; + pc.float_properties = {health}; + pc.vector_properties = {position}; + pc.content_hash = VTX::Helpers::CalculateContainerHash(pc); + return pc; + } } // namespace VtxTest diff --git a/tests/util/test_main.cpp b/tests/util/test_main.cpp index b8e2031..6cc2d14 100644 --- a/tests/util/test_main.cpp +++ b/tests/util/test_main.cpp @@ -6,18 +6,16 @@ namespace { -// Install a silent sink on the VTX logger so info/debug noise doesn't spam the -// test runner. Tests that want to assert on logger output can pull from an -// override sink instead (see util/test_fixtures.h). -void InstallSilentLoggerSink() -{ - VTX::Logger::Instance().AddSink([](const VTX::Logger::Entry&) {}); -} + // Install a silent sink on the VTX logger so info/debug noise doesn't spam the + // test runner. Tests that want to assert on logger output can pull from an + // override sink instead (see util/test_fixtures.h). + void InstallSilentLoggerSink() { + VTX::Logger::Instance().AddSink([](const VTX::Logger::Entry&) {}); + } } // namespace -int main(int argc, char** argv) -{ +int main(int argc, char** argv) { InstallSilentLoggerSink(); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/tests/writer/test_roundtrip.cpp b/tests/writer/test_roundtrip.cpp index 7c61e68..3fa3121 100644 --- a/tests/writer/test_roundtrip.cpp +++ b/tests/writer/test_roundtrip.cpp @@ -17,34 +17,31 @@ namespace { -constexpr int kTotalFrames = 120; // enough to force multiple chunks -constexpr int kChunkMaxFrames = 40; // = 3 chunks -constexpr float kFps = 60.0f; + constexpr int kTotalFrames = 120; // enough to force multiple chunks + constexpr int kChunkMaxFrames = 40; // = 3 chunks + constexpr float kFps = 60.0f; -const char* FormatName(VTX::VtxFormat f) { - return f == VTX::VtxFormat::FlatBuffers ? "FlatBuffers" : "Protobuf"; -} + const char* FormatName(VTX::VtxFormat f) { + return f == VTX::VtxFormat::FlatBuffers ? "FlatBuffers" : "Protobuf"; + } -VTX::Frame BuildFrame(int frame_index) { - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); - - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = { "player_0", "Alpha" }; - pc.int32_properties = { 1, frame_index, 0 }; // Team, Score(=frame), Deaths - pc.float_properties = { 100.0f - float(frame_index), 50.0f }; - pc.vector_properties = { - VTX::Vector{ double(frame_index), 0.0, 0.0 }, - VTX::Vector{ 1.0, 0.0, 0.0 } - }; - pc.quat_properties = { VTX::Quat{0.0f, 0.0f, 0.0f, 1.0f} }; - pc.bool_properties = { true }; - - bucket.unique_ids.push_back("player_0"); - bucket.entities.push_back(std::move(pc)); - return f; -} + VTX::Frame BuildFrame(int frame_index) { + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); + + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {"player_0", "Alpha"}; + pc.int32_properties = {1, frame_index, 0}; // Team, Score(=frame), Deaths + pc.float_properties = {100.0f - float(frame_index), 50.0f}; + pc.vector_properties = {VTX::Vector {double(frame_index), 0.0, 0.0}, VTX::Vector {1.0, 0.0, 0.0}}; + pc.quat_properties = {VTX::Quat {0.0f, 0.0f, 0.0f, 1.0f}}; + pc.bool_properties = {true}; + + bucket.unique_ids.push_back("player_0"); + bucket.entities.push_back(std::move(pc)); + return f; + } } // namespace @@ -55,27 +52,22 @@ VTX::Frame BuildFrame(int frame_index) { class RoundtripTest : public ::testing::TestWithParam { protected: - VTX::WriterFacadeConfig MakeConfig(const std::string& suffix, - const std::string& uuid) const - { + VTX::WriterFacadeConfig MakeConfig(const std::string& suffix, const std::string& uuid) const { VTX::WriterFacadeConfig cfg; - cfg.output_filepath = VtxTest::OutputPath( - std::string("roundtrip_") + FormatName(GetParam()) + "_" + suffix + ".vtx"); + cfg.output_filepath = + VtxTest::OutputPath(std::string("roundtrip_") + FormatName(GetParam()) + "_" + suffix + ".vtx"); cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "RoundtripTest"; - cfg.replay_uuid = uuid; - cfg.default_fps = kFps; + cfg.replay_name = "RoundtripTest"; + cfg.replay_uuid = uuid; + cfg.default_fps = kFps; cfg.chunk_max_frames = kChunkMaxFrames; - cfg.use_compression = true; + cfg.use_compression = true; return cfg; } - std::unique_ptr CreateWriter( - const VTX::WriterFacadeConfig& cfg) const - { - return GetParam() == VTX::VtxFormat::FlatBuffers - ? VTX::CreateFlatBuffersWriterFacade(cfg) - : VTX::CreateProtobuffWriterFacade(cfg); + std::unique_ptr CreateWriter(const VTX::WriterFacadeConfig& cfg) const { + return GetParam() == VTX::VtxFormat::FlatBuffers ? VTX::CreateFlatBuffersWriterFacade(cfg) + : VTX::CreateProtobuffWriterFacade(cfg); } }; @@ -147,11 +139,11 @@ TEST_P(RoundtripTest, AcceptsHistoricalUtc) { auto writer = CreateWriter(cfg); ASSERT_TRUE(writer); - const int64_t base_utc = 1'745'000'000LL * 10'000'000LL; // 2025-04-19 + const int64_t base_utc = 1'745'000'000LL * 10'000'000LL; // 2025-04-19 for (int i = 0; i < 20; ++i) { auto frame = BuildFrame(i); VTX::GameTime::GameTimeRegister t; - t.game_time = float(i) / kFps; + t.game_time = float(i) / kFps; t.created_utc_time = base_utc + int64_t(i) * 166'666LL; writer->RecordFrame(frame, t); } @@ -172,11 +164,8 @@ TEST_P(RoundtripTest, AcceptsHistoricalUtc) { // BothBackends/RoundtripTest.AcceptsHistoricalUtc/Protobuf // --------------------------------------------------------------------------- -INSTANTIATE_TEST_SUITE_P( - BothBackends, - RoundtripTest, - ::testing::Values(VTX::VtxFormat::FlatBuffers, VTX::VtxFormat::Protobuf), - [](const ::testing::TestParamInfo& info) { - return std::string(FormatName(info.param)); - } -); +INSTANTIATE_TEST_SUITE_P(BothBackends, RoundtripTest, + ::testing::Values(VTX::VtxFormat::FlatBuffers, VTX::VtxFormat::Protobuf), + [](const ::testing::TestParamInfo& info) { + return std::string(FormatName(info.param)); + }); diff --git a/tests/writer/test_writer_advanced.cpp b/tests/writer/test_writer_advanced.cpp index 73a61de..8c3a81a 100644 --- a/tests/writer/test_writer_advanced.cpp +++ b/tests/writer/test_writer_advanced.cpp @@ -14,96 +14,81 @@ namespace { -const char* FormatName(VTX::VtxFormat format) -{ - return format == VTX::VtxFormat::FlatBuffers ? "FlatBuffers" : "Protobuf"; -} + const char* FormatName(VTX::VtxFormat format) { + return format == VTX::VtxFormat::FlatBuffers ? "FlatBuffers" : "Protobuf"; + } -std::string UniqueOutputPath(VTX::VtxFormat format, const std::string& suffix) -{ - const auto* info = ::testing::UnitTest::GetInstance()->current_test_info(); - return VtxTest::OutputPath( - VtxTest::SanitizePathComponent(std::string(info->test_suite_name())) + "_" + - VtxTest::SanitizePathComponent(std::string(info->name())) + "_" + - FormatName(format) + "_" + suffix + ".vtx"); -} + std::string UniqueOutputPath(VTX::VtxFormat format, const std::string& suffix) { + const auto* info = ::testing::UnitTest::GetInstance()->current_test_info(); + return VtxTest::OutputPath(VtxTest::SanitizePathComponent(std::string(info->test_suite_name())) + "_" + + VtxTest::SanitizePathComponent(std::string(info->name())) + "_" + + FormatName(format) + "_" + suffix + ".vtx"); + } -VTX::WriterFacadeConfig MakeConfig(VTX::VtxFormat format, - const std::string& suffix, - int32_t chunk_max_frames, - bool use_compression) -{ - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = UniqueOutputPath(format, suffix); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "WriterAdvancedTest"; - cfg.replay_uuid = ""; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = chunk_max_frames; - cfg.use_compression = use_compression; - return cfg; -} + VTX::WriterFacadeConfig MakeConfig(VTX::VtxFormat format, const std::string& suffix, int32_t chunk_max_frames, + bool use_compression) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = UniqueOutputPath(format, suffix); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "WriterAdvancedTest"; + cfg.replay_uuid = ""; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = chunk_max_frames; + cfg.use_compression = use_compression; + return cfg; + } -std::unique_ptr CreateWriter(VTX::VtxFormat format, - const VTX::WriterFacadeConfig& cfg) -{ - return format == VTX::VtxFormat::FlatBuffers - ? VTX::CreateFlatBuffersWriterFacade(cfg) - : VTX::CreateProtobuffWriterFacade(cfg); -} + std::unique_ptr CreateWriter(VTX::VtxFormat format, const VTX::WriterFacadeConfig& cfg) { + return format == VTX::VtxFormat::FlatBuffers ? VTX::CreateFlatBuffersWriterFacade(cfg) + : VTX::CreateProtobuffWriterFacade(cfg); + } -VTX::Frame MakeSimpleFrame(int frame_index) -{ - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); - - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {"player_0", "Alpha"}; - pc.int32_properties = {1, frame_index, 0}; - pc.float_properties = {100.0f - float(frame_index), 50.0f}; - pc.vector_properties = { - VTX::Vector{double(frame_index), 0.0, 0.0}, - VTX::Vector{1.0, 0.0, 0.0} - }; - pc.quat_properties = {VTX::Quat{0.0f, 0.0f, 0.0f, 1.0f}}; - pc.bool_properties = {true}; - - bucket.unique_ids.push_back("player_0"); - bucket.entities.push_back(std::move(pc)); - return f; -} + VTX::Frame MakeSimpleFrame(int frame_index) { + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); + + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {"player_0", "Alpha"}; + pc.int32_properties = {1, frame_index, 0}; + pc.float_properties = {100.0f - float(frame_index), 50.0f}; + pc.vector_properties = {VTX::Vector {double(frame_index), 0.0, 0.0}, VTX::Vector {1.0, 0.0, 0.0}}; + pc.quat_properties = {VTX::Quat {0.0f, 0.0f, 0.0f, 1.0f}}; + pc.bool_properties = {true}; + + bucket.unique_ids.push_back("player_0"); + bucket.entities.push_back(std::move(pc)); + return f; + } -VTX::Frame MakeUnsortedTypesFrame() -{ - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); + VTX::Frame MakeUnsortedTypesFrame() { + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); - VTX::PropertyContainer type1_a; - type1_a.entity_type_id = 1; - type1_a.string_properties = {"id_b", "Bravo"}; + VTX::PropertyContainer type1_a; + type1_a.entity_type_id = 1; + type1_a.string_properties = {"id_b", "Bravo"}; - VTX::PropertyContainer type0; - type0.entity_type_id = 0; - type0.string_properties = {"id_a", "Alpha"}; + VTX::PropertyContainer type0; + type0.entity_type_id = 0; + type0.string_properties = {"id_a", "Alpha"}; - VTX::PropertyContainer type1_b; - type1_b.entity_type_id = 1; - type1_b.string_properties = {"id_c", "Charlie"}; + VTX::PropertyContainer type1_b; + type1_b.entity_type_id = 1; + type1_b.string_properties = {"id_c", "Charlie"}; - bucket.unique_ids = {"id_b", "id_a", "id_c"}; - bucket.entities.push_back(type1_a); - bucket.entities.push_back(type0); - bucket.entities.push_back(type1_b); - return f; -} + bucket.unique_ids = {"id_b", "id_a", "id_c"}; + bucket.entities.push_back(type1_a); + bucket.entities.push_back(type0); + bucket.entities.push_back(type1_b); + return f; + } } // namespace class WriterAdvancedTest : public ::testing::TestWithParam {}; -TEST_P(WriterAdvancedTest, ChunkMaxFramesProducesExpectedSeekTable) -{ +TEST_P(WriterAdvancedTest, ChunkMaxFramesProducesExpectedSeekTable) { const auto cfg = MakeConfig(GetParam(), "chunking", 1, false); { @@ -134,8 +119,7 @@ TEST_P(WriterAdvancedTest, ChunkMaxFramesProducesExpectedSeekTable) EXPECT_EQ(seek_table[2].end_frame, 2); } -TEST_P(WriterAdvancedTest, SortsEntitiesByTypeAndBuildsTypeRanges) -{ +TEST_P(WriterAdvancedTest, SortsEntitiesByTypeAndBuildsTypeRanges) { const auto cfg = MakeConfig(GetParam(), "sorted_types", 8, true); { @@ -173,8 +157,7 @@ TEST_P(WriterAdvancedTest, SortsEntitiesByTypeAndBuildsTypeRanges) EXPECT_EQ(bucket.type_ranges[1].count, 2); } -TEST_P(WriterAdvancedTest, UncompressedFilesRemainReadableAndUseDefaultUuid) -{ +TEST_P(WriterAdvancedTest, UncompressedFilesRemainReadableAndUseDefaultUuid) { const auto cfg = MakeConfig(GetParam(), "uncompressed", 8, false); { @@ -201,8 +184,7 @@ TEST_P(WriterAdvancedTest, UncompressedFilesRemainReadableAndUseDefaultUuid) EXPECT_EQ(footer.total_frames, 2); } -TEST_P(WriterAdvancedTest, StopWithoutFramesProducesReadableEmptyReplay) -{ +TEST_P(WriterAdvancedTest, StopWithoutFramesProducesReadableEmptyReplay) { const auto cfg = MakeConfig(GetParam(), "empty", 8, true); { @@ -217,8 +199,7 @@ TEST_P(WriterAdvancedTest, StopWithoutFramesProducesReadableEmptyReplay) EXPECT_TRUE(ctx.reader->GetSeekTable().empty()); } -TEST_P(WriterAdvancedTest, ThrowsWhenParentDirectoryDoesNotExist) -{ +TEST_P(WriterAdvancedTest, ThrowsWhenParentDirectoryDoesNotExist) { auto cfg = MakeConfig(GetParam(), "missing_dir", 8, true); const std::filesystem::path missing_dir = std::filesystem::path(VtxTest::OutputPath("writer_missing_dir")) / "nested"; @@ -233,10 +214,8 @@ TEST_P(WriterAdvancedTest, ThrowsWhenParentDirectoryDoesNotExist) std::runtime_error); } -INSTANTIATE_TEST_SUITE_P( - BothBackends, - WriterAdvancedTest, - ::testing::Values(VTX::VtxFormat::FlatBuffers, VTX::VtxFormat::Protobuf), - [](const ::testing::TestParamInfo& info) { - return std::string(FormatName(info.param)); - }); +INSTANTIATE_TEST_SUITE_P(BothBackends, WriterAdvancedTest, + ::testing::Values(VTX::VtxFormat::FlatBuffers, VTX::VtxFormat::Protobuf), + [](const ::testing::TestParamInfo& info) { + return std::string(FormatName(info.param)); + }); diff --git a/tests/writer/test_writer_basic.cpp b/tests/writer/test_writer_basic.cpp index a2b0310..764f763 100644 --- a/tests/writer/test_writer_basic.cpp +++ b/tests/writer/test_writer_basic.cpp @@ -10,35 +10,35 @@ namespace { -VTX::WriterFacadeConfig MakeConfig(const std::string& out_name, const std::string& uuid) { - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = VtxTest::OutputPath(out_name); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "WriterBasicTest"; - cfg.replay_uuid = uuid; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = 50; - cfg.use_compression = true; - return cfg; -} + VTX::WriterFacadeConfig MakeConfig(const std::string& out_name, const std::string& uuid) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = VtxTest::OutputPath(out_name); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "WriterBasicTest"; + cfg.replay_uuid = uuid; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = 50; + cfg.use_compression = true; + return cfg; + } -VTX::Frame MakeOneEntityFrame() { - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); + VTX::Frame MakeOneEntityFrame() { + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); - VTX::PropertyContainer pc; - pc.entity_type_id = 0; // Player - pc.string_properties = {"id_a", "Alpha"}; - pc.int32_properties = {1, 0, 0}; // Team, Score, Deaths - pc.float_properties = {100.0f, 50.0f}; // Health, Armor - pc.vector_properties = {VTX::Vector{0.0, 0.0, 0.0}, VTX::Vector{0.0, 0.0, 0.0}}; - pc.quat_properties = {VTX::Quat{0.0f, 0.0f, 0.0f, 1.0f}}; - pc.bool_properties = {true}; + VTX::PropertyContainer pc; + pc.entity_type_id = 0; // Player + pc.string_properties = {"id_a", "Alpha"}; + pc.int32_properties = {1, 0, 0}; // Team, Score, Deaths + pc.float_properties = {100.0f, 50.0f}; // Health, Armor + pc.vector_properties = {VTX::Vector {0.0, 0.0, 0.0}, VTX::Vector {0.0, 0.0, 0.0}}; + pc.quat_properties = {VTX::Quat {0.0f, 0.0f, 0.0f, 1.0f}}; + pc.bool_properties = {true}; - bucket.unique_ids.push_back("id_a"); - bucket.entities.push_back(std::move(pc)); - return f; -} + bucket.unique_ids.push_back("id_a"); + bucket.entities.push_back(std::move(pc)); + return f; + } } // namespace @@ -48,7 +48,7 @@ TEST(WriterBasic, FlatBuffersFactoryProducesValidWriter) { auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); ASSERT_TRUE(writer); writer->Stop(); - } // destructor releases any remaining file handles + } // destructor releases any remaining file handles EXPECT_TRUE(std::filesystem::exists(cfg.output_filepath)); EXPECT_GT(std::filesystem::file_size(cfg.output_filepath), 0u); } @@ -78,7 +78,7 @@ TEST(WriterBasic, RecordAndStopWritesNonEmptyFile) { } writer->Flush(); writer->Stop(); - } // release writer so the file is fully closed before we inspect it + } // release writer so the file is fully closed before we inspect it ASSERT_TRUE(std::filesystem::exists(cfg.output_filepath)); EXPECT_GT(std::filesystem::file_size(cfg.output_filepath), 100u); diff --git a/tests/writer/test_writer_edges.cpp b/tests/writer/test_writer_edges.cpp index 7265cb3..c8bacf9 100644 --- a/tests/writer/test_writer_edges.cpp +++ b/tests/writer/test_writer_edges.cpp @@ -17,40 +17,37 @@ namespace { -VTX::WriterFacadeConfig MakeBaseConfig(const std::string& out_name, - const std::string& uuid, - int32_t chunk_max_frames = 500, - size_t chunk_max_bytes = 10 * 1024 * 1024) -{ - VTX::WriterFacadeConfig cfg; - cfg.output_filepath = VtxTest::OutputPath(out_name); - cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); - cfg.replay_name = "WriterEdgeTest"; - cfg.replay_uuid = uuid; - cfg.default_fps = 60.0f; - cfg.chunk_max_frames = chunk_max_frames; - cfg.chunk_max_bytes = chunk_max_bytes; - cfg.use_compression = true; - return cfg; -} + VTX::WriterFacadeConfig MakeBaseConfig(const std::string& out_name, const std::string& uuid, + int32_t chunk_max_frames = 500, size_t chunk_max_bytes = 10 * 1024 * 1024) { + VTX::WriterFacadeConfig cfg; + cfg.output_filepath = VtxTest::OutputPath(out_name); + cfg.schema_json_path = VtxTest::FixturePath("test_schema.json"); + cfg.replay_name = "WriterEdgeTest"; + cfg.replay_uuid = uuid; + cfg.default_fps = 60.0f; + cfg.chunk_max_frames = chunk_max_frames; + cfg.chunk_max_bytes = chunk_max_bytes; + cfg.use_compression = true; + return cfg; + } -VTX::Frame MakeTrivialFrame(int frame_index = 0) { - VTX::Frame f; - auto& bucket = f.CreateBucket("entity"); - - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = { "p", "player" }; - pc.int32_properties = { 1, frame_index, 0 }; - pc.float_properties = { 100.0f, 50.0f }; - pc.vector_properties = { VTX::Vector{}, VTX::Vector{} }; - pc.quat_properties = { VTX::Quat{} }; - pc.bool_properties = { true }; - - bucket.unique_ids.push_back("p"); - bucket.entities.push_back(std::move(pc)); - return f; -} + VTX::Frame MakeTrivialFrame(int frame_index = 0) { + VTX::Frame f; + auto& bucket = f.CreateBucket("entity"); + + VTX::PropertyContainer pc; + pc.entity_type_id = 0; + pc.string_properties = {"p", "player"}; + pc.int32_properties = {1, frame_index, 0}; + pc.float_properties = {100.0f, 50.0f}; + pc.vector_properties = {VTX::Vector {}, VTX::Vector {}}; + pc.quat_properties = {VTX::Quat {}}; + pc.bool_properties = {true}; + + bucket.unique_ids.push_back("p"); + bucket.entities.push_back(std::move(pc)); + return f; + } } // namespace @@ -63,7 +60,7 @@ TEST(WriterEdges, WriteZeroFramesThenStop) { { auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); ASSERT_TRUE(writer); - writer->Stop(); // no RecordFrame calls + writer->Stop(); // no RecordFrame calls } ASSERT_TRUE(std::filesystem::exists(cfg.output_filepath)); @@ -87,7 +84,7 @@ TEST(WriterEdges, WriteSingleFrame) { const auto cfg = MakeBaseConfig("edge_single_frame.vtx", "edge-single"); { auto writer = VTX::CreateFlatBuffersWriterFacade(cfg); - auto frame = MakeTrivialFrame(0); + auto frame = MakeTrivialFrame(0); VTX::GameTime::GameTimeRegister t; t.game_time = 0.0f; writer->RecordFrame(frame, t); @@ -138,7 +135,7 @@ TEST(WriterEdges, ChunkMaxFramesIsOne) { EXPECT_EQ(seek.size(), static_cast(kFrames)); for (size_t i = 0; i < seek.size(); ++i) { EXPECT_EQ(seek[i].start_frame, static_cast(i)); - EXPECT_EQ(seek[i].end_frame, static_cast(i)); + EXPECT_EQ(seek[i].end_frame, static_cast(i)); } } @@ -165,13 +162,13 @@ TEST(WriterEdges, RecordFrameAfterStopIsSafe) { writer->RecordFrame(f2, t); writer->Flush(); - writer.reset(); // release before inspecting + writer.reset(); // release before inspecting auto ctx = VTX::OpenReplayFile(cfg.output_filepath); ASSERT_TRUE(ctx); // Whatever total_frames ends up being, the file should be readable. const int total = ctx.reader->GetTotalFrames(); - EXPECT_GE(total, 1); // at least the frame recorded before Stop + EXPECT_GE(total, 1); // at least the frame recorded before Stop } TEST(WriterEdges, DoubleStopIsIdempotent) { @@ -186,8 +183,8 @@ TEST(WriterEdges, DoubleStopIsIdempotent) { writer->Flush(); writer->Stop(); - writer->Stop(); // second call must be a safe no-op - writer->Stop(); // third for good measure + writer->Stop(); // second call must be a safe no-op + writer->Stop(); // third for good measure writer.reset(); @@ -215,15 +212,12 @@ TEST(WriterEdges, GiantFrameLargerThanChunkMaxBytes) { auto& bucket = f.CreateBucket("entity"); VTX::PropertyContainer pc; pc.entity_type_id = 0; - pc.string_properties = { - "giant", - std::string(64 * 1024, 'X') - }; - pc.int32_properties = {1, 0, 0}; - pc.float_properties = {100.0f, 50.0f}; - pc.vector_properties = {VTX::Vector{}, VTX::Vector{}}; - pc.quat_properties = {VTX::Quat{}}; - pc.bool_properties = {true}; + pc.string_properties = {"giant", std::string(64 * 1024, 'X')}; + pc.int32_properties = {1, 0, 0}; + pc.float_properties = {100.0f, 50.0f}; + pc.vector_properties = {VTX::Vector {}, VTX::Vector {}}; + pc.quat_properties = {VTX::Quat {}}; + pc.bool_properties = {true}; bucket.unique_ids.push_back("giant_id"); bucket.entities.push_back(std::move(pc)); diff --git a/tools/cli/include/commands/analysis_commands.h b/tools/cli/include/commands/analysis_commands.h index e0d0b4a..fdcb3a6 100644 --- a/tools/cli/include/commands/analysis_commands.h +++ b/tools/cli/include/commands/analysis_commands.h @@ -16,123 +16,172 @@ namespace VtxCli { namespace detail { inline bool CompareNumeric(double a, const std::string& op, double b) { - if (op == "==") return a == b; - if (op == "!=") return a != b; - if (op == ">") return a > b; - if (op == "<") return a < b; - if (op == ">=") return a >= b; - if (op == "<=") return a <= b; + if (op == "==") + return a == b; + if (op == "!=") + return a != b; + if (op == ">") + return a > b; + if (op == "<") + return a < b; + if (op == ">=") + return a >= b; + if (op == "<=") + return a <= b; return false; } - inline bool MatchesScalar( - const VTX::PropertyContainer& pc, - const VTX::PropertyAddress& addr, - const std::string& op, - const std::string& target) - { - if (addr.container_type != VTX::FieldContainerType::None) return false; + inline bool MatchesScalar(const VTX::PropertyContainer& pc, const VTX::PropertyAddress& addr, + const std::string& op, const std::string& target) { + if (addr.container_type != VTX::FieldContainerType::None) + return false; auto idx = static_cast(addr.index); switch (addr.type_id) { - case VTX::FieldType::Bool: { - if (idx >= pc.bool_properties.size()) return false; - bool val = static_cast(pc.bool_properties[idx]); - bool tgt = (target == "true" || target == "1"); - if (op == "==") return val == tgt; - if (op == "!=") return val != tgt; + case VTX::FieldType::Bool: { + if (idx >= pc.bool_properties.size()) + return false; + bool val = static_cast(pc.bool_properties[idx]); + bool tgt = (target == "true" || target == "1"); + if (op == "==") + return val == tgt; + if (op == "!=") + return val != tgt; + return false; + } + case VTX::FieldType::Int32: + case VTX::FieldType::Enum: { + if (idx >= pc.int32_properties.size()) + return false; + try { + return CompareNumeric(pc.int32_properties[idx], op, std::stod(target)); + } catch (...) { return false; } - case VTX::FieldType::Int32: - case VTX::FieldType::Enum: { - if (idx >= pc.int32_properties.size()) return false; - try { return CompareNumeric(pc.int32_properties[idx], op, std::stod(target)); } - catch (...) { return false; } - } - case VTX::FieldType::Int64: { - if (idx >= pc.int64_properties.size()) return false; - try { return CompareNumeric(static_cast(pc.int64_properties[idx]), op, std::stod(target)); } - catch (...) { return false; } - } - case VTX::FieldType::Float: { - if (idx >= pc.float_properties.size()) return false; - try { return CompareNumeric(pc.float_properties[idx], op, std::stod(target)); } - catch (...) { return false; } + } + case VTX::FieldType::Int64: { + if (idx >= pc.int64_properties.size()) + return false; + try { + return CompareNumeric(static_cast(pc.int64_properties[idx]), op, std::stod(target)); + } catch (...) { + return false; } - case VTX::FieldType::Double: { - if (idx >= pc.double_properties.size()) return false; - try { return CompareNumeric(pc.double_properties[idx], op, std::stod(target)); } - catch (...) { return false; } + } + case VTX::FieldType::Float: { + if (idx >= pc.float_properties.size()) + return false; + try { + return CompareNumeric(pc.float_properties[idx], op, std::stod(target)); + } catch (...) { + return false; } - case VTX::FieldType::String: { - if (idx >= pc.string_properties.size()) return false; - if (op == "==") return pc.string_properties[idx] == target; - if (op == "!=") return pc.string_properties[idx] != target; + } + case VTX::FieldType::Double: { + if (idx >= pc.double_properties.size()) + return false; + try { + return CompareNumeric(pc.double_properties[idx], op, std::stod(target)); + } catch (...) { return false; } - default: return false; + } + case VTX::FieldType::String: { + if (idx >= pc.string_properties.size()) + return false; + if (op == "==") + return pc.string_properties[idx] == target; + if (op == "!=") + return pc.string_properties[idx] != target; + return false; + } + default: + return false; } } //Map EVTXContainerType (FieldType, FieldContainerType) for schema lookup ── - inline std::pair - MapContainerType(VtxDiff::EVTXContainerType ct) { + inline std::pair MapContainerType(VtxDiff::EVTXContainerType ct) { using CT = VtxDiff::EVTXContainerType; using FT = VTX::FieldType; using FCT = VTX::FieldContainerType; switch (ct) { - case CT::BoolProperties: return {FT::Bool, FCT::None}; - case CT::Int32Properties: return {FT::Int32, FCT::None}; - case CT::Int64Properties: return {FT::Int64, FCT::None}; - case CT::FloatProperties: return {FT::Float, FCT::None}; - case CT::DoubleProperties: return {FT::Double, FCT::None}; - case CT::StringProperties: return {FT::String, FCT::None}; - case CT::TransformProperties: return {FT::Transform, FCT::None}; - case CT::VectorProperties: return {FT::Vector, FCT::None}; - case CT::QuatProperties: return {FT::Quat, FCT::None}; - case CT::RangeProperties: return {FT::FloatRange, FCT::None}; - case CT::AnyStructProperties: return {FT::Struct, FCT::None}; - case CT::BoolArrays: return {FT::Bool, FCT::Array}; - case CT::Int32Arrays: return {FT::Int32, FCT::Array}; - case CT::Int64Arrays: return {FT::Int64, FCT::Array}; - case CT::FloatArrays: return {FT::Float, FCT::Array}; - case CT::DoubleArrays: return {FT::Double, FCT::Array}; - case CT::StringArrays: return {FT::String, FCT::Array}; - case CT::TransformArrays: return {FT::Transform, FCT::Array}; - case CT::VectorArrays: return {FT::Vector, FCT::Array}; - case CT::QuatArrays: return {FT::Quat, FCT::Array}; - case CT::RangeArrays: return {FT::FloatRange, FCT::Array}; - case CT::AnyStructArrays: return {FT::Struct, FCT::Array}; - case CT::MapProperties: return {FT::Struct, FCT::Map}; - case CT::MapArrays: return {FT::Struct, FCT::Map}; - default: return {FT::None, FCT::None}; + case CT::BoolProperties: + return {FT::Bool, FCT::None}; + case CT::Int32Properties: + return {FT::Int32, FCT::None}; + case CT::Int64Properties: + return {FT::Int64, FCT::None}; + case CT::FloatProperties: + return {FT::Float, FCT::None}; + case CT::DoubleProperties: + return {FT::Double, FCT::None}; + case CT::StringProperties: + return {FT::String, FCT::None}; + case CT::TransformProperties: + return {FT::Transform, FCT::None}; + case CT::VectorProperties: + return {FT::Vector, FCT::None}; + case CT::QuatProperties: + return {FT::Quat, FCT::None}; + case CT::RangeProperties: + return {FT::FloatRange, FCT::None}; + case CT::AnyStructProperties: + return {FT::Struct, FCT::None}; + case CT::BoolArrays: + return {FT::Bool, FCT::Array}; + case CT::Int32Arrays: + return {FT::Int32, FCT::Array}; + case CT::Int64Arrays: + return {FT::Int64, FCT::Array}; + case CT::FloatArrays: + return {FT::Float, FCT::Array}; + case CT::DoubleArrays: + return {FT::Double, FCT::Array}; + case CT::StringArrays: + return {FT::String, FCT::Array}; + case CT::TransformArrays: + return {FT::Transform, FCT::Array}; + case CT::VectorArrays: + return {FT::Vector, FCT::Array}; + case CT::QuatArrays: + return {FT::Quat, FCT::Array}; + case CT::RangeArrays: + return {FT::FloatRange, FCT::Array}; + case CT::AnyStructArrays: + return {FT::Struct, FCT::Array}; + case CT::MapProperties: + return {FT::Struct, FCT::Map}; + case CT::MapArrays: + return {FT::Struct, FCT::Map}; + default: + return {FT::None, FCT::None}; } } - inline std::string ResolvePropertyName( - const VTX::StructSchemaCache& schema, - VtxDiff::EVTXContainerType ct, int32_t index) - { + inline std::string ResolvePropertyName(const VTX::StructSchemaCache& schema, VtxDiff::EVTXContainerType ct, + int32_t index) { auto [ft, fct] = MapContainerType(ct); uint64_t key = VTX::MakePropertyLookupKey(index, ft, fct); auto it = schema.names_by_lookup_key.find(key); - if (it != schema.names_by_lookup_key.end()) return it->second; + if (it != schema.names_by_lookup_key.end()) + return it->second; return VtxDiff::TypeToFieldName(ct) + "[" + std::to_string(index) + "]"; } } // namespace detail - - template + + template struct DiffCommand { static constexpr std::string_view Name = "diff"; static constexpr std::string_view Help = "diff [--epsilon ] - Compare entities between two frames"; - void Run(CommandContext& ctx, std::span args, Fmt& w) - { - if (!RequireLoaded(ctx, w, Name)) return; + void Run(CommandContext& ctx, std::span args, Fmt& w) { + if (!RequireLoaded(ctx, w, Name)) + return; if (args.size() < 2) { ResponseError(w, Name, "Usage: diff [--epsilon ]"); @@ -155,8 +204,9 @@ namespace VtxCli { // Parse optional --epsilon for (size_t i = 2; i + 1 < args.size(); ++i) { if (args[i] == "--epsilon") { - try { opts.float_epsilon = std::stof(std::string(args[i + 1])); } - catch (...) { + try { + opts.float_epsilon = std::stof(std::string(args[i + 1])); + } catch (...) { ResponseError(w, Name, "Invalid epsilon value: " + std::string(args[i + 1])); return; } @@ -173,17 +223,23 @@ namespace VtxCli { auto patch = ctx.session.DiffFrames(frame_a, frame_b, opts); if (patch.operations.empty()) { - std::string reason = (frame_a == frame_b) - ? "Same frame index; no diff possible" - : "Frames are identical"; + std::string reason = + (frame_a == frame_b) ? "Same frame index; no diff possible" : "Frames are identical"; ResponseOk(w, Name) - .Key("frame_a").WriteInt(frame_a) - .Key("frame_b").WriteInt(frame_b) - .Key("total_ops").WriteInt(0) - .Key("reason").WriteString(reason); - w.Key("changed"); w.BeginArray().EndArray(); - w.Key("added"); w.BeginArray().EndArray(); - w.Key("removed"); w.BeginArray().EndArray(); + .Key("frame_a") + .WriteInt(frame_a) + .Key("frame_b") + .WriteInt(frame_b) + .Key("total_ops") + .WriteInt(0) + .Key("reason") + .WriteString(reason); + w.Key("changed"); + w.BeginArray().EndArray(); + w.Key("added"); + w.BeginArray().EndArray(); + w.Key("removed"); + w.BeginArray().EndArray(); EndResponse(w); return; } @@ -239,16 +295,19 @@ namespace VtxCli { } } // Fallback: use container type name - changed_map[op.ActorId].insert( - VtxDiff::TypeToFieldName(container_type) + "[" + std::to_string(prop_index) + "]"); + changed_map[op.ActorId].insert(VtxDiff::TypeToFieldName(container_type) + "[" + + std::to_string(prop_index) + "]"); } } // Write output ResponseOk(w, Name) - .Key("frame_a").WriteInt(frame_a) - .Key("frame_b").WriteInt(frame_b) - .Key("total_ops").WriteInt(static_cast(patch.operations.size())); + .Key("frame_a") + .WriteInt(frame_a) + .Key("frame_b") + .WriteInt(frame_b) + .Key("total_ops") + .WriteInt(static_cast(patch.operations.size())); // Helper: resolve type_name from uid, always writes the key auto writeTypeName = [&](const std::string& uid) { @@ -266,14 +325,13 @@ namespace VtxCli { w.Key("changed"); w.BeginArray(); for (const auto& [uid, props] : changed_map) { - w.BeginObject() - .Key("unique_id").WriteString(uid) - .Key("operation").WriteString("modify"); + w.BeginObject().Key("unique_id").WriteString(uid).Key("operation").WriteString("modify"); writeTypeName(uid); w.Key("property_count").WriteInt(static_cast(props.size())); w.Key("properties"); w.BeginArray(); - for (const auto& prop : props) w.WriteString(prop); + for (const auto& prop : props) + w.WriteString(prop); w.EndArray(); w.EndObject(); } @@ -282,9 +340,7 @@ namespace VtxCli { w.Key("added"); w.BeginArray(); for (const auto& uid : added_ids) { - w.BeginObject() - .Key("unique_id").WriteString(uid) - .Key("operation").WriteString("add"); + w.BeginObject().Key("unique_id").WriteString(uid).Key("operation").WriteString("add"); writeTypeName(uid); w.EndObject(); } @@ -293,9 +349,7 @@ namespace VtxCli { w.Key("removed"); w.BeginArray(); for (const auto& uid : removed_ids) { - w.BeginObject() - .Key("unique_id").WriteString(uid) - .Key("operation").WriteString("remove"); + w.BeginObject().Key("unique_id").WriteString(uid).Key("operation").WriteString("remove"); writeTypeName(uid); w.EndObject(); } @@ -304,22 +358,22 @@ namespace VtxCli { EndResponse(w); } }; - - template + + template struct TrackCommand { static constexpr std::string_view Name = "track"; static constexpr std::string_view Help = "track [step] | --index | --id "; - void Run(CommandContext& ctx, std::span args, Fmt& w) - { - if (!RequireLoaded(ctx, w, Name)) return; + void Run(CommandContext& ctx, std::span args, Fmt& w) { + if (!RequireLoaded(ctx, w, Name)) + return; if (args.size() < 5) { ResponseError(w, Name, - "Usage: track [step]\n" - " track --index [step]\n" - " track --id [step]"); + "Usage: track [step]\n" + " track --index [step]\n" + " track --id [step]"); return; } @@ -342,15 +396,22 @@ namespace VtxCli { int32_t start_frame, end_frame, step = 1; try { start_frame = std::stoi(std::string(args[next + 1])); - end_frame = std::stoi(std::string(args[next + 2])); - if (next + 3 < args.size()) step = std::stoi(std::string(args[next + 3])); + end_frame = std::stoi(std::string(args[next + 2])); + if (next + 3 < args.size()) + step = std::stoi(std::string(args[next + 3])); } catch (...) { ResponseError(w, Name, "Invalid frame range or step"); return; } - if (step <= 0) { ResponseError(w, Name, "Step must be > 0"); return; } - if (start_frame > end_frame) { ResponseError(w, Name, "start must be <= end"); return; } + if (step <= 0) { + ResponseError(w, Name, "Step must be > 0"); + return; + } + if (start_frame > end_frame) { + ResponseError(w, Name, "start must be <= end"); + return; + } int32_t total = ctx.session.GetTotalFrames(); if (start_frame < 0 || start_frame >= total || end_frame < 0 || end_frame >= total) { @@ -385,9 +446,8 @@ namespace VtxCli { if (!sc_it->second.properties.contains(prop_name)) { std::string suggestion = SuggestProperty(cache, first_entity.entity_type_id, prop_name); if (!suggestion.empty()) { - ResponseErrorHint(w, Name, - "Property not found: " + prop_name, - "Did you mean '" + suggestion + "'?"); + ResponseErrorHint(w, Name, "Property not found: " + prop_name, + "Did you mean '" + suggestion + "'?"); } else { ResponseError(w, Name, "Property not found: " + prop_name); } @@ -395,29 +455,36 @@ namespace VtxCli { } ResponseOk(w, Name) - .Key("entity").WriteString(sel.value) - .Key("property").WriteString(prop_name) - .Key("start_frame").WriteInt(start_frame) - .Key("end_frame").WriteInt(end_frame) - .Key("step").WriteInt(step); + .Key("entity") + .WriteString(sel.value) + .Key("property") + .WriteString(prop_name) + .Key("start_frame") + .WriteInt(start_frame) + .Key("end_frame") + .WriteInt(end_frame) + .Key("step") + .WriteInt(step); w.Key("samples"); w.BeginArray(); for (int32_t f = start_frame; f <= end_frame; f += step) { const auto* frame = ctx.session.GetFrameData(f); - if (!frame) continue; + if (!frame) + continue; auto ref = detail::ResolveBucket(ctx, *frame, bucket_arg); - if (!ref.bucket) continue; + if (!ref.bucket) + continue; int eidx = detail::ResolveEntity(*ref.bucket, sel); - if (eidx < 0) continue; + if (eidx < 0) + continue; const auto& entity = ref.bucket->entities[eidx]; - w.BeginObject() - .Key("frame").WriteInt(f); + w.BeginObject().Key("frame").WriteInt(f); w.Key("value"); if (!SerializeProperty(w, entity, prop_name, cache)) { @@ -431,15 +498,16 @@ namespace VtxCli { EndResponse(w); } }; - - template + + template struct SearchCommand { static constexpr std::string_view Name = "search"; - static constexpr std::string_view Help = "search [bucket] - Find entities by property value"; + static constexpr std::string_view Help = + "search [bucket] - Find entities by property value"; - void Run(CommandContext& ctx, std::span args, Fmt& w) - { - if (!RequireLoaded(ctx, w, Name)) return; + void Run(CommandContext& ctx, std::span args, Fmt& w) { + if (!RequireLoaded(ctx, w, Name)) + return; if (args.size() < 3) { ResponseError(w, Name, "Usage: search [bucket] (ops: == != > < >= <=)"); @@ -450,7 +518,7 @@ namespace VtxCli { const std::string op(args[1]); const std::string target(args[2]); - static const std::unordered_set valid_ops = {"==","!=",">","<",">=","<="}; + static const std::unordered_set valid_ops = {"==", "!=", ">", "<", ">=", "<="}; if (!valid_ops.contains(op)) { ResponseError(w, Name, "Invalid operator: " + op + " (use == != > < >= <=)"); return; @@ -488,16 +556,17 @@ namespace VtxCli { for (const auto& [_, sc] : cache.structs) { for (const auto& [pname, __] : sc.properties) { size_t d = EditDistance(prop_name, pname); - if (d < best_dist) { best_dist = d; suggestion = pname; } + if (d < best_dist) { + best_dist = d; + suggestion = pname; + } } } if (!suggestion.empty()) { - ResponseErrorHint(w, Name, - "Property '" + prop_name + "' not found in any entity type", - "Did you mean '" + suggestion + "'?"); + ResponseErrorHint(w, Name, "Property '" + prop_name + "' not found in any entity type", + "Did you mean '" + suggestion + "'?"); } else { - ResponseError(w, Name, - "Property '" + prop_name + "' not found in any entity type"); + ResponseError(w, Name, "Property '" + prop_name + "' not found in any entity type"); } return; } @@ -505,45 +574,59 @@ namespace VtxCli { // Collect matches int32_t match_count = 0; struct Match { - int32_t bi; int32_t ei; - std::string uid; std::string type; + int32_t bi; + int32_t ei; + std::string uid; + std::string type; const VTX::PropertyContainer* entity; }; std::vector matches; const auto& buckets = frame->GetBuckets(); for (size_t bi = 0; bi < buckets.size(); ++bi) { - if (only_bucket >= 0 && static_cast(bi) != only_bucket) continue; + if (only_bucket >= 0 && static_cast(bi) != only_bucket) + continue; const auto& bucket = buckets[bi]; for (size_t ei = 0; ei < bucket.entities.size(); ++ei) { const auto& entity = bucket.entities[ei]; auto sc_it = cache.structs.find(entity.entity_type_id); - if (sc_it == cache.structs.end()) continue; + if (sc_it == cache.structs.end()) + continue; auto pr_it = sc_it->second.properties.find(prop_name); - if (pr_it == sc_it->second.properties.end()) continue; - if (!detail::MatchesScalar(entity, pr_it->second, op, target)) continue; - matches.push_back({ - static_cast(bi), static_cast(ei), - ei < bucket.unique_ids.size() ? bucket.unique_ids[ei] : std::string{}, - sc_it->second.name, &entity}); + if (pr_it == sc_it->second.properties.end()) + continue; + if (!detail::MatchesScalar(entity, pr_it->second, op, target)) + continue; + matches.push_back({static_cast(bi), static_cast(ei), + ei < bucket.unique_ids.size() ? bucket.unique_ids[ei] : std::string {}, + sc_it->second.name, &entity}); } } ResponseOk(w, Name) - .Key("frame").WriteInt(ctx.session.GetCurrentFrame()) - .Key("property").WriteString(prop_name) - .Key("operator").WriteString(op) - .Key("value").WriteString(target) - .Key("match_count").WriteInt(static_cast(matches.size())); + .Key("frame") + .WriteInt(ctx.session.GetCurrentFrame()) + .Key("property") + .WriteString(prop_name) + .Key("operator") + .WriteString(op) + .Key("value") + .WriteString(target) + .Key("match_count") + .WriteInt(static_cast(matches.size())); w.Key("matches"); w.BeginArray(); for (const auto& m : matches) { w.BeginObject() - .Key("bucket_index").WriteInt(m.bi) - .Key("entity_index").WriteInt(m.ei) - .Key("unique_id").WriteString(m.uid) - .Key("type_name").WriteString(m.type); + .Key("bucket_index") + .WriteInt(m.bi) + .Key("entity_index") + .WriteInt(m.ei) + .Key("unique_id") + .WriteString(m.uid) + .Key("type_name") + .WriteString(m.type); w.Key("current_value"); SerializeProperty(w, *m.entity, prop_name, cache); w.EndObject(); diff --git a/tools/cli/include/commands/command_helpers.h b/tools/cli/include/commands/command_helpers.h index fa8a64b..ede11d2 100644 --- a/tools/cli/include/commands/command_helpers.h +++ b/tools/cli/include/commands/command_helpers.h @@ -10,72 +10,77 @@ namespace VtxCli { - template - Fmt& ResponseOk(Fmt& writer, std::string_view command_name) - { + template + Fmt& ResponseOk(Fmt& writer, std::string_view command_name) { writer.BeginObject() - .Key("status").WriteString("ok") - .Key("command").WriteString(std::string(command_name)) + .Key("status") + .WriteString("ok") + .Key("command") + .WriteString(std::string(command_name)) .Key("data"); return writer.BeginObject(); } - template - void EndResponse(Fmt& w) - { - w.EndObject() - .EndObject(); + template + void EndResponse(Fmt& w) { + w.EndObject().EndObject(); } - template - Fmt& ResponseError(Fmt& writer, std::string_view command_name, const std::string& message) - { + template + Fmt& ResponseError(Fmt& writer, std::string_view command_name, const std::string& message) { writer.BeginObject() - .Key("status").WriteString("error") - .Key("command").WriteString(std::string(command_name)) - .Key("error").WriteString(message) - .Key("hint").WriteNull() + .Key("status") + .WriteString("error") + .Key("command") + .WriteString(std::string(command_name)) + .Key("error") + .WriteString(message) + .Key("hint") + .WriteNull() .EndObject(); return writer; } /// Error response with a hint (e.g. "did you mean X?") - template - Fmt& ResponseErrorHint(Fmt& writer, std::string_view command_name, - const std::string& message, const std::string& hint) - { + template + Fmt& ResponseErrorHint(Fmt& writer, std::string_view command_name, const std::string& message, + const std::string& hint) { writer.BeginObject() - .Key("status").WriteString("error") - .Key("command").WriteString(std::string(command_name)) - .Key("error").WriteString(message) - .Key("hint").WriteString(hint) + .Key("status") + .WriteString("error") + .Key("command") + .WriteString(std::string(command_name)) + .Key("error") + .WriteString(message) + .Key("hint") + .WriteString(hint) .EndObject(); return writer; } //returns true if a file is loaded, false after writing error. - template - bool RequireLoaded(const CommandContext& context, Fmt& writer, std::string_view command_name) - { - if (context.session.IsLoaded()) return true; + template + bool RequireLoaded(const CommandContext& context, Fmt& writer, std::string_view command_name) { + if (context.session.IsLoaded()) + return true; ResponseError(writer, command_name, "No file loaded"); return false; } - /// Case-insensitive Levenshtein distance (for short strings). inline size_t EditDistance(std::string_view a, std::string_view b) { const size_t m = a.size(), n = b.size(); std::vector prev(n + 1), curr(n + 1); - for (size_t j = 0; j <= n; ++j) prev[j] = j; + for (size_t j = 0; j <= n; ++j) + prev[j] = j; for (size_t i = 1; i <= m; ++i) { curr[0] = i; for (size_t j = 1; j <= n; ++j) { - char ca = static_cast(std::tolower(static_cast(a[i-1]))); - char cb = static_cast(std::tolower(static_cast(b[j-1]))); + char ca = static_cast(std::tolower(static_cast(a[i - 1]))); + char cb = static_cast(std::tolower(static_cast(b[j - 1]))); size_t cost = (ca == cb) ? 0 : 1; - curr[j] = std::min({curr[j-1] + 1, prev[j] + 1, prev[j-1] + cost}); + curr[j] = std::min({curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost}); } std::swap(prev, curr); } @@ -84,10 +89,8 @@ namespace VtxCli { /// Find the closest match from a set of candidates. /// Returns empty string if no match is close enough (threshold = max edit distance). - inline std::string FindClosestMatch(const std::string& input, - const std::vector& candidates, - size_t threshold = 3) - { + inline std::string FindClosestMatch(const std::string& input, const std::vector& candidates, + size_t threshold = 3) { std::string best; size_t best_dist = threshold + 1; for (const auto& c : candidates) { @@ -101,12 +104,11 @@ namespace VtxCli { } /// Find closest property name for a given entity type. - inline std::string SuggestProperty(const VTX::PropertyAddressCache& cache, - int32_t entity_type_id, - const std::string& input) - { + inline std::string SuggestProperty(const VTX::PropertyAddressCache& cache, int32_t entity_type_id, + const std::string& input) { auto it = cache.structs.find(entity_type_id); - if (it == cache.structs.end()) return {}; + if (it == cache.structs.end()) + return {}; std::vector names; names.reserve(it->second.properties.size()); for (const auto& [name, _] : it->second.properties) { @@ -116,9 +118,7 @@ namespace VtxCli { } /// Find closest type name. - inline std::string SuggestTypeName(const VTX::PropertyAddressCache& cache, - const std::string& input) - { + inline std::string SuggestTypeName(const VTX::PropertyAddressCache& cache, const std::string& input) { std::vector names; names.reserve(cache.name_to_id.size()); for (const auto& [name, _] : cache.name_to_id) { diff --git a/tools/cli/include/commands/command_registry.h b/tools/cli/include/commands/command_registry.h index df7ee4d..56bf249 100644 --- a/tools/cli/include/commands/command_registry.h +++ b/tools/cli/include/commands/command_registry.h @@ -19,24 +19,23 @@ namespace VtxCli { inline size_t CmdEditDistance(std::string_view a, std::string_view b) { const size_t m = a.size(), n = b.size(); std::vector prev(n + 1), curr(n + 1); - for (size_t j = 0; j <= n; ++j) prev[j] = j; + for (size_t j = 0; j <= n; ++j) + prev[j] = j; for (size_t i = 1; i <= m; ++i) { curr[0] = i; for (size_t j = 1; j <= n; ++j) { - char ca = static_cast(std::tolower(static_cast(a[i-1]))); - char cb = static_cast(std::tolower(static_cast(b[j-1]))); + char ca = static_cast(std::tolower(static_cast(a[i - 1]))); + char cb = static_cast(std::tolower(static_cast(b[j - 1]))); size_t cost = (ca == cb) ? 0 : 1; - curr[j] = std::min({curr[j-1] + 1, prev[j] + 1, prev[j-1] + cost}); + curr[j] = std::min({curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost}); } std::swap(prev, curr); } return prev[n]; } - inline std::string FindClosestCommand(const std::string& input, - const std::vector& candidates, - size_t threshold = 3) - { + inline std::string FindClosestCommand(const std::string& input, const std::vector& candidates, + size_t threshold = 3) { std::string best; size_t best_dist = threshold + 1; for (const auto& c : candidates) { @@ -54,33 +53,37 @@ namespace VtxCli { std::string description; }; - + struct CommandContext { CliSession& session; bool exit_requested = false; const std::vector* help_entries = nullptr; }; - - inline std::vector Tokenize(const std::string& line) - { + + inline std::vector Tokenize(const std::string& line) { std::vector tokens; std::string current; bool in_quotes = false; for (char c : line) { if (in_quotes) { - if (c == '"') { in_quotes = false; } - else { current += c; } + if (c == '"') { + in_quotes = false; + } else { + current += c; + } } else { - if (c == '"') { in_quotes = true; } - else if (c == ' ' || c == '\t') { + if (c == '"') { + in_quotes = true; + } else if (c == ' ' || c == '\t') { if (!current.empty()) { tokens.push_back(std::move(current)); current.clear(); } + } else { + current += c; } - else { current += c; } } } if (!current.empty()) { @@ -88,25 +91,21 @@ namespace VtxCli { } return tokens; } - - template + + template class CommandRegistry { public: - /// Register a command template. Cmd must expose: /// static constexpr std::string_view Name; /// static constexpr std::string_view Help; /// void Run(CommandContext&, std::span, Fmt&); - template typename Cmd> - void Register() - { + template