Skip to content

Commit 9c8d078

Browse files
Merge branch 'main' into feat/toolbox-remove-data-source
2 parents ca7e0c7 + 0c3aa51 commit 9c8d078

8 files changed

Lines changed: 36 additions & 4 deletions

File tree

docs/builtin_type.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ messages.
150150
| `anchor` | `BufferAnchor` | Keeps `data` alive when it references shared storage. |
151151
| `compressed_depth_min` | `std::optional<float>` | ROS compressed-depth quantization metadata, when present. |
152152
| `compressed_depth_max` | `std::optional<float>` | ROS compressed-depth quantization metadata, when present. |
153+
| `frame_id` | `std::string` | Source coordinate frame (ROS `sensor_msgs/Image` and `foxglove.CompressedImage` both carry it). Lets a consumer match the image to the `CameraInfo` of the same `frame_id` (calibration / native resolution), e.g. to rectify lens distortion so 2D annotations align with the image. Empty when the producer has no frame information. |
153154

154155
Common raw encodings are `rgb8`, `rgba8`, `bgr8`, `bgra8`, `mono8`, and
155156
`mono16`. Common compressed encodings are `jpeg`, `png`, and `qoi`.

pj_base/include/pj_base/builtin/image.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ struct Image {
150150
std::optional<float> compressed_depth_max;
151151

152152
Timestamp timestamp_ns = 0;
153+
154+
/// Source coordinate frame (ROS sensor_msgs/Image and foxglove.CompressedImage
155+
/// both carry it). Lets a consumer match the image to the CameraInfo of the same
156+
/// frame_id (calibration / native resolution), e.g. to rectify lens distortion so
157+
/// 2D annotations align with the image. Empty when the producer has no frame
158+
/// information.
159+
std::string frame_id;
153160
};
154161

155162
} // namespace sdk

pj_base/proto/pj/Image.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,9 @@ message Image {
5757
// ROS compressedDepth quantization maximum; set together with `compressed_depth_min` when `encoding ==
5858
// "compressedDepth"`.
5959
optional float compressed_depth_max = 9;
60+
61+
// Source coordinate frame (ROS sensor_msgs/Image, foxglove.CompressedImage). Lets a consumer match the image to the
62+
// CameraInfo of the same frame_id (calibration / native resolution), e.g. to rectify lens distortion so 2D
63+
// annotations align with the image. Empty when the producer has no frame information.
64+
string frame_id = 10;
6065
}

pj_base/src/builtin/image_codec.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ std::vector<uint8_t> serializeImage(const Image& image) {
5353
if (image.compressed_depth_max.has_value()) {
5454
writer.floatField(9, *image.compressed_depth_max);
5555
}
56+
writer.string(10, image.frame_id);
5657

5758
return out;
5859
}
@@ -139,6 +140,8 @@ Expected<sdk::Image> deserializeImage(const uint8_t* data, size_t size) {
139140
image.compressed_depth_max = v;
140141
return true;
141142
}
143+
case 10:
144+
return tag.type == WireType::kLengthDelimited && r.readString(image.frame_id);
142145
default:
143146
return false;
144147
}

pj_base/tests/image_codec_test.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ TEST(ImageCodecTest, RoundTripRawRGB8) {
3333
in.encoding = "rgb8";
3434
in.row_step = 6; // 2 px * 3 bytes
3535
in.is_bigendian = false;
36+
in.frame_id = "camera_front";
3637
const std::vector<uint8_t> pixels = {255, 0, 0, 0, 255, 0, 0, 0, 255, 128, 128, 128};
3738
in.data = Span<const uint8_t>(pixels.data(), pixels.size());
3839

@@ -45,6 +46,7 @@ TEST(ImageCodecTest, RoundTripRawRGB8) {
4546
EXPECT_EQ(out->encoding, in.encoding);
4647
EXPECT_EQ(out->row_step, in.row_step);
4748
EXPECT_FALSE(out->is_bigendian);
49+
EXPECT_EQ(out->frame_id, "camera_front");
4850
EXPECT_FALSE(out->compressed_depth_min.has_value());
4951
EXPECT_FALSE(out->compressed_depth_max.has_value());
5052
ASSERT_EQ(out->data.size(), pixels.size());
@@ -70,6 +72,7 @@ TEST(ImageCodecTest, RoundTripCompressedDepthWithRange) {
7072
ASSERT_TRUE(out->compressed_depth_max.has_value());
7173
EXPECT_FLOAT_EQ(*out->compressed_depth_min, 0.5f);
7274
EXPECT_FLOAT_EQ(*out->compressed_depth_max, 10.0f);
75+
EXPECT_TRUE(out->frame_id.empty()); // unset frame_id round-trips as empty
7376
}
7477

7578
} // namespace

pj_plugins/CLAUDE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ submodule-internal modules; `pj_base` carries none).
3535
only `MessageParserPluginBase` + `object_ingest_policy.hpp` live here under
3636
`pj_plugins/sdk/`. (The `docs/ARCHITECTURE.md` §2 diagram is stale on this.)
3737
- **Handles keep the DSO mapped.** Every handle holds a `shared_ptr<void>`
38-
library token, so destroying/hot-reloading the loader cannot `dlclose` a live
39-
plugin. Dialog handles add a non-owning `borrowed()` form for source/toolbox
40-
embedded dialogs — those must not outlive the owning handle.
38+
library token (exposed via `libraryOwner()`), so destroying/hot-reloading the
39+
loader cannot `dlclose` a live plugin — and a lazy ObjectStore payload anchor,
40+
whose `release` fn is plugin code, can capture that token to stay safe past the
41+
handle's own lifetime. Dialog handles add a non-owning `borrowed()` form for
42+
source/toolbox embedded dialogs — those must not outlive the owning handle.
4143

4244
## Read deeper
4345
| For | Read |

pj_plugins/docs/ARCHITECTURE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,10 @@ Each family has a move-only RAII handle:
359359
- Destructor calls `vt->destroy(ctx)`.
360360
- Handles created by a loader retain a shared DSO owner; destroying or
361361
hot-reloading the loader/catalog entry cannot `dlclose` the plugin while
362-
live handles still call its vtable.
362+
live handles still call its vtable. `DataSourceHandle` exposes this token via
363+
`libraryOwner()` so it can be captured anywhere plugin code may outlive the
364+
handle — e.g. a lazy `ObjectStore` payload anchor whose `release` fn lives in
365+
the plugin `.so` — keeping the DSO mapped until that captor is gone too.
363366
- No copy, move-only semantics.
364367
- Methods delegate to vtable functions with the stored context pointer.
365368

pj_plugins/include/pj_plugins/host/data_source_handle.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ class DataSourceHandle {
7373
return vt_ != nullptr && ctx_ != nullptr;
7474
}
7575

76+
// The shared library token that keeps this plugin's DSO mapped. Capture a copy
77+
// anywhere a callback or payload anchor PRODUCED BY the plugin may outlive this
78+
// handle (e.g. lazy ObjectStore payloads): the .so must not be dlclosed while
79+
// plugin code (a payload anchor's release fn) can still run.
80+
[[nodiscard]] std::shared_ptr<void> libraryOwner() const {
81+
return library_owner_;
82+
}
83+
7684
[[nodiscard]] std::string manifest() const {
7785
return vt_->manifest_json != nullptr ? std::string(vt_->manifest_json) : std::string();
7886
}

0 commit comments

Comments
 (0)