-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpost_process_write.cpp
More file actions
223 lines (193 loc) · 9.01 KB
/
post_process_write.cpp
File metadata and controls
223 lines (193 loc) · 9.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// post_process_write.cpp -- Record synthetic frames with a frame post-processor.
//
// Purpose
// Demonstrates the writer-side frame post-processor pipeline using the
// strongly-typed API generated by scripts/vtx_codegen.py. The processor
// has zero hardcoded strings, zero PropertyKey<T> members, and no manual
// entity_type_id gating -- the codegen takes care of all of that.
//
// This sample drives four behaviours through a single processor:
// 1. Clamp Health to [0, 100] (value mutation)
// 2. Force IsAlive=false when Health drops to 0 (derived state)
// 3. Count frames where a player is "low health" (cross-frame state)
// 4. Dump that count via PrintInfo() on teardown (lifecycle hook)
//
// After the .vtx is written we re-open it with the reader and print the
// first few frames to prove the persisted bytes reflect the mutations.
//
// Args
// argv[1] -- schema JSON path (default: content/writer/arena/arena_schema.json)
// argv[2] -- output .vtx path (default: post_processed.vtx)
// argv[3] -- frame count (default: 30)
//
// Build
// Link against vtx_writer + vtx_reader (vtx_common is transitive).
#include "vtx/writer/core/vtx_writer_facade.h"
#include "vtx/writer/core/vtx_frame_post_processor.h"
#include "vtx/writer/core/vtx_frame_mutation_view.h"
#include "vtx/reader/core/vtx_reader_facade.h"
#include "vtx/common/vtx_types.h"
#include "vtx/common/vtx_logger.h"
#include "arena_generated.h"
#include <algorithm>
#include <memory>
#include <string>
// ---------------------------------------------------------------------------
// PlayerHealthProcessor
// ---------------------------------------------------------------------------
// Uses the codegen-generated PlayerMutator + ForEachPlayer helpers.
// * No PropertyKey<T> members.
// * No Init() to resolve them (the Mutator caches keys in static locals).
// * No manual entity_type_id gate (ForEachPlayer does it).
// * No "Player" / "Health" string literals in the processor body.
class PlayerHealthProcessor : public VTX::IFramePostProcessor {
public:
void Process(VTX::FrameMutationView& view, const VTX::FramePostProcessContext& ctx) override {
if (!view.HasBucket("entity"))
return;
auto bucket = view.GetBucket("entity");
VTX::ArenaSchema::ForEachPlayer(bucket, *view.accessor(), [&](auto& p) {
// (1) Clamp Health to [0, 100].
const float hp = p.GetHealth();
const float clamped_hp = std::clamp(hp, 0.0f, 100.0f);
if (clamped_hp != hp)
p.SetHealth(clamped_hp);
// (2) Derived state: IsAlive=false when Health hits 0.
if (clamped_hp <= 0.0f && p.GetIsAlive())
p.SetIsAlive(false);
// (3) Cross-frame accumulator: count "low health" frame-entities.
// Demonstrates state lives on the processor instance.
if (clamped_hp < 50.0f)
++low_health_frame_entities_;
});
++frames_seen_;
last_global_frame_ = ctx.global_frame_index;
}
void PrintInfo() const override {
VTX_INFO("PlayerHealthProcessor: frames_seen={}, last_global_frame={}, "
"low_health_frame_entities={}",
frames_seen_, last_global_frame_, low_health_frame_entities_);
}
void Clear() override {
VTX_INFO("PlayerHealthProcessor::Clear -- resetting state");
frames_seen_ = 0;
last_global_frame_ = -1;
low_health_frame_entities_ = 0;
}
private:
int frames_seen_ = 0;
int32_t last_global_frame_ = -1;
int low_health_frame_entities_ = 0;
};
// ---------------------------------------------------------------------------
// Helpers: build a synthetic Player frame with two entities.
// ---------------------------------------------------------------------------
// Builds entities using PlayerMutator. Health is intentionally driven out
// of range (negative on even frames, >100 on multiples of 7) so the
// processor's clamp has something to do.
//
// Note: PlayerMutator writes into pre-sized typed vectors of the
// PropertyContainer. The writer's schema layout dictates how big each
// vector is -- we resize them here based on the schema struct definition
// (counted from arena_generated.h).
static VTX::Frame MakeSyntheticFrame(int frame_index, const VTX::FrameAccessor& accessor) {
VTX::Frame frame;
auto& bucket = frame.CreateBucket("entity");
auto push_player = [&](int player_id, int team, float health) {
VTX::PropertyContainer pc;
pc.entity_type_id = static_cast<int32_t>(VTX::ArenaSchema::EntityType::Player);
// Pre-size the typed vectors so PlayerMutator::SetX can index them.
// Player schema: UniqueID, Name (strings); Team, Score, Deaths (int32);
// Health, Armor (floats); Position, Velocity (vectors); Rotation (quat);
// IsAlive (bool).
pc.string_properties.resize(2);
pc.int32_properties.resize(3);
pc.float_properties.resize(2);
pc.vector_properties.resize(2);
pc.quat_properties.resize(1);
pc.bool_properties.resize(1);
VTX::ArenaSchema::PlayerMutator p(pc, accessor);
p.SetUniqueID("player_" + std::to_string(player_id));
p.SetName("Player_" + std::to_string(player_id));
p.SetTeam(team);
p.SetHealth(health);
p.SetArmor(50.0f);
p.SetPosition({double(frame_index), double(player_id), 0.0});
p.SetVelocity({});
p.SetRotation({0.0f, 0.0f, 0.0f, 1.0f});
p.SetIsAlive(true);
p.SetScore(0);
p.SetDeaths(0);
bucket.unique_ids.push_back("player_" + std::to_string(player_id));
bucket.entities.push_back(std::move(pc));
};
// Two players. P0 drifts into negative health on even frames; P1 drifts
// over 100 every 7th frame. Both will be clamped by the processor.
push_player(0, 1, (frame_index % 2 == 0) ? -5.0f - float(frame_index) : 80.0f);
push_player(1, 2, (frame_index % 7 == 0) ? 150.0f : 60.0f - float(frame_index) * 0.5f);
return frame;
}
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] : "post_processed.vtx";
const int frame_count = (argc > 3) ? std::max(1, std::atoi(argv[3])) : 30;
// --- 1. Build the writer + register the processor BEFORE the first frame.
VTX::WriterFacadeConfig config;
config.output_filepath = output_path;
config.schema_json_path = schema_path;
config.replay_name = "PostProcessSample";
config.replay_uuid = "sample-postproc-0001";
config.default_fps = 60.0f;
config.chunk_max_frames = 16;
config.use_compression = true;
auto writer = VTX::CreateFlatBuffersWriterFacade(config);
if (!writer) {
VTX_ERROR("Failed to create writer. Schema path: {}", schema_path);
return 1;
}
// Build a FrameAccessor from the writer's loaded schema so MakeSyntheticFrame
// can use the strongly-typed PlayerMutator.
VTX::FrameAccessor accessor;
accessor.InitializeFromCache(writer->GetSchema().GetPropertyCache());
auto processor = std::make_shared<PlayerHealthProcessor>();
writer->SetPostProcessor(processor);
// --- 2. Record synthetic frames. The processor runs on each one.
for (int i = 0; i < frame_count; ++i) {
VTX::Frame frame = MakeSyntheticFrame(i, accessor);
VTX::GameTime::GameTimeRegister game_time;
game_time.game_time = static_cast<float>(i) / 60.0f;
writer->RecordFrame(frame, game_time);
}
// Dump telemetry from the processor before tearing it down.
processor->PrintInfo();
writer->Flush();
writer->Stop();
// Writer's destructor will invoke processor->Clear().
writer.reset();
VTX_INFO("Wrote {} post-processed frames to {}", frame_count, output_path);
// --- 3. Re-open the .vtx and prove the persisted bytes are post-processed.
auto ctx = VTX::OpenReplayFile(output_path);
if (!ctx) {
VTX_ERROR("Re-open failed: {}", ctx.GetError());
return 1;
}
if (!ctx.WaitUntilReady(std::chrono::seconds(2))) {
VTX_ERROR("Reader never became ready");
return 1;
}
VTX_INFO("Reader sees {} frames", ctx.reader->GetTotalFrames());
const VTX::FrameAccessor read_accessor = ctx.reader->CreateAccessor();
const int frames_to_print = std::min(frame_count, 5);
for (int i = 0; i < frames_to_print; ++i) {
const VTX::Frame* frame = ctx.reader->GetFrameSync(i);
if (!frame || frame->GetBuckets().empty())
continue;
const auto& bucket = frame->GetBuckets()[0];
// Use the codegen-generated PlayerView on the reader side too --
// zero hardcoded strings, mirrors the writer-side processor.
VTX::ArenaSchema::ForEachPlayerView(bucket, read_accessor, [&](auto& p) {
VTX_INFO("frame {} player {} -> Health={} IsAlive={}", i, p.GetName(), p.GetHealth(), p.GetIsAlive());
});
}
return 0;
}