Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
All notable changes to `plotjuggler_sdk` are recorded here. Versioning policy is in
[`CLAUDE.md`](./CLAUDE.md) → "Release Versioning".

## [0.35.0]

Host contract: unchanged (no floor impact) — a client-side codec helper, no new
host surface.

- Add `deserializePointCloudView(data, size, anchor)`: decodes a canonical
PJ.PointCloud without copying its packed point bytes (`data` aliases the input,
`anchor` keeps it alive), like `deserializeVideoFrameView`. A null anchor copies.

## [0.34.1]

- Fix a data race in `JobControl::armWatchdog`: the self-join guard read the
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.34.1
0.35.0
9 changes: 9 additions & 0 deletions pj_base/include/pj_base/builtin/point_cloud_codec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <string_view>
#include <vector>

#include "pj_base/buffer_anchor.hpp"
#include "pj_base/builtin/point_cloud.hpp"
#include "pj_base/expected.hpp"

Expand All @@ -22,4 +23,12 @@ inline constexpr std::string_view kSchemaPointCloud = "PJ.PointCloud";
/// its packed point bytes via `anchor`.
[[nodiscard]] Expected<sdk::PointCloud> deserializePointCloud(const uint8_t* data, size_t size);

/// Decodes canonical PJ.PointCloud wire bytes without copying the packed point
/// bytes: the returned cloud's `data` ALIASES the input buffer and its `anchor`
/// is the supplied `anchor`, which must keep that buffer alive and unchanged for
/// as long as the cloud is used. A null `anchor` copies, like
/// deserializePointCloud().
[[nodiscard]] Expected<sdk::PointCloud> deserializePointCloudView(
const uint8_t* data, size_t size, sdk::BufferAnchor anchor);

} // namespace PJ
24 changes: 21 additions & 3 deletions pj_base/src/builtin/point_cloud_codec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ using sdk::PointField;

// ---------- PointCloud payload bytes ----------

bool readBytesIntoCloud(Reader& reader, PointCloud& out) {
// Points `out.data` into the input when `input_anchor` owns it, else copies.
bool readBytesIntoCloud(Reader& reader, PointCloud& out, const sdk::BufferAnchor& input_anchor) {
const uint8_t* data = nullptr;
size_t size = 0;
if (!reader.readBytes(data, size)) {
return false;
}
if (input_anchor) {
out.data = Span<const uint8_t>(data, size);
out.anchor = input_anchor;
return true;
}
auto owned = std::make_shared<std::vector<uint8_t>>(data, data + size);
out.data = Span<const uint8_t>(owned->data(), owned->size());
out.anchor = owned;
Expand Down Expand Up @@ -62,7 +68,9 @@ std::vector<uint8_t> serializePointCloud(const PointCloud& cloud) {
return out;
}

Expected<sdk::PointCloud> deserializePointCloud(const uint8_t* data, size_t size) {
namespace {

Expected<sdk::PointCloud> decodePointCloud(const uint8_t* data, size_t size, const sdk::BufferAnchor& input_anchor) {
if (data == nullptr || size == 0) {
return unexpected(std::string("PointCloud wire: empty buffer"));
}
Expand Down Expand Up @@ -143,7 +151,7 @@ Expected<sdk::PointCloud> deserializePointCloud(const uint8_t* data, size_t size
case 8:
return tag.type == WireType::kLengthDelimited && readPointFieldIntoVector(r, cloud.fields);
case 9:
return tag.type == WireType::kLengthDelimited && readBytesIntoCloud(r, cloud);
return tag.type == WireType::kLengthDelimited && readBytesIntoCloud(r, cloud, input_anchor);
case 10:
return tag.type == WireType::kLengthDelimited && r.readString(cloud.frame_id);
default:
Expand All @@ -158,4 +166,14 @@ Expected<sdk::PointCloud> deserializePointCloud(const uint8_t* data, size_t size
return cloud;
}

} // namespace

Expected<sdk::PointCloud> deserializePointCloud(const uint8_t* data, size_t size) {
return decodePointCloud(data, size, nullptr);
}

Expected<sdk::PointCloud> deserializePointCloudView(const uint8_t* data, size_t size, sdk::BufferAnchor anchor) {
return decodePointCloud(data, size, anchor);
}

} // namespace PJ
55 changes: 55 additions & 0 deletions pj_base/tests/point_cloud_codec_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <cstdint>
#include <cstring>
#include <memory>
#include <vector>

#include "protobuf_wire_test_helpers.hpp"
Expand Down Expand Up @@ -79,5 +80,59 @@ TEST(PointCloudCodecTest, FrameIdAbsentRoundTrips) {
EXPECT_TRUE(out->frame_id.empty());
}

PointCloud smallCloud(std::vector<uint8_t>& payload) {
PointCloud in;
in.width = 2;
in.point_step = 12;
in.row_step = 24;
in.frame_id = "lidar";
in.fields = {
{.name = "x", .offset = 0, .datatype = PointField::Datatype::kFloat32, .count = 1},
{.name = "y", .offset = 4, .datatype = PointField::Datatype::kFloat32, .count = 1},
{.name = "z", .offset = 8, .datatype = PointField::Datatype::kFloat32, .count = 1},
};
payload.assign(24, 0);
for (size_t i = 0; i < payload.size(); ++i) {
payload[i] = static_cast<uint8_t>(i * 7);
}
in.data = Span<const uint8_t>(payload.data(), payload.size());
return in;
}

TEST(PointCloudCodecTest, ViewAliasesAnchoredBytes) {
std::vector<uint8_t> points;
const PointCloud in = smallCloud(points);
const auto wire = std::make_shared<const std::vector<uint8_t>>(serializePointCloud(in));
auto out = deserializePointCloudView(wire->data(), wire->size(), wire);
ASSERT_TRUE(out.has_value());
EXPECT_EQ(out->width, in.width);
EXPECT_EQ(out->frame_id, in.frame_id);
ASSERT_EQ(out->fields.size(), 3u);
ASSERT_EQ(out->data.size(), points.size());
EXPECT_EQ(std::memcmp(out->data.data(), points.data(), points.size()), 0);
// No copy: the bytes live inside the wire buffer, which the result keeps alive.
EXPECT_GE(out->data.data(), wire->data());
EXPECT_LE(out->data.data() + out->data.size(), wire->data() + wire->size());
EXPECT_EQ(out->anchor, wire);
}

TEST(PointCloudCodecTest, ViewWithoutAnchorCopies) {
std::vector<uint8_t> points;
const PointCloud in = smallCloud(points);
const std::vector<uint8_t> wire = serializePointCloud(in);
auto out = deserializePointCloudView(wire.data(), wire.size(), nullptr);
ASSERT_TRUE(out.has_value());
ASSERT_EQ(out->data.size(), points.size());
EXPECT_EQ(std::memcmp(out->data.data(), points.data(), points.size()), 0);
EXPECT_TRUE(out->data.data() < wire.data() || out->data.data() >= wire.data() + wire.size());
EXPECT_NE(out->anchor, nullptr);
}

TEST(PointCloudCodecTest, ViewRejectsWhatTheCopyingDecoderRejects) {
const auto garbage = std::make_shared<const std::vector<uint8_t>>(std::vector<uint8_t>{0xFF, 0xFF, 0xFF});
EXPECT_FALSE(deserializePointCloudView(garbage->data(), garbage->size(), garbage).has_value());
EXPECT_FALSE(deserializePointCloudView(nullptr, 0, nullptr).has_value());
}

} // namespace
} // namespace PJ
Loading