Skip to content

Releases: ZenosInteractive/VTX

VTX SDK v0.3.0

26 May 15:42

Choose a tag to compare

[0.3.0] - 2026-05-26

Added

  • tests: integration coverage for both new streaming IFrameDataSource implementations -- runs in the existing GoogleTest suite and so on every GitHub Actions matrix entry (Windows + Linux). tests/writer/test_pipe_source.cpp drives PipeFrameDataSource<TestAdapter> in server mode with VTX on the receive end and a test-spawned client thread as the producer (Windows CreateFileA+WriteFile, POSIX open+write); covers the happy path (50 frames + sentinel round-trip via OpenReplayFile), sentinel-only empty stream, adapter-false-stops-stream after N valid frames, and the GetExpectedTotalFrames() == 0 streaming contract. tests/writer/test_websocket_source.cpp drives WebSocketFrameDataSource<TestWsAdapter> against an in-process ix::WebSocketServer loopback on an OS-assigned port -- covers the happy path (25 messages + parseable .vtx), Initialize() returns false on a refused connection, and the streaming-total contract. Both files share a DrainSourceIntoWriter helper that takes the writer by unique_ptr and destroys it before reading back the file -- the ChunkedFileSink only flushes its std::ofstream in its destructor, so an in-scope writer would race the reader. vtx_tests links $<BUILD_INTERFACE:VTX::deps::ixwebsocket> directly because vtx_writer keeps the dep PRIVATE behind its PIMPL boundary. WebSocket happy-path uses the per-client message callback (setOnClientMessageCallback, v12 path) which gives a WebSocket& directly and avoids the weak_ptr-vs-handshake race that the v11-style setOnConnectionCallback triggers; the bound port is pre-allocated with a raw bind+getsockname dance because ix::WebSocketServer::getPort() returns the constructor argument, not the OS-assigned port

  • writer/sinks: ChunkedNetworkSink<Policy> -- second sink alongside ChunkedFileSink, streams the identical binary .vtx byte stream over a TCP socket instead of writing to disk. Receiver concatenates the bytes into a file and gets a valid .vtx; no custom wire framing beyond the on-disk format's own magic + size prefixes. New public header sdk/include/vtx/writer/policies/sinks/network_sink.h -- self-contained Win32/POSIX socket portability layer (getaddrinfo + socket + connect), throws std::runtime_error on connect failure (parallel to ChunkedFileSink throwing on open failure), SendAll retries on partial send, chunk index entries record file_offset via a running bytes_sent_ counter (the tellp() analogue for a stream). New facade entry points: NetworkWriterFacadeConfig (host + port + the usual replay/chunking/compression knobs, no output_filepath) plus CreateFlatBuffersNetworkWriterFacade / CreateProtobuffNetworkWriterFacade -- mirror of the file-sink factories, behind the same IVtxWriterFacade abstraction so existing user code is sink-agnostic. vtx_writer propagates ws2_32 PUBLIC on Windows so consumers that instantiate ChunkedNetworkSink get the linkage automatically. Six integration tests in tests/writer/test_network_sink.cpp spin up a loopback TCP server on a free ephemeral port (server thread signals readiness via std::promise so the client cannot connect before listen() returns), drain bytes into a vector, and prove the received stream parses cleanly via VTX::OpenReplayFile (FlatBuffers + Protobuf), the constructor throws on a refused connection, zero-frame sessions produce a valid stream, and chunked sessions produce the expected seek-table entries

  • writer/sources: WebSocketFrameDataSource<Adapter> -- streaming IFrameDataSource over WebSocket (RFC 6455), connects as a client to ws://host:port/path or wss://host:port/path (TLS). Each WebSocket message is one serialized frame; ping/pong + close + fragment reassembly are handled transparently by the underlying transport. New public header sdk/include/vtx/writer/sources/websocket_frame_source.h plus a PIMPL facade in sdk/include/vtx/writer/sources/detail/websocket_client.h -- the implementation in sdk/src/vtx_writer/.../websocket_client.cpp wraps IXWebSocket and bridges its async callbacks to a blocking ReadMessage via a thread-safe queue (std::mutex + std::condition_variable), so the writer's pull-based GetNextFrame slots in cleanly. IXWebSocket stays fully hidden behind the PIMPL boundary -- the public SDK headers do not see it. wss:// verifies the server certificate against the OS trust store by default (tls.caFile = "SYSTEM"). Auto-reconnect is disabled: a dropped connection surfaces as end-of-stream so the consumer finalises the .vtx instead of silently resuming mid-file. Format-agnostic: the Adapter (constrained by the IFramePayloadAdapter concept) is the only piece that knows the on-wire payload format -- the sample's JsonWebSocketAdapter parses each message as JSON via nlohmann::json + JsonMapping<T> + UniversalDeserializer (declarative -- same pattern as arena_mappings.h)

  • writer/sources: PipeFrameDataSource<Adapter> -- streaming IFrameDataSource over OS pipes, format-agnostic via a caller-supplied adapter. New public header sdk/include/vtx/writer/sources/pipe_frame_source.h -- length-prefixed framing on the wire ([uint32 LE size][payload], zero-size sentinel = clean EOF) with the payload format determined by the Adapter (sample uses JSON). Three transport modes selected by Config:

    • stdin (pipe_path empty) -- for shell pipelines producer | vtx. Sets _O_BINARY on Windows so CRLF translation doesn't corrupt the wire.
    • client (pipe_path set, as_server = false) -- VTX connects to a pipe / FIFO a producer already created (fopen("rb")).
    • server (pipe_path set, as_server = true) -- VTX creates the pipe and blocks waiting for a producer to connect. Windows: CreateNamedPipeA(PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE) then ConnectNamedPipe, with the resulting HANDLE wrapped in a FILE* via _open_osfhandle + _fdopen so ReadExact / GetNextFrame stay platform-agnostic. POSIX: mkfifo(0666) (tolerates EEXIST) then fopen -- blocks until a writer opens the other end. The FIFO is unlinked on destruction.

    Server mode is the mode for an external, independent producer -- e.g. a game injector that opens \\.\pipe\vtx as a client when its game launches. The injector needs nothing from the VTX SDK: it speaks the [uint32][payload] framing and writes a zero-size sentinel when the session ends. Self-contained contract -- any language

  • writer/sources: IFramePayloadAdapter concept (sdk/include/vtx/writer/sources/frame_payload_adapter.h) -- shared compile-time contract for adapters used by streaming sources (PipeFrameDataSource, WebSocketFrameDataSource). Single requirement: bool ParseFrame(std::span<const std::byte> payload, VTX::Frame&, GameTime::GameTimeRegister&). The source owns the transport + framing; the adapter is the only place that knows the wire format (JSON / Protobuf / custom binary). Applied as the template constraint on WebSocketFrameDataSource

  • samples: vtx_sample_websocket_consumer (samples/websocket_consumer.cpp) -- connects to a WebSocket server as a client and records the stream into a .vtx. Uses a JsonWebSocketAdapter built on VTX::UniversalDeserializer<>::Load<WsFrame>(JsonAdapter) -- the JSON->struct step is fully declarative (JsonMapping<WsFrame> + JsonMapping<WsEntity>), only the struct->VTX::Frame mapping is spelled out. Companion samples/websocket_server.py -- minimal Python WebSocket server using the websockets library; streams JSON frames continuously (~20 entities/frame) until the client disconnects or the user presses Ctrl+C. CLI: vtx_sample_websocket_consumer ws://127.0.0.1:8765/ out.vtx schema.json

  • samples: vtx_sample_pipe_producer (samples/pipe_producer.cpp) + vtx_sample_pipe_consumer (samples/pipe_consumer.cpp) -- pair of CLI tools exercising the pipe data source. Producer emits length-prefixed JSON frames to stdout; consumer reads either stdin (-), a connected pipe (<path>), or creates one (serve:<path>). Producer supports two modes: bounded (producer N) sends exactly N frames then a sentinel; continuous (producer 0) streams indefinitely at ~60 fps until the user presses Enter in its terminal -- a detached stdin watcher thread sets an std::atomic<bool> stop flag, which makes the loop emit the sentinel cleanly and exit, so the consumer finalises a valid .vtx. Three demo .bat scripts:

    • samples/pipe_demo.bat -- anonymous-pipe demo (producer | consumer, single launcher, bounded run).
    • samples/named_pipe_vtx.bat + samples/named_pipe_producer.bat -- two-terminal demo of independent processes meeting over a Windows named pipe. VTX bat runs the consumer in server mode (serve:\\.\pipe\vtx); producer bat retry-connects (the redirect to a not-yet-existing pipe fails, so the retry loop IS the "wait for VTX" handshake -- mirrors how a real named-pipe client like a game injector waits for its server)
  • dependencies: IXWebSocket v12.0.0 + mbedTLS v3.6.2 via FetchContent (cmake/VtxDependencies.cmake). Same pattern as FlatBuffers / zstd: one pinned version on every platform, no system packages, nothing shipped at runtime. IXWebSocket consumed through a new VTX::deps::ixwebsocket INTERFACE target; linked PRIVATE into vtx_writer (and so never reaches the public SDK headers thanks to the websocket-client PIMPL). mbedTLS detection inside IXWebSocket is fragile across versions, so MBEDTLS_INCLUDE_DIRS + MBEDTLS_LIBRARY + MBEDTLS_X509_LIBRARY + MBEDTLS_CRYPTO_LIBRARY are pre-seeded in the CMake cache before FetchContent_MakeAvailable(ixwebsocket); IXWebSocket's bundled FindMbedTLS.cmake's find_path / find_library short-circuit on the already-set cache entries and resolve to our FetchCont...

Read more

VTX SDK v0.2.0

12 May 16:07
d135879

Choose a tag to compare

[0.2.0] - 2026-05-12

Added

  • writer/api: writer-side frame post-processor pipeline. A new hook fires inside ReplayWriter::RecordFrame after timer validation and before Serializer::FromNative consumes the native Frame, so whatever the processor mutates is what gets serialised to the on-disk .vtx. Three new public headers and three new facade methods materialise the feature:

    • sdk/include/vtx/writer/core/vtx_frame_post_processor.h -- IFramePostProcessor interface (Init / Process / Clear / PrintInfo), FramePostProcessorChain composable container, FramePostProcessorInitContext (frame_accessor + total_frames + schema/format version) and FramePostProcessContext (per-frame: global_frame_index, schema_version, frame_accessor) carriers. Chain execution: Init/Process/PrintInfo in registration order; Clear in reverse (destructor-like teardown); last writer wins on shared property mutations.
    • sdk/include/vtx/writer/core/vtx_frame_mutation_view.h -- write-side mirror of EntityView / FrameAccessor. EntityMutator (non-owning wrapper over PropertyContainer* with Get<T> + Set<T> + GetMutableView + GetMutableArray<T>); BucketMutator (mutable iteration + structural mutation: AddEntity / RemoveEntity / RemoveIf / Clear); FrameMutationView (entry point the processor receives -- wraps Frame& + borrows a FrameAccessor* so processors can resolve schema names without coupling to reader internals). Hot-path cost is identical to EntityView::Get -- single non-owning pointer indirection, fully inlinable.
    • IVtxWriterFacade::SetPostProcessor(std::shared_ptr<IFramePostProcessor>) / GetPostProcessor() / ClearPostProcessor() -- registration API on the writer facade, forwarded to both FlatBuffersWriterFacadeImpl and ProtobuffWriterFacadeImpl. Init() runs synchronously inside SetPostProcessor BEFORE the new processor becomes visible to any RecordFrame() -- this is the right place to resolve every PropertyKey<T> upfront since the schema is constant for the recording session. The writer is single-threaded by design (RecordFrame called sequentially from the capture loop) so no mutex is needed on post_processor_. The destructor invokes Clear() on whatever is currently registered. SetPostProcessor does NOT call Clear on the previously-registered processor; the caller keeps the shared_ptr and calls Clear explicitly if they need outgoing teardown -- use ClearPostProcessor() for the common case of explicit pre-destruction reset
  • scripts/codegen: scripts/vtx_codegen.py extended to emit, per schema struct, in addition to the existing XView read-only wrapper:

    • XMutator -- write-capable wrapper around EntityMutator. All Get* methods identical to the View; adds Set*(value) for scalars and GetMutable*() returning std::span<T> (arrays) or EntityMutator (nested structs). PropertyKey<T> resolution stays cached in static locals per-method on first use, so registering a processor doesn't trigger a one-time hash sweep.
    • ForEachX(BucketMutator&, FrameAccessor&, Fn) -- template helper that filters a bucket by entity_type_id (matching EntityType::X) and invokes the lambda with an XMutator&. Read-only counterpart ForEachXView(const Bucket&, FrameAccessor&, Fn) paralleled. Result: processors operate on strongly-typed views (p.SetHealth(...)) with zero hardcoded schema strings, zero PropertyKey<T> members on the processor, and no manual entity_type_id gating -- if the schema changes, regenerating the header makes new properties available; if a property is renamed or removed, code fails to compile early instead of silently mismatching at runtime
  • samples: vtx_sample_post_process_write target (samples/post_process_write.cpp) -- minimum end-to-end demo of the writer-side post-processor. Builds synthetic frames with intentionally out-of-range Health values via the codegen-generated PlayerMutator, registers a PlayerHealthProcessor (clamp [0, 100], derive IsAlive=false when Health<=0, cross-frame low-health counter, lifecycle hooks), records 30 frames, then re-opens the .vtx with OpenReplayFile and uses ForEachPlayerView (also codegen-generated) to print the persisted values -- proving the on-disk bytes contain the post-processed state, not the raw input

  • samples: samples/advance_write.cpp extended to register an ArenaConsistencyProcessor on each of the three pipelines (JSON / Protobuf / FlatBuffers source). Same processor instance per pipeline using VTX::ArenaSchema::ForEachPlayer -- demonstrates that frame post-processing is orthogonal to the source format: the same logic runs on the canonical VTX::Frame regardless of whether it came from JSON, Protobuf, or FlatBuffers

  • tests: tests/writer/test_frame_post_processor.cpp with 10 cases:

    • WriterPostProcessor_MutationViewUnit.SetThenGetRoundTrips and WriterPostProcessor_ChainUnit.OrderAndRemove -- standalone unit smokes for the mutation view + chain primitives.
    • WriterPostProcessorTest.NoProcessorBaselineUnchanged -- behaviour identical when no processor is registered.
    • WriterPostProcessorTest.DoubleHealthIsPersistedToDisk -- Init resolves the Health key, processor doubles values pre-serialise, readback confirms 200.0f on disk.
    • WriterPostProcessorTest.ChainLastWriterWinsOnDisk and .ChainRemoveDropsAndOtherStillFires -- chain ordering + Remove semantics from disk.
    • WriterPostProcessorTest.GhostInjectorEntityIsOnDisk -- BucketMutator::AddEntity injects a synthetic entity with entity_type_id set explicitly, readback confirms it persisted.
    • WriterPostProcessorTest.TeamTwoFilterDropsEntitiesFromDisk -- BucketMutator::RemoveIf filters entities pre-serialise.
    • WriterPostProcessorTest.GlobalFrameIndexIsMonotonic -- ctx.global_frame_index monotonically increments across RecordFrame calls.
    • WriterPostProcessorTest.ClearPostProcessorCallsClearAndUnregisters -- explicit teardown invokes Clear and subsequent RecordFrame calls bypass the processor entirely
  • docs: new docs/POST_PROCESSING.md -- dedicated reference covering the feature pipeline diagram, lifecycle (Init synchronous before first Process, Clear on destructor / explicit teardown), threading model (single-threaded writer, no mutex needed), two ways to write a processor (generic with raw PropertyKey<T> vs codegen-driven strongly-typed XMutator / ForEachX), patterns for cross-frame state / chains / replay-level metadata / schema-version branching / structural mutation, error handling (exceptions swallowed at hook boundary), performance characteristics (zero overhead when unused; same hot path as EntityView when active), gotchas (the FlatBuffers serialiser drops entities with entity_type_id < 0, the writer renames bucket[0] to "data" / bucket[1] to "bone_data" / drops bucket[2+] silently, type_ranges invalidated after RemoveIf but rebuilt by the serializer), and pointers to the runnable demos

  • docs: docs/SDK_API.md new "Frame Post-Processor" section between "Writing Replays" and "Diffing Frames" -- API cheat-sheet covering processor implementation, registration on the writer, chain composition, the strongly-typed codegen alternative, and the mutation view API surface. Links to POST_PROCESSING.md for the full reference

  • docs: docs/SAMPLES.md updated for the two new sample targets (vtx_sample_post_process_write and the post-processor addition in vtx_sample_advance_write) plus the extended arena_generated.h codegen output (now includes *Mutator classes + ForEachX helpers). "What each sample teaches" table gains four new rows covering the post-processor and codegen-driven typed accessor patterns

  • docs: README.md "Write a replay" snippet gains a sub-section showing a minimal IFramePostProcessor implementation and writer->SetPostProcessor registration. In-tree docs list updated to include POST_PROCESSING.md

  • scripts: scripts/check_clang_format.py (+ .sh and .bat wrappers) -- local mirror of the CI clang-format diff-gate. Validates only the lines you've modified vs a base ref (default origin/main), matching the CI's exclusion list (thirdparty/, *generated/, arena_generated.h, portable-file-dialogs.h) and scope (.cpp / .cc / .h / .hpp). Three modes via --fix / --base <ref>: read-only check, apply fixes in place, or check against a different ref. Auto-detects clang-format-diff.py under Program Files\LLVM\share\clang\ on Windows when it's not on PATH. Cross-platform wrappers delegate to the Python implementation. Exit codes match CI semantics: 0 clean, 1 violations, 2 tooling missing

  • scripts: scripts/git-hooks/pre-push -- versioned pre-push hook that runs check_clang_format.py and aborts the push on violation. Opt-in per clone via git config core.hooksPath scripts/git-hooks (built-in to git ≥ 2.9, no Husky / pre-commit dependency). Bypass for a one-off push with git push --no-verify

  • docs: docs/BUILD.md "Formatting gate" subsection extended with the local helper script usage (read-only check, --fix, --base arg) and the pre-push hook activation one-liner. Same coverage in CONTRIBUTING.md under "Validate formatting before pushing" + "Pre-push hook" so contributors landing on either doc find the workflow

Changed

  • sdk/include layout: vtx_frame_accessor.h moved from sdk/include/vtx/reader/core/ to sdk/include/vtx/common/. The header is fundamentally a schema utility (FrameAccessor resolves names against PropertyAddressCache, EntityView is a generic read-only wrapper over PropertyContainer); pre-move it lived under reader/ for historical reasons, which forced the new writer-side post-processor headers (vtx_frame_mutation_view.h, vtx_frame_post_processor.h) to either re-implement the ...
Read more

VTX SDK v0.1.1

28 Apr 13:04

Choose a tag to compare

[0.1.1] - 2026-04-28

Added

  • scripts: scripts/release_sdk.sh -- Linux/macOS counterpart to scripts/release_sdk.bat. Builds the SDK libs + vtx_cli in Release mode and installs into ./dist. Removes the build/release script asymmetry between Windows and Linux
  • reader/api: ReaderContext::IsReady(), IsReadyFailed(), GetReadyError(), WaitUntilReady() + WaitUntilReady(std::chrono::milliseconds) for explicit "first chunk in RAM" signalling, plus new ReplayReaderEvents::OnReady / OnReadyFailed callbacks. Previously ReaderContext::Loaded() flipped to true the instant OpenReplayFile() returned -- header and footer parsed, property-address cache built, seek table ready, but zero chunks decompressed in RAM. The first GetFrameSync() call still paid the full ZSTD + deserialise cost synchronously, and the Inspector already carried a redundant is_file_loaded_ flag alongside Loaded() to paper over the gap (tools/inspector/include/inspector_session.h:25). Now OpenReplayFile() eagerly kicks off an async load of chunk 0 as part of opening (via the existing WarmAt(0) / UpdateCacheWindow pipeline; empty 0-frame replays flip the flag vacuously through a new MarkReadyVacuous() facade hook so waiters never hang). Callers consume the signal in whichever style they prefer: poll (while (!ctx.IsReady()) ...), block (ctx.WaitUntilReady(2s)), or register a callback (OnReady / OnReadyFailed fire exactly once each, single-shot guarded under ready_mutex_ so racing async + sync load paths cannot double-fire). Failure semantics: a corrupt or unreadable chunk 0 does NOT fail OpenReplayFile() itself -- the reader is still constructed, IsReadyFailed() returns true, GetReadyError() carries the message, and downstream GetFrame*() calls behave as before (return nullptr / empty). The header-parsed-ok-but-chunk-zero-broken state stays useful to inspector-style tools that want to show partial file info. Destructor best-effort unblocks any waiter by flipping ready_failed_ + notifying the condition variable under ready_mutex_; callers remain responsible for joining their waiter threads before destroying the ReaderContext (C++ standard requires no blocked waiters at condition-variable destruction time)
  • tests: six new cases in tests/reader/test_reader_context.cpp under "§READY: chunk-0 ready signalling". ReaderContextHappy.ReadyFlipsWithinTimeoutOnValidReplay asserts WaitUntilReady(5s) returns true on a well-formed replay; ReadyIsStableAcrossRepeatedQueries pins the terminal-state stability guarantee; WaitUntilReadyIsIdempotent asserts repeated calls after ready return immediately; ReaderContextReady.OnReadyFiresOnDirectFacadeWithPreWiredEvents uses CreateFlatBuffersFacade() directly, wires events before WarmAt(0), and polls an atomic counter to verify single-shot firing; ReadyIsVacuousForZeroFrameReplay exercises the MarkReadyVacuous path with a GTEST_SKIP fallback if the writer refuses a 0-frame replay; ReadyFailsOnCorruptChunkZero writes a valid file then overwrites its middle third with 0xFF bytes and verifies WaitUntilReady returns false + IsReadyFailed() + non-empty GetReadyError(). No destruction-race test: destroying std::condition_variable / std::mutex while waiters are blocked is UB per the standard, so the API contract is "join waiters before destroying" and the dtor's notify_all is best-effort only

Changed

  • repo layout: all five build/clean/release wrappers moved from the repo root into scripts/ (build_sdk.bat, build_sdk.sh, clean.bat, clean.sh, release_sdk.bat). Each script now cds to the repo root internally so invocations like ./scripts/build_sdk.sh or scripts\build_sdk.bat work from any working directory. Documentation references (README, CONTRIBUTING, docs/BUILD.md) updated accordingly
  • repo layout: reports/benchmarks/ renamed to docs/benchmarks/ to signal that the committed baseline outputs are reference documentation (co-located with docs/PERFORMANCE.md which narrates them) rather than stray CI artefacts. reports/ directory removed. References in docs/PERFORMANCE.md, docs/BUILD.md, and the benchmark write-ups updated
  • reader/api: OpenReplayFile() now triggers an eager prefetch of chunk 0 via the existing async pipeline before returning. Open latency on the calling thread is unchanged because the load runs on the same background thread WarmAt / UpdateCacheWindow already dispatches to; the prior "first GetFrame*() is slow" cost is moved off the first access onto the open-time spawn path (same total work, just overlapped with caller init). Only chunk 0 is warmed -- the facade temporarily narrows the cache window to (0, 0) around the warm call and restores it to the default (2, 2) immediately after, so callers that set a narrow window right after OpenReplayFile() (memory-constrained tools, tests that isolate a single chunk) observe exactly the cache contents they asked for. ReaderContext::Loaded() semantics are unchanged: still means "reader object exists". New concept is IsReady() == "chunk 0 decompressed and deserialised in RAM"

VTX SDK v0.1.0

24 Apr 15:29

Choose a tag to compare

[0.1.0] - 2026-04-24

Added

  • reader: IVtxReaderFacade::WarmAt(int32_t frame_index) (§3.A) -- explicit prefetch hint. If the enclosing chunk is cached or in flight, this is a no-op; otherwise it kicks off an asynchronous load and returns immediately. Intended use is to call WarmAt(target_frame) at the end of a seek gesture so the ZSTD decompress overlaps with any UI teardown, eliminating the "first frame after seek is slow" stutter. Implemented by routing through UpdateCacheWindow, which means WarmAt also updates the §1.B EWMA -- from the reader's perspective it is indistinguishable from a "virtual" access

  • tests: ReaderApiFlatBuffers.CancelledPrefetchReEntersWindow -- focused regression for the UpdateCacheWindow cancel + re-enter bug (see Fixed). Runs the cancel + re-enter pattern (prime chunks 0..2, jump to chunk 10 to cancel, jump to chunk 2 to revive) 50 times against a fresh reader each iteration. Pre-fix this fails ~every run under TSan and flakes at single-digit-% on stock release; post-fix it is deterministic green on both

  • tests: two new regression tests in tests/reader/test_reader_api.cpp.

    • ReaderApiFlatBuffers.RandomAccessSkipsLateralPrefetches -- writes a 20-chunk replay, opens with SetCacheWindow(2, 2), performs 10 far-apart jumps (distance 10 chunks each, well above window=2). Asserts that the total distinct chunks loaded is <= 2 * jump_count; pre-§1.B this would be ~5x. The bound is conservative enough to tolerate the first two EWMA bootstrap samples still triggering laterals, tight enough to catch a regression
    • ReaderApiFlatBuffers.WarmAtTriggersAsyncLoadWithoutReading -- opens a 5-chunk replay with SetCacheWindow(0, 0), calls WarmAt(30), polls ReaderChunkState::GetSnapshot() with a 5s deadline, asserts chunk 3 is present. Pins the WarmAt contract: load happens asynchronously, no GetFrame required
  • tests: 47 new tests across 8 new files, driven by a targeted SDK audit. Test suite total: 89 -> 187 passing + 1 intentionally skipped (awaiting a fixture schema with a Map field).

Read more