Releases: ZenosInteractive/VTX
VTX SDK v0.3.0
[0.3.0] - 2026-05-26
Added
-
tests: integration coverage for both new streaming
IFrameDataSourceimplementations -- runs in the existing GoogleTest suite and so on every GitHub Actions matrix entry (Windows + Linux).tests/writer/test_pipe_source.cppdrivesPipeFrameDataSource<TestAdapter>in server mode with VTX on the receive end and a test-spawned client thread as the producer (WindowsCreateFileA+WriteFile, POSIXopen+write); covers the happy path (50 frames + sentinel round-trip viaOpenReplayFile), sentinel-only empty stream, adapter-false-stops-stream after N valid frames, and theGetExpectedTotalFrames() == 0streaming contract.tests/writer/test_websocket_source.cppdrivesWebSocketFrameDataSource<TestWsAdapter>against an in-processix::WebSocketServerloopback 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 aDrainSourceIntoWriterhelper that takes the writer byunique_ptrand destroys it before reading back the file -- theChunkedFileSinkonly flushes itsstd::ofstreamin its destructor, so an in-scope writer would race the reader.vtx_testslinks$<BUILD_INTERFACE:VTX::deps::ixwebsocket>directly becausevtx_writerkeeps the dep PRIVATE behind its PIMPL boundary. WebSocket happy-path uses the per-client message callback (setOnClientMessageCallback, v12 path) which gives aWebSocket&directly and avoids the weak_ptr-vs-handshake race that the v11-stylesetOnConnectionCallbacktriggers; the bound port is pre-allocated with a rawbind+getsocknamedance becauseix::WebSocketServer::getPort()returns the constructor argument, not the OS-assigned port -
writer/sinks:
ChunkedNetworkSink<Policy>-- second sink alongsideChunkedFileSink, streams the identical binary.vtxbyte 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 headersdk/include/vtx/writer/policies/sinks/network_sink.h-- self-contained Win32/POSIX socket portability layer (getaddrinfo+socket+connect), throwsstd::runtime_erroron connect failure (parallel toChunkedFileSinkthrowing on open failure),SendAllretries on partialsend, chunk index entries recordfile_offsetvia a runningbytes_sent_counter (thetellp()analogue for a stream). New facade entry points:NetworkWriterFacadeConfig(host + port + the usual replay/chunking/compression knobs, nooutput_filepath) plusCreateFlatBuffersNetworkWriterFacade/CreateProtobuffNetworkWriterFacade-- mirror of the file-sink factories, behind the sameIVtxWriterFacadeabstraction so existing user code is sink-agnostic.vtx_writerpropagatesws2_32PUBLIC on Windows so consumers that instantiateChunkedNetworkSinkget the linkage automatically. Six integration tests intests/writer/test_network_sink.cppspin up a loopback TCP server on a free ephemeral port (server thread signals readiness viastd::promiseso the client cannot connect beforelisten()returns), drain bytes into a vector, and prove the received stream parses cleanly viaVTX::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>-- streamingIFrameDataSourceover WebSocket (RFC 6455), connects as a client tows://host:port/pathorwss://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 headersdk/include/vtx/writer/sources/websocket_frame_source.hplus a PIMPL facade insdk/include/vtx/writer/sources/detail/websocket_client.h-- the implementation insdk/src/vtx_writer/.../websocket_client.cppwraps IXWebSocket and bridges its async callbacks to a blockingReadMessagevia a thread-safe queue (std::mutex+std::condition_variable), so the writer's pull-basedGetNextFrameslots 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.vtxinstead of silently resuming mid-file. Format-agnostic: theAdapter(constrained by theIFramePayloadAdapterconcept) is the only piece that knows the on-wire payload format -- the sample'sJsonWebSocketAdapterparses each message as JSON vianlohmann::json+JsonMapping<T>+UniversalDeserializer(declarative -- same pattern asarena_mappings.h) -
writer/sources:
PipeFrameDataSource<Adapter>-- streamingIFrameDataSourceover OS pipes, format-agnostic via a caller-supplied adapter. New public headersdk/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 theAdapter(sample uses JSON). Three transport modes selected byConfig:- stdin (
pipe_pathempty) -- for shell pipelinesproducer | vtx. Sets_O_BINARYon Windows so CRLF translation doesn't corrupt the wire. - client (
pipe_pathset,as_server = false) -- VTX connects to a pipe / FIFO a producer already created (fopen("rb")). - server (
pipe_pathset,as_server = true) -- VTX creates the pipe and blocks waiting for a producer to connect. Windows:CreateNamedPipeA(PIPE_ACCESS_INBOUND, PIPE_TYPE_BYTE)thenConnectNamedPipe, with the resultingHANDLEwrapped in aFILE*via_open_osfhandle+_fdopensoReadExact/GetNextFramestay platform-agnostic. POSIX:mkfifo(0666)(toleratesEEXIST) thenfopen-- blocks until a writer opens the other end. The FIFO isunlinked on destruction.
Server mode is the mode for an external, independent producer -- e.g. a game injector that opens
\\.\pipe\vtxas 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 - stdin (
-
writer/sources:
IFramePayloadAdapterconcept (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 onWebSocketFrameDataSource -
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 aJsonWebSocketAdapterbuilt onVTX::UniversalDeserializer<>::Load<WsFrame>(JsonAdapter)-- the JSON->struct step is fully declarative (JsonMapping<WsFrame>+JsonMapping<WsEntity>), only the struct->VTX::Framemapping is spelled out. Companionsamples/websocket_server.py-- minimal Python WebSocket server using thewebsocketslibrary; 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 anstd::atomic<bool>stop flag, which makes the loop emit the sentinel cleanly and exit, so the consumer finalises a valid.vtx. Three demo.batscripts: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 newVTX::deps::ixwebsocketINTERFACE target; linked PRIVATE intovtx_writer(and so never reaches the public SDK headers thanks to the websocket-client PIMPL). mbedTLS detection inside IXWebSocket is fragile across versions, soMBEDTLS_INCLUDE_DIRS+MBEDTLS_LIBRARY+MBEDTLS_X509_LIBRARY+MBEDTLS_CRYPTO_LIBRARYare pre-seeded in the CMake cache beforeFetchContent_MakeAvailable(ixwebsocket); IXWebSocket's bundledFindMbedTLS.cmake'sfind_path/find_libraryshort-circuit on the already-set cache entries and resolve to our FetchCont...
VTX SDK v0.2.0
[0.2.0] - 2026-05-12
Added
-
writer/api: writer-side frame post-processor pipeline. A new hook fires inside
ReplayWriter::RecordFrameafter timer validation and beforeSerializer::FromNativeconsumes the nativeFrame, 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--IFramePostProcessorinterface (Init/Process/Clear/PrintInfo),FramePostProcessorChaincomposable container,FramePostProcessorInitContext(frame_accessor + total_frames + schema/format version) andFramePostProcessContext(per-frame: global_frame_index, schema_version, frame_accessor) carriers. Chain execution:Init/Process/PrintInfoin registration order;Clearin reverse (destructor-like teardown); last writer wins on shared property mutations.sdk/include/vtx/writer/core/vtx_frame_mutation_view.h-- write-side mirror ofEntityView/FrameAccessor.EntityMutator(non-owning wrapper overPropertyContainer*withGet<T>+Set<T>+GetMutableView+GetMutableArray<T>);BucketMutator(mutable iteration + structural mutation:AddEntity/RemoveEntity/RemoveIf/Clear);FrameMutationView(entry point the processor receives -- wrapsFrame&+ borrows aFrameAccessor*so processors can resolve schema names without coupling to reader internals). Hot-path cost is identical toEntityView::Get-- single non-owning pointer indirection, fully inlinable.IVtxWriterFacade::SetPostProcessor(std::shared_ptr<IFramePostProcessor>)/GetPostProcessor()/ClearPostProcessor()-- registration API on the writer facade, forwarded to bothFlatBuffersWriterFacadeImplandProtobuffWriterFacadeImpl.Init()runs synchronously insideSetPostProcessorBEFORE the new processor becomes visible to anyRecordFrame()-- this is the right place to resolve everyPropertyKey<T>upfront since the schema is constant for the recording session. The writer is single-threaded by design (RecordFramecalled sequentially from the capture loop) so no mutex is needed onpost_processor_. The destructor invokesClear()on whatever is currently registered.SetPostProcessordoes NOT callClearon the previously-registered processor; the caller keeps theshared_ptrand callsClearexplicitly if they need outgoing teardown -- useClearPostProcessor()for the common case of explicit pre-destruction reset
-
scripts/codegen:
scripts/vtx_codegen.pyextended to emit, per schema struct, in addition to the existingXViewread-only wrapper:XMutator-- write-capable wrapper aroundEntityMutator. AllGet*methods identical to the View; addsSet*(value)for scalars andGetMutable*()returningstd::span<T>(arrays) orEntityMutator(nested structs).PropertyKey<T>resolution stays cached instaticlocals 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 byentity_type_id(matchingEntityType::X) and invokes the lambda with anXMutator&. Read-only counterpartForEachXView(const Bucket&, FrameAccessor&, Fn)paralleled. Result: processors operate on strongly-typed views (p.SetHealth(...)) with zero hardcoded schema strings, zeroPropertyKey<T>members on the processor, and no manualentity_type_idgating -- 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_writetarget (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-generatedPlayerMutator, registers aPlayerHealthProcessor(clamp[0, 100], deriveIsAlive=falsewhenHealth<=0, cross-frame low-health counter, lifecycle hooks), records 30 frames, then re-opens the.vtxwithOpenReplayFileand usesForEachPlayerView(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.cppextended to register anArenaConsistencyProcessoron each of the three pipelines (JSON / Protobuf / FlatBuffers source). Same processor instance per pipeline usingVTX::ArenaSchema::ForEachPlayer-- demonstrates that frame post-processing is orthogonal to the source format: the same logic runs on the canonicalVTX::Frameregardless of whether it came from JSON, Protobuf, or FlatBuffers -
tests:
tests/writer/test_frame_post_processor.cppwith 10 cases:WriterPostProcessor_MutationViewUnit.SetThenGetRoundTripsandWriterPostProcessor_ChainUnit.OrderAndRemove-- standalone unit smokes for the mutation view + chain primitives.WriterPostProcessorTest.NoProcessorBaselineUnchanged-- behaviour identical when no processor is registered.WriterPostProcessorTest.DoubleHealthIsPersistedToDisk--Initresolves the Health key, processor doubles values pre-serialise, readback confirms 200.0f on disk.WriterPostProcessorTest.ChainLastWriterWinsOnDiskand.ChainRemoveDropsAndOtherStillFires-- chain ordering +Removesemantics from disk.WriterPostProcessorTest.GhostInjectorEntityIsOnDisk--BucketMutator::AddEntityinjects a synthetic entity withentity_type_idset explicitly, readback confirms it persisted.WriterPostProcessorTest.TeamTwoFilterDropsEntitiesFromDisk--BucketMutator::RemoveIffilters entities pre-serialise.WriterPostProcessorTest.GlobalFrameIndexIsMonotonic--ctx.global_frame_indexmonotonically increments acrossRecordFramecalls.WriterPostProcessorTest.ClearPostProcessorCallsClearAndUnregisters-- explicit teardown invokesClearand subsequentRecordFramecalls bypass the processor entirely
-
docs: new
docs/POST_PROCESSING.md-- dedicated reference covering the feature pipeline diagram, lifecycle (Initsynchronous before firstProcess,Clearon destructor / explicit teardown), threading model (single-threaded writer, no mutex needed), two ways to write a processor (generic with rawPropertyKey<T>vs codegen-driven strongly-typedXMutator/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 asEntityViewwhen active), gotchas (the FlatBuffers serialiser drops entities withentity_type_id < 0, the writer renames bucket[0] to "data" / bucket[1] to "bone_data" / drops bucket[2+] silently,type_rangesinvalidated afterRemoveIfbut rebuilt by the serializer), and pointers to the runnable demos -
docs:
docs/SDK_API.mdnew "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 toPOST_PROCESSING.mdfor the full reference -
docs:
docs/SAMPLES.mdupdated for the two new sample targets (vtx_sample_post_process_writeand the post-processor addition invtx_sample_advance_write) plus the extendedarena_generated.hcodegen output (now includes*Mutatorclasses +ForEachXhelpers). "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 minimalIFramePostProcessorimplementation andwriter->SetPostProcessorregistration. In-tree docs list updated to includePOST_PROCESSING.md -
scripts:
scripts/check_clang_format.py(+.shand.batwrappers) -- local mirror of the CI clang-format diff-gate. Validates only the lines you've modified vs a base ref (defaultorigin/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-detectsclang-format-diff.pyunderProgram Files\LLVM\share\clang\on Windows when it's not onPATH. 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 runscheck_clang_format.pyand aborts the push on violation. Opt-in per clone viagit config core.hooksPath scripts/git-hooks(built-in to git ≥ 2.9, no Husky / pre-commit dependency). Bypass for a one-off push withgit push --no-verify -
docs:
docs/BUILD.md"Formatting gate" subsection extended with the local helper script usage (read-only check,--fix,--basearg) and the pre-push hook activation one-liner. Same coverage inCONTRIBUTING.mdunder "Validate formatting before pushing" + "Pre-push hook" so contributors landing on either doc find the workflow
Changed
- sdk/include layout:
vtx_frame_accessor.hmoved fromsdk/include/vtx/reader/core/tosdk/include/vtx/common/. The header is fundamentally a schema utility (FrameAccessorresolves names againstPropertyAddressCache,EntityViewis a generic read-only wrapper overPropertyContainer); pre-move it lived underreader/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 ...
VTX SDK v0.1.1
[0.1.1] - 2026-04-28
Added
- scripts:
scripts/release_sdk.sh-- Linux/macOS counterpart toscripts/release_sdk.bat. Builds the SDK libs +vtx_cliin 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 newReplayReaderEvents::OnReady/OnReadyFailedcallbacks. PreviouslyReaderContext::Loaded()flipped totruethe instantOpenReplayFile()returned -- header and footer parsed, property-address cache built, seek table ready, but zero chunks decompressed in RAM. The firstGetFrameSync()call still paid the full ZSTD + deserialise cost synchronously, and the Inspector already carried a redundantis_file_loaded_flag alongsideLoaded()to paper over the gap (tools/inspector/include/inspector_session.h:25). NowOpenReplayFile()eagerly kicks off an async load of chunk 0 as part of opening (via the existingWarmAt(0)/UpdateCacheWindowpipeline; empty 0-frame replays flip the flag vacuously through a newMarkReadyVacuous()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/OnReadyFailedfire exactly once each, single-shot guarded underready_mutex_so racing async + sync load paths cannot double-fire). Failure semantics: a corrupt or unreadable chunk 0 does NOT failOpenReplayFile()itself -- the reader is still constructed,IsReadyFailed()returnstrue,GetReadyError()carries the message, and downstreamGetFrame*()calls behave as before (returnnullptr/ 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 flippingready_failed_+ notifying the condition variable underready_mutex_; callers remain responsible for joining their waiter threads before destroying theReaderContext(C++ standard requires no blocked waiters at condition-variable destruction time) - tests: six new cases in
tests/reader/test_reader_context.cppunder "§READY: chunk-0 ready signalling".ReaderContextHappy.ReadyFlipsWithinTimeoutOnValidReplayassertsWaitUntilReady(5s)returnstrueon a well-formed replay;ReadyIsStableAcrossRepeatedQueriespins the terminal-state stability guarantee;WaitUntilReadyIsIdempotentasserts repeated calls after ready return immediately;ReaderContextReady.OnReadyFiresOnDirectFacadeWithPreWiredEventsusesCreateFlatBuffersFacade()directly, wires events beforeWarmAt(0), and polls an atomic counter to verify single-shot firing;ReadyIsVacuousForZeroFrameReplayexercises theMarkReadyVacuouspath with aGTEST_SKIPfallback if the writer refuses a 0-frame replay;ReadyFailsOnCorruptChunkZerowrites a valid file then overwrites its middle third with0xFFbytes and verifiesWaitUntilReadyreturnsfalse+IsReadyFailed()+ non-emptyGetReadyError(). No destruction-race test: destroyingstd::condition_variable/std::mutexwhile waiters are blocked is UB per the standard, so the API contract is "join waiters before destroying" and the dtor'snotify_allis 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 nowcds to the repo root internally so invocations like./scripts/build_sdk.shorscripts\build_sdk.batwork from any working directory. Documentation references (README, CONTRIBUTING, docs/BUILD.md) updated accordingly - repo layout:
reports/benchmarks/renamed todocs/benchmarks/to signal that the committed baseline outputs are reference documentation (co-located withdocs/PERFORMANCE.mdwhich narrates them) rather than stray CI artefacts.reports/directory removed. References indocs/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 threadWarmAt/UpdateCacheWindowalready dispatches to; the prior "firstGetFrame*()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 afterOpenReplayFile()(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 isIsReady()== "chunk 0 decompressed and deserialised in RAM"
VTX SDK v0.1.0
[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 callWarmAt(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 throughUpdateCacheWindow, which means WarmAt also updates the §1.B EWMA -- from the reader's perspective it is indistinguishable from a "virtual" accesstests:
ReaderApiFlatBuffers.CancelledPrefetchReEntersWindow-- focused regression for theUpdateCacheWindowcancel + 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 bothtests: two new regression tests in
tests/reader/test_reader_api.cpp.ReaderApiFlatBuffers.RandomAccessSkipsLateralPrefetches-- writes a 20-chunk replay, opens withSetCacheWindow(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 regressionReaderApiFlatBuffers.WarmAtTriggersAsyncLoadWithoutReading-- opens a 5-chunk replay withSetCacheWindow(0, 0), callsWarmAt(30), pollsReaderChunkState::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).