CARLA
Loading...
Searching...
No Matches
test_multigpu_frame_data.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB).
2// This work is licensed under the terms of the MIT license.
3// For a copy, see <https://opensource.org/licenses/MIT>.
4
5#include "test.h"
6
7#include <carla/Buffer.h>
12
13#include <chrono>
14#include <cstring>
15#include <future>
16#include <memory>
17#include <optional>
18#include <sstream>
19#include <stdexcept>
20#include <string>
21#include <thread>
22#include <vector>
23
24namespace {
25
33
34 /// A packet this test treats as known: one uint32 value.
35 constexpr uint8_t kValuePacketId = 2u;
36
37 /// Runs @a task on a detached thread so that a reader that spins fails the
38 /// test instead of hanging the whole suite.
39 template <typename F>
40 auto RunWithWatchdog(F task) -> std::optional<decltype(task())> {
41 using Result = decltype(task());
42 auto done = std::make_shared<std::promise<Result>>();
43 auto future = done->get_future();
44 std::thread([done, task = std::move(task)]() mutable {
45 try {
46 done->set_value(task());
47 } catch (...) {
48 done->set_exception(std::current_exception());
49 }
50 }).detach();
51 if (future.wait_for(std::chrono::seconds(5)) != std::future_status::ready) {
52 return std::nullopt;
53 }
54 return future.get();
55 }
56
57 template <typename T>
58 void Put(std::ostream &out, const T &value) {
59 out.write(reinterpret_cast<const char *>(&value), sizeof(T));
60 }
61
62 void PutValuePacket(std::ostream &out, uint32_t value) {
63 WriteFrameDataHeader(out, kValuePacketId, sizeof(value));
64 Put(out, value);
65 }
66
67 void PutFrameCounter(std::ostream &out, uint64_t frame) {
68 WriteFrameDataHeader(out, kFrameCounterPacketId, sizeof(frame));
69 Put(out, frame);
70 }
71
72 struct Parsed {
73 FrameDataReadResult result;
74 std::vector<uint32_t> values;
75 std::optional<FrameTime> time;
76 };
77
78 /// Parses @a bytes the way FFrameData does: value packets and FrameTime are
79 /// known, every other id is not.
80 Parsed Parse(std::string bytes) {
81 auto parsed = RunWithWatchdog([bytes = std::move(bytes)]() {
82 Parsed out;
83 out.result = ReadFrameData(
84 reinterpret_cast<const uint8_t *>(bytes.data()), bytes.size(),
85 [&](uint8_t id, std::istream &in, uint32_t size) {
86 switch (id) {
87 case kValuePacketId: {
88 uint32_t value = 0u;
89 in.read(reinterpret_cast<char *>(&value), sizeof(value));
90 if (!in.fail()) {
91 out.values.push_back(value);
92 }
93 return true;
94 }
95 case FrameTime::kPacketId: {
96 FrameTime time;
97 if (time.Read(in, size)) {
98 out.time = time;
99 }
100 return true;
101 }
102 default:
103 return false;
104 }
105 });
106 return out;
107 });
108 EXPECT_TRUE(parsed.has_value()) << "the frame reader did not terminate";
109 return parsed.value_or(Parsed{});
110 }
111
112 FrameTime SampleFrameTime() {
113 FrameTime time;
114 time.frame = 223316u;
115 time.delta_seconds = 0.05f;
116 time.elapsed_game_time = 11.39318;
117 time.visual_game_time = 11.39318 + 0.5;
118 time.world_time_seconds = 100.125;
119 time.world_unpaused_time_seconds = 101.25;
120 time.world_real_time_seconds = 102.5;
121 time.world_audio_time_seconds = 103.75;
122 time.synchronous_mode = true;
123 time.no_rendering_mode = true;
124 time.fixed_delta_seconds = 0.05;
125 time.substepping = false;
126 time.max_substep_delta_time = 0.002;
127 time.max_substeps = 16;
128 time.max_culling_distance = 12.5f;
129 time.deterministic_ragdolls = false;
130 time.tile_streaming_distance = 123456.0f;
131 time.actor_active_distance = 65432.0f;
132 time.spectator_as_ego = false;
133 time.adjust_signs_height_to_ground = true;
134 time.time_dilation = 0.5f;
135 time.full_resync = true;
136 return time;
137 }
138
139} // namespace
140
141TEST(MultiGpuFrameData, complete_frame_is_read_in_order) {
142 std::ostringstream out;
143 PutValuePacket(out, 7u);
144 PutValuePacket(out, 8u);
145 PutFrameCounter(out, 42u);
146 const Parsed parsed = Parse(out.str());
147 EXPECT_TRUE(parsed.result.IsComplete());
148 ASSERT_TRUE(parsed.result.frame.has_value());
149 EXPECT_EQ(*parsed.result.frame, 42u);
150 EXPECT_EQ(parsed.values, (std::vector<uint32_t>{7u, 8u}));
151 EXPECT_EQ(parsed.result.skipped, 0u);
152 EXPECT_EQ(parsed.result.grown, 0u);
153}
154
155TEST(MultiGpuFrameData, unknown_packet_mid_stream_is_skipped) {
156 std::ostringstream out;
157 PutValuePacket(out, 1u);
158 WriteFrameDataHeader(out, 200u, 3u);
159 out.write("abc", 3);
160 PutValuePacket(out, 2u);
161 PutFrameCounter(out, 9u);
162 const Parsed parsed = Parse(out.str());
163 EXPECT_TRUE(parsed.result.IsComplete());
164 EXPECT_EQ(parsed.values, (std::vector<uint32_t>{1u, 2u}));
165 EXPECT_EQ(parsed.result.skipped, 1u);
166 EXPECT_EQ(parsed.result.first_skipped_id, 200u);
167}
168
169TEST(MultiGpuFrameData, unknown_packet_as_the_last_packet_terminates) {
170 std::ostringstream out;
171 PutFrameCounter(out, 3u);
172 WriteFrameDataHeader(out, 201u, 0u);
173 const Parsed parsed = Parse(out.str());
174 EXPECT_TRUE(parsed.result.IsComplete());
175 EXPECT_EQ(parsed.result.skipped, 1u);
176}
177
178TEST(MultiGpuFrameData, frame_without_frame_counter_is_incomplete) {
179 std::ostringstream out;
180 PutValuePacket(out, 5u);
181 const Parsed parsed = Parse(out.str());
182 EXPECT_EQ(parsed.result.status, FrameDataStatus::MissingFrameCounter);
183 EXPECT_FALSE(parsed.result.IsComplete());
184 EXPECT_EQ(Parse(std::string{}).result.status, FrameDataStatus::MissingFrameCounter);
185}
186
187TEST(MultiGpuFrameData, truncated_header_stops_the_read) {
188 std::ostringstream out;
189 PutFrameCounter(out, 1u);
190 out.write("\x02\x04", 2);
191 const Parsed parsed = Parse(out.str());
192 EXPECT_EQ(parsed.result.status, FrameDataStatus::TruncatedHeader);
193}
194
195TEST(MultiGpuFrameData, truncated_payload_stops_the_read) {
196 std::ostringstream out;
197 WriteFrameDataHeader(out, kValuePacketId, 4000u);
198 Put(out, uint32_t{1u});
199 const Parsed parsed = Parse(out.str());
200 EXPECT_EQ(parsed.result.status, FrameDataStatus::TruncatedPayload);
201 EXPECT_EQ(parsed.result.packet_size, 4000u);
202 EXPECT_TRUE(parsed.values.empty());
203}
204
205TEST(MultiGpuFrameData, short_known_packet_fails_inside_its_own_packet) {
206 std::ostringstream out;
207 WriteFrameDataHeader(out, kValuePacketId, 2u);
208 out.write("\x01\x02", 2);
209 PutValuePacket(out, 77u);
210 PutFrameCounter(out, 1u);
211 const Parsed parsed = Parse(out.str());
212 EXPECT_EQ(parsed.result.status, FrameDataStatus::ShortPacket);
213 EXPECT_EQ(parsed.result.packet_id, kValuePacketId);
214 EXPECT_EQ(parsed.result.offset, 0u);
215 // The reader could not consume the next packet's bytes.
216 EXPECT_TRUE(parsed.values.empty());
217}
218
219TEST(MultiGpuFrameData, longer_known_packet_is_counted_and_skipped) {
220 std::ostringstream out;
221 WriteFrameDataHeader(out, kValuePacketId, 8u);
222 Put(out, uint32_t{11u});
223 Put(out, uint32_t{0xDEADBEEFu});
224 PutValuePacket(out, 12u);
225 PutFrameCounter(out, 1u);
226 const Parsed parsed = Parse(out.str());
227 EXPECT_TRUE(parsed.result.IsComplete());
228 EXPECT_EQ(parsed.result.grown, 1u);
229 EXPECT_EQ(parsed.values, (std::vector<uint32_t>{11u, 12u}));
230}
231
232TEST(MultiGpuFrameData, short_frame_counter_is_a_short_packet) {
233 std::ostringstream out;
234 WriteFrameDataHeader(out, kFrameCounterPacketId, 4u);
235 Put(out, uint32_t{1u});
236 EXPECT_EQ(Parse(out.str()).result.status, FrameDataStatus::ShortPacket);
237}
238
239TEST(MultiGpuFrameData, reader_exception_is_reported) {
240 std::ostringstream out;
241 PutValuePacket(out, 1u);
242 PutFrameCounter(out, 1u);
243 const std::string bytes = out.str();
244 const auto result = RunWithWatchdog([bytes]() {
245 return ReadFrameData(
246 reinterpret_cast<const uint8_t *>(bytes.data()), bytes.size(),
247 [](uint8_t, std::istream &, uint32_t) -> bool { throw std::length_error("bad count"); });
248 });
249 ASSERT_TRUE(result.has_value());
250 EXPECT_EQ(result->status, FrameDataStatus::ReaderError);
251}
252
253TEST(MultiGpuFrameData, nested_counts_read_off_the_wire_terminate) {
254 // A recorder-style reader: an outer count of 65535 records, each with an
255 // inner count, over a 3-byte payload. Loops of the form (i < Total) && In
256 // must end as soon as the payload runs out.
257 std::ostringstream out;
258 WriteFrameDataHeader(out, kValuePacketId, 3u);
259 out.write("\xFF\xFF\x07", 3);
260 PutFrameCounter(out, 1u);
261 const std::string bytes = out.str();
262 const auto result = RunWithWatchdog([bytes]() {
263 return ReadFrameData(
264 reinterpret_cast<const uint8_t *>(bytes.data()), bytes.size(),
265 [](uint8_t, std::istream &in, uint32_t) {
266 uint16_t total = 0u;
267 in.read(reinterpret_cast<char *>(&total), sizeof(total));
268 for (uint16_t i = 0u; (i < total) && in; ++i) {
269 uint32_t inner = 0u;
270 in.read(reinterpret_cast<char *>(&inner), sizeof(inner));
271 for (uint32_t j = 0u; (j < inner) && in; ++j) {
272 uint64_t value = 0u;
273 in.read(reinterpret_cast<char *>(&value), sizeof(value));
274 }
275 }
276 return true;
277 });
278 });
279 ASSERT_TRUE(result.has_value()) << "the nested reader did not terminate";
280 EXPECT_EQ(result->status, FrameDataStatus::ShortPacket);
281}
282
283TEST(MultiGpuFrameData, bounded_reader_rejects_lists_larger_than_the_packet) {
284 std::ostringstream out;
285 Put(out, uint32_t{1000u});
286 Put(out, uint16_t{8u});
287 Put(out, uint64_t{1u});
288 const std::string bytes = out.str();
289 std::istringstream in(bytes);
290 BoundedReader reader(in, static_cast<uint32_t>(bytes.size()));
291 uint32_t count = 0u;
292 uint16_t record_size = 0u;
293 EXPECT_FALSE(reader.ReadFixedListHeader(8u, count, record_size));
294 EXPECT_TRUE(reader.Failed());
295 EXPECT_TRUE(in.fail());
296}
297
298TEST(MultiGpuFrameData, bounded_reader_skips_what_a_record_grew) {
299 std::ostringstream out;
300 Put(out, uint32_t{2u});
301 Put(out, uint16_t{6u}); // records of 6 bytes; this build reads 4
302 Put(out, uint32_t{10u});
303 Put(out, uint16_t{0xAAAAu});
304 Put(out, uint32_t{20u});
305 Put(out, uint16_t{0xBBBBu});
306 const std::string bytes = out.str();
307 std::istringstream in(bytes);
308 BoundedReader reader(in, static_cast<uint32_t>(bytes.size()));
309 uint32_t count = 0u;
310 uint16_t record_size = 0u;
311 ASSERT_TRUE(reader.ReadFixedListHeader(4u, count, record_size));
312 std::vector<uint32_t> values;
313 for (uint32_t i = 0u; i < count; ++i) {
314 ASSERT_TRUE(reader.ReadRecord(record_size, [&](BoundedReader &record) {
315 uint32_t value = 0u;
316 if (!record.Read(value)) {
317 return false;
318 }
319 values.push_back(value);
320 return true;
321 }));
322 }
323 EXPECT_EQ(values, (std::vector<uint32_t>{10u, 20u}));
324 EXPECT_EQ(reader.Remaining(), 0u);
325 EXPECT_FALSE(reader.Failed());
326}
327
328TEST(MultiGpuFrameData, bounded_reader_skips_unknown_fields) {
329 std::ostringstream out;
330 carla::multigpu::WriteField(out, 9u, std::string("unknown"));
331 std::ostringstream known;
332 Put(known, uint32_t{1234u});
333 carla::multigpu::WriteField(out, 1u, known.str());
334 const std::string bytes = out.str();
335 std::istringstream in(bytes);
336 BoundedReader reader(in, static_cast<uint32_t>(bytes.size()));
337 std::optional<uint32_t> value;
338 EXPECT_TRUE(reader.ReadFields([&](uint8_t kind, BoundedReader &field) {
339 if (kind == 1u) {
340 uint32_t v = 0u;
341 if (!field.Read(v)) {
342 return false;
343 }
344 value = v;
345 }
346 return true;
347 }));
348 ASSERT_TRUE(value.has_value());
349 EXPECT_EQ(*value, 1234u);
350}
351
352TEST(MultiGpuFrameData, bounded_reader_cannot_read_past_its_field) {
353 std::ostringstream out;
354 carla::multigpu::WriteField(out, 1u, std::string("ab"));
355 Put(out, uint32_t{99u});
356 const std::string bytes = out.str();
357 std::istringstream in(bytes);
358 BoundedReader reader(in, static_cast<uint32_t>(bytes.size()));
359 EXPECT_FALSE(reader.ReadFields([](uint8_t, BoundedReader &field) {
360 uint32_t v = 0u;
361 return field.Read(v); // the field holds 2 bytes only
362 }));
363}
364
365TEST(MultiGpuFrameTime, round_trips_every_field) {
366 const FrameTime sent = SampleFrameTime();
367 std::ostringstream out;
368 sent.Write(out);
369 PutFrameCounter(out, sent.frame);
370 const Parsed parsed = Parse(out.str());
371 ASSERT_TRUE(parsed.result.IsComplete());
372 ASSERT_TRUE(parsed.time.has_value());
373 const FrameTime &got = *parsed.time;
374 EXPECT_EQ(got.protocol_version, carla::multigpu::kMultiGpuProtocolVersion);
375 EXPECT_EQ(got.frame, sent.frame);
376 EXPECT_EQ(got.delta_seconds, sent.delta_seconds);
377 EXPECT_EQ(got.elapsed_game_time, sent.elapsed_game_time);
378 EXPECT_EQ(got.visual_game_time, sent.visual_game_time);
379 EXPECT_EQ(got.world_time_seconds, sent.world_time_seconds);
380 EXPECT_EQ(got.world_unpaused_time_seconds, sent.world_unpaused_time_seconds);
381 EXPECT_EQ(got.world_real_time_seconds, sent.world_real_time_seconds);
382 EXPECT_EQ(got.world_audio_time_seconds, sent.world_audio_time_seconds);
383 EXPECT_EQ(got.synchronous_mode, sent.synchronous_mode);
384 EXPECT_EQ(got.no_rendering_mode, sent.no_rendering_mode);
385 EXPECT_EQ(got.fixed_delta_seconds, sent.fixed_delta_seconds);
386 EXPECT_EQ(got.substepping, sent.substepping);
387 EXPECT_EQ(got.max_substep_delta_time, sent.max_substep_delta_time);
388 EXPECT_EQ(got.max_substeps, sent.max_substeps);
389 EXPECT_EQ(got.max_culling_distance, sent.max_culling_distance);
390 EXPECT_EQ(got.deterministic_ragdolls, sent.deterministic_ragdolls);
391 EXPECT_EQ(got.tile_streaming_distance, sent.tile_streaming_distance);
392 EXPECT_EQ(got.actor_active_distance, sent.actor_active_distance);
393 EXPECT_EQ(got.spectator_as_ego, sent.spectator_as_ego);
394 EXPECT_EQ(got.adjust_signs_height_to_ground, sent.adjust_signs_height_to_ground);
395 EXPECT_EQ(got.time_dilation, sent.time_dilation);
396 EXPECT_EQ(got.full_resync, sent.full_resync);
397}
398
399TEST(MultiGpuFrameTime, unset_fixed_delta_round_trips) {
400 FrameTime sent = SampleFrameTime();
401 sent.fixed_delta_seconds.reset();
402 sent.full_resync = false;
403 std::ostringstream out;
404 sent.Write(out);
405 PutFrameCounter(out, sent.frame);
406 const Parsed parsed = Parse(out.str());
407 ASSERT_TRUE(parsed.time.has_value());
408 EXPECT_FALSE(parsed.time->fixed_delta_seconds.has_value());
409 EXPECT_FALSE(parsed.time->full_resync);
410}
411
412TEST(MultiGpuFrameTime, v1_layout_is_pinned) {
413 const FrameTime sent = SampleFrameTime();
414 std::ostringstream out;
415 sent.Write(out);
416 const std::string bytes = out.str();
417 ASSERT_EQ(bytes.size(), carla::multigpu::kFrameDataHeaderSize + FrameTime::kPayloadSizeV1);
418 ASSERT_EQ(bytes.size(), 103u);
419 const char *payload = bytes.data() + carla::multigpu::kFrameDataHeaderSize;
420 EXPECT_EQ(static_cast<uint8_t>(bytes[0]), FrameTime::kPacketId);
421 uint64_t frame = 0u;
422 std::memcpy(&frame, payload + 2, sizeof(frame));
423 EXPECT_EQ(frame, sent.frame);
424 float delta = 0.0f;
425 std::memcpy(&delta, payload + 10, sizeof(delta));
426 EXPECT_EQ(delta, sent.delta_seconds);
427 double world_time = 0.0;
428 std::memcpy(&world_time, payload + 30, sizeof(world_time));
429 EXPECT_EQ(world_time, sent.world_time_seconds);
430 double fixed_delta = 0.0;
431 std::memcpy(&fixed_delta, payload + 62, sizeof(fixed_delta));
432 EXPECT_EQ(fixed_delta, *sent.fixed_delta_seconds);
433 float dilation = 0.0f;
434 std::memcpy(&dilation, payload + 94, sizeof(dilation));
435 EXPECT_EQ(dilation, sent.time_dilation);
436}
437
438TEST(MultiGpuFrameTime, shorter_than_v1_is_rejected) {
439 std::ostringstream out;
440 WriteFrameDataHeader(out, FrameTime::kPacketId, 20u);
441 out.write(std::string(20u, '\0').data(), 20);
442 PutFrameCounter(out, 1u);
443 const Parsed parsed = Parse(out.str());
444 EXPECT_EQ(parsed.result.status, FrameDataStatus::ShortPacket);
445 EXPECT_FALSE(parsed.time.has_value());
446}
447
448TEST(MultiGpuFrameTime, longer_is_reported_as_grown) {
449 const FrameTime sent = SampleFrameTime();
450 std::ostringstream v1;
451 sent.Write(v1);
452 std::string bytes = v1.str();
453 // Rewrite it as a newer build would: the same fields plus 6 more bytes.
454 const uint32_t grown_size = FrameTime::kPayloadSizeV1 + 6u;
455 std::memcpy(&bytes[1], &grown_size, sizeof(grown_size));
456 bytes.append(6u, '\x5A');
457 std::ostringstream out;
458 out << bytes;
459 PutFrameCounter(out, sent.frame);
460 const Parsed parsed = Parse(out.str());
461 EXPECT_TRUE(parsed.result.IsComplete());
462 EXPECT_EQ(parsed.result.grown, 1u);
463 ASSERT_TRUE(parsed.time.has_value());
464 EXPECT_EQ(parsed.time->time_dilation, sent.time_dilation);
465}
466
467TEST(MultiGpuFrameTime, legacy_frame_without_frame_time_is_complete) {
468 std::ostringstream out;
469 PutValuePacket(out, 1u);
470 PutFrameCounter(out, 5u);
471 const Parsed parsed = Parse(out.str());
472 EXPECT_TRUE(parsed.result.IsComplete());
473 EXPECT_FALSE(parsed.time.has_value());
474}
475
476namespace {
477
478 struct Delivered {
479 std::vector<carla::multigpu::MultiGPUCommand> ids;
480 std::vector<std::string> payloads;
481 };
482
483 /// Feeds @a message to SecondaryCommands::process_command and records what
484 /// reaches the engine's callback.
485 Delivered Process(const std::string &message) {
486 Delivered delivered;
489 delivered.ids.push_back(id);
490 delivered.payloads.emplace_back(reinterpret_cast<const char *>(data.data()), data.size());
491 });
493 reinterpret_cast<const unsigned char *>(message.data()), message.size()));
494 return delivered;
495 }
496
497 std::string Command(carla::multigpu::MultiGPUCommand id, uint32_t declared, const std::string &payload) {
498 carla::multigpu::CommandHeader header{id, declared};
499 std::string message(reinterpret_cast<const char *>(&header), sizeof(header));
500 return message + payload;
501 }
502
503} // namespace
504
505TEST(MultiGpuCommands, process_command_delivers_exactly_the_declared_payload) {
506 const Delivered delivered = Process(Command(carla::multigpu::SEND_FRAME, 3u, "abc"));
507 ASSERT_EQ(delivered.ids.size(), 1u);
508 EXPECT_EQ(delivered.ids[0], carla::multigpu::SEND_FRAME);
509 EXPECT_EQ(delivered.payloads[0], "abc");
510}
511
512TEST(MultiGpuCommands, process_command_drops_a_message_shorter_than_its_header) {
513 EXPECT_TRUE(Process(std::string("\x01\x00\x00", 3)).ids.empty());
514}
515
516TEST(MultiGpuCommands, process_command_drops_a_payload_larger_than_the_message) {
517 EXPECT_TRUE(Process(Command(carla::multigpu::LOAD_MAP, 4096u, "Town10HD")).ids.empty());
518}
519
520TEST(MultiGpuCommands, process_command_ignores_trailing_bytes) {
521 const Delivered delivered = Process(Command(carla::multigpu::GET_TOKEN, 2u, "xyz"));
522 ASSERT_EQ(delivered.ids.size(), 1u);
523 EXPECT_EQ(delivered.payloads[0], "xy");
524}
A piece of raw data.
const value_type * data() const noexcept
Direct access to the allocated memory or nullptr if no memory is allocated.
size_type size() const noexcept
Reads the fields of one packet (or one record inside it) without ever consuming more than its declare...
void set_callback(callback_type callback)
FrameDataReadResult ReadFrameData(const uint8_t *data, std::size_t size, ReadPacket &&read_packet)
Walks the packets of one frame.
constexpr std::size_t kFrameDataHeaderSize
Frame data (the SEND_FRAME payload) is a sequence of packets [uint8 id][uint32 payload size][payload]...
void WriteField(std::ostream &out, uint8_t kind, const std::string &value)
void WriteFrameDataHeader(std::ostream &out, uint8_t id, uint32_t payload_size)
constexpr uint8_t kFrameCounterPacketId
Id of the frame counter packet, the last packet of every complete frame.
constexpr uint8_t kMultiGpuProtocolVersion
Version of the whole Multi-GPU wire format, carried in every frame.
Frame data packet 25: the primary's clocks and episode settings for one frame.
Definition frameTime.h:20
TEST(MultiGpuFrameData, complete_frame_is_read_in_order)