Skip to content

feat(writer): Added frame post processing. Frame post processing allo…#12

Merged
virtexalejandro merged 1 commit into
mainfrom
feature/frame-post-processor
May 12, 2026
Merged

feat(writer): Added frame post processing. Frame post processing allo…#12
virtexalejandro merged 1 commit into
mainfrom
feature/frame-post-processor

Conversation

@virtexalejandro
Copy link
Copy Markdown
Contributor

Writer-side hook that runs on every RecordFrame() after timer validation and before the serializer touches the native Frame. Whatever the processor mutates is what gets persisted to disk -- callers who re-read the .vtx see the post-processed values.

What's in this PR

Core API (writer-side)

  • sdk/include/vtx/writer/core/vtx_frame_post_processor.h (new)
    IFramePostProcessor interface with Init / Process / Clear / PrintInfo lifecycle; FramePostProcessorChain for composition; FramePostProcessorInitContext + FramePostProcessContext carriers.
  • sdk/include/vtx/writer/core/vtx_frame_mutation_view.h (new)
    Write-side mirror of EntityView / FrameAccessor. EntityMutator (Get + Set + nested + array), BucketMutator (mutable iteration + structural mutation: AddEntity / RemoveEntity / RemoveIf / Clear), FrameMutationView (entry point with FrameAccessor* for schema resolution).
  • sdk/include/vtx/writer/core/writer.h (modified)
    Hook in RecordFrame() after timer validation, before Serializer::FromNative. Destructor invokes processor->Clear().
  • sdk/include/vtx/writer/core/vtx_writer_facade.h + .cpp (modified)
    Three new virtual methods on IVtxWriterFacade: SetPostProcessor / GetPostProcessor / ClearPostProcessor, forwarded to both FlatBuffers and Protobuf impls. Init() runs synchronously on SetPostProcessor BEFORE the new processor becomes visible to any RecordFrame().

Structural refactor (sustaining the feature)

  • vtx_frame_accessor.h moved from sdk/include/vtx/reader/core/ to sdk/include/vtx/common/.
    Pre-move it lived under reader/ for historical reasons; the new writer-side headers would have created a writer→reader cross-dependency. Post-move both writer/core/ and reader/core/ share common/vtx_frame_accessor.h with zero include edges between them.
    11 include sites updated: benchmarks, tests, samples, scripts, reader, writer headers.

Codegen extension

  • scripts/vtx_codegen.py (modified)
    Emits per struct alongside the existing XView:
    • XMutator -- write-capable wrapper; Get* (same as View) + Set* for scalars + GetMutable* for arrays / nested.
    • ForEachX(bucket, accessor, fn) -- filters a BucketMutator by entity_type_id, invokes fn with a strongly-typed XMutator&.
    • ForEachXView(bucket, accessor, fn) -- read-only counterpart.
  • samples/arena_generated.h regenerated; now includes PlayerMutator, ProjectileMutator, MatchStateMutator, plus all the ForEachX helpers.

Tests (tests/writer/test_frame_post_processor.cpp, 10 cases)

  • MutationViewUnit.SetThenGetRoundTrips -- unit smoke for the mutation view primitives.
  • ChainUnit.OrderAndRemove -- chain composition + removal semantics.
  • NoProcessorBaselineUnchanged -- zero-overhead path identical to baseline.
  • DoubleHealthIsPersistedToDisk -- Init resolves the Health key; processor doubles values pre-serialise; readback confirms 200.0f on disk.
  • ChainLastWriterWinsOnDisk -- ordering semantics.
  • ChainRemoveDropsAndOtherStillFires -- Chain::Remove correctness.
  • GhostInjectorEntityIsOnDisk -- BucketMutator::AddEntity + explicit entity_type_id persists into the file.
  • TeamTwoFilterDropsEntitiesFromDisk -- BucketMutator::RemoveIf pre-serialise filter.
  • GlobalFrameIndexIsMonotonic -- ctx.global_frame_index monotonically increments.
  • ClearPostProcessorCallsClearAndUnregisters -- explicit teardown + subsequent RecordFrame bypasses processor.

Samples

  • samples/post_process_write.cpp (new) -- minimal end-to-end demo. Synthetic frames with out-of-range Health, PlayerHealthProcessor clamps via PlayerMutator from the codegen output, re-opens the .vtx with the reader to verify persisted bytes.
  • samples/advance_write.cpp (modified) -- registers ArenaConsistencyProcessor on every pipeline (JSON / Protobuf / FlatBuffers). The same processor instance per pipeline -- proves post-processing is orthogonal to the source format.

Documentation

  • docs/POST_PROCESSING.md (new) -- dedicated reference: pipeline diagram, lifecycle, threading model, two ways to write a processor (generic vs codegen), patterns (cross-frame state, chains, replay-level metadata, schema-version branching, structural mutation), gotchas, performance, out-of-scope.
  • docs/SDK_API.md -- new "Frame Post-Processor" section between "Writing Replays" and "Diffing Frames".
  • docs/SAMPLES.md -- two new samples documented (post_process_write standalone + post-processor in advance_write), codegen output extended description.
  • README.md -- "Optional: post-process every frame" snippet under "Write a replay" + POST_PROCESSING.md in the docs list.

Design highlights

  • Schema-agnostic at SDK level: IFramePostProcessor operates on generic Frame / PropertyContainer. Each integration plugs in via codegen-generated types from their own schema JSON. The SDK doesn't know about Player / Champion / Projectile -- the codegen output does.
  • SOLID: processors are single-responsibility; chain composes them open/closed.
  • Zero overhead when not used: hot path is one null-check + one untaken branch when no processor is set.
  • Single-threaded writer: no mutex needed on post_processor_ (writer is the user's capture thread).
  • Error tolerance: out-of-range Set is silent no-op (matches EntityView::Get tolerance); processor exceptions are swallowed with VTX_ERROR.

…ws to sanitize, mofidy, infer or remove data before it goes inside of VTX. It runs in the writer during the ProcessFrame, implemented as single post process or a a chain of post process. Call SetPostPRocessor from the writer facade and pass the passing a FramePostProcessorChain previusly filled with IFramePostProcessor

To achive this vtx_codegen.py is extended to generate entityview mutators to easily modify values by SetHealth,SetArmor,etc. Removing the need of knowing and harcoding strings in cpp.

Added a unit test, bencharmk and samples of the new frame post processing benchmark
@virtexalejandro virtexalejandro self-assigned this May 12, 2026
@virtexalejandro virtexalejandro merged commit d135879 into main May 12, 2026
9 checks passed
@virtexalejandro virtexalejandro deleted the feature/frame-post-processor branch May 12, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant