Skip to content

Conversation

fmz
Copy link
Owner

@fmz fmz commented Oct 2, 2025

No description provided.

@fmz fmz requested a review from Copilot October 2, 2025 19:02
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces multi-camera stream support per Spot robot. It refactors the camera handling and vision pipeline to operate on per-stream identifiers, updates the C API and Unity interop to propagate stream IDs, and adapts the integration test accordingly.

  • Add SpotCamStream abstraction and refactor VisionPipeline to operate per camera stream
  • Extend C/Unity APIs to take cam_stream_id and maintain per-robot/per-stream output buffers
  • Update the integration test to create camera streams, launch pipelines per stream, and read image sets per stream

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
tests/integ-test/integ-test.cpp Refactors connection/stream lifecycle, launches vision pipeline per stream, and reads images per cam_stream_id
src/vision-pipeline.cpp Switches from SpotConnection to SpotCamStream and validates tensor shapes against stream-provided shapes
src/unity-cuda-interop.cpp Adds per-stream texture registration and upload logic; updates function signatures to include cam_stream_id
src/spot-observer.cpp Moves pipeline management into SpotConnection; adds per-stream APIs; updates image retrieval and pipeline control
src/spot-connection.cpp Introduces SpotCamStream, creates/destroys streams and pipelines per stream; updates streaming logic and shape handling
src/include/vision-pipeline.h Updates VisionPipeline to take SpotCamStream instead of SpotConnection
src/include/utils.h Adds TensorShape with equality and string formatting support
src/include/unity-cuda-interop.h Updates Unity interop function signatures to include cam_stream_id
src/include/spot-connection.h Splits SpotConnection and SpotCamStream, adds per-stream lifecycle and pipeline APIs
src/include/model.h Removes duplicate TensorShape (now in utils.h)
include/spot-observer.h Updates extern "C" API to include cam_stream_id and wraps in C++ guards

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

if (spot_ids[i] < 0) continue;
bool ret = SOb_LaunchVisionPipeline(spot_ids[i], model);
// Launch vision pipeline only on the first camera stream
bool ret = SOb_LaunchVisionPipeline(spot_ids[i], cam_stream_ids[i][0], model);
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map cam_stream_ids is keyed by spot_id, but here it's indexed by i (robot index). This can access the wrong vector or crash. Use cam_stream_ids[spot_ids[i]][0] instead.

Suggested change
bool ret = SOb_LaunchVisionPipeline(spot_ids[i], cam_stream_ids[i][0], model);
bool ret = SOb_LaunchVisionPipeline(spot_ids[i], cam_stream_ids[spot_ids[i]][0], model);

Copilot uses AI. Check for mistakes.


for (uint32_t cam_bitmask : cam_bitmasks) {
uint32_t num_bits_set = __num_set_bits(cam_bitmask);
num_images_requested_per_stream.push_back(__num_set_bits(num_bits_set));
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

num_bits_set is already the count of set bits. Calling __num_set_bits(num_bits_set) computes popcount on a count, producing incorrect values. Replace push_back(__num_set_bits(num_bits_set)) with push_back(num_bits_set).

Suggested change
num_images_requested_per_stream.push_back(__num_set_bits(num_bits_set));
num_images_requested_per_stream.push_back(num_bits_set);

Copilot uses AI. Check for mistakes.

}
if (n_images_requested <= 0 || n_images_requested > robot.getCurrentNumCams()) {

if (n_images_requested <= 0 || n_images_requested > stream->getCurrentCamMask()) {
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bounds check uses the bitmask (getCurrentCamMask) instead of the number of cameras. Compare against stream->getCurrentNumCams() to validate n_images_requested.

Suggested change
if (n_images_requested <= 0 || n_images_requested > stream->getCurrentCamMask()) {
if (n_images_requested <= 0 || n_images_requested > stream->getCurrentNumCams()) {

Copilot uses AI. Check for mistakes.

return false;
}

VisionPipeline* pipeline = robot.getVisionPipeline(cam_stream_id);
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null dereference: if getVisionPipeline returns nullptr, calling pipeline->isRunning() will crash. Add a nullptr check before isRunning().

Suggested change
VisionPipeline* pipeline = robot.getVisionPipeline(cam_stream_id);
VisionPipeline* pipeline = robot.getVisionPipeline(cam_stream_id);
if (!pipeline) {
LogMessage("SOb_GetNextVisionPipelineImageSet: Vision pipeline not found for robot ID {} and cam_stream_id {}", robot_id, cam_stream_id);
return false;
}

Copilot uses AI. Check for mistakes.

if (it == SOb::__robot_connections.end()) {
SOb::LogMessage("SOb_ReadCameraFeeds: Robot ID {} not found", robot_id);
SOb::LogMessage("SOb_CreateCameraStream: Robot ID {} not found", robot_id);
return false; // Robot ID not found
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function returns int32_t but returns false (0) on error, which can be misinterpreted as a valid stream id. Return -1 for the 'not found' case to match other error paths.

Suggested change
return false; // Robot ID not found
return -1; // Robot ID not found

Copilot uses AI. Check for mistakes.

Comment on lines 707 to 709
if (!it->second->streamCameras(cam_mask)) {
LogMessage("SpotConnection::createCamStream: Failed to start streaming cameras with mask {:#x}",
cam_mask);
return false;
}
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createCamStream returns int32_t but returns false (0) on error. Return -1 for error paths to avoid ambiguity with a valid stream id.

Copilot uses AI. Check for mistakes.

Comment on lines 675 to 676
cam_streams_.clear();
vision_pipelines_.clear();
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Destructing cam_streams_ before vision_pipelines_ can leave VisionPipeline instances holding dangling references to SpotCamStream during stop()/destruction. Stop and clear vision_pipelines_ first, then clear cam_streams_.

Suggested change
cam_streams_.clear();
vision_pipelines_.clear();
vision_pipelines_.clear();
cam_streams_.clear();

Copilot uses AI. Check for mistakes.

Comment on lines 379 to 380
LogMessage("SpotConnection::connect: Created CUDA stream {:#x} and attached to buffer",
size_t(cuda_stream_));
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These log messages are emitted from SpotCamStream, but reference SpotConnection, which can confuse diagnostics. Update the text to reference SpotCamStream.

Copilot uses AI. Check for mistakes.

checkCudaError(cudaStreamDestroy(cuda_stream_), "cudaStreamDestroy for SpotConnection");
cuda_stream_ = nullptr;
LogMessage("Destroyed CUDA stream for SpotConnection");
LogMessage("SpotConnection::~SpotConnection: Destroyed CUDA stream");
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These log messages are emitted from SpotCamStream, but reference SpotConnection, which can confuse diagnostics. Update the text to reference SpotCamStream.

Copilot uses AI. Check for mistakes.


} catch (const std::exception& e) {
LogMessage("SOb_StopVisionPipeline: Exception while stopping vision pipeline for robot ID {}: {}", robot_id, e.what());
LogMessage("SOb_StopVisionPipeline: Exception while launching vision pipeline for robot ID {} @ stream-ID: {}",
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log message mentions 'launching' in StopVisionPipeline. Replace 'launching' with 'stopping' to reflect the actual operation.

Suggested change
LogMessage("SOb_StopVisionPipeline: Exception while launching vision pipeline for robot ID {} @ stream-ID: {}",
LogMessage("SOb_StopVisionPipeline: Exception while stopping vision pipeline for robot ID {} @ stream-ID: {}",

Copilot uses AI. Check for mistakes.

@fmz fmz requested a review from Copilot October 3, 2025 23:00
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 11 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines 684 to 688
int32_t SpotConnection::createCamStream(uint32_t cam_mask) {
if (!connected_) {
LogMessage("SpotConnection::createCamStream: Not connected to robot");
return false;
}
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function returns int32_t but uses 'return false;' (value 0) on failure; calling code/tests treat any value < 0 as failure, so a failure may be misinterpreted as success, and 0 could be a valid stream ID later. Return a negative error code (e.g. -1) consistently and ensure failed insertion/stream start cleans up the partially added entry.

Copilot uses AI. Check for mistakes.

Comment on lines 705 to 709
if (!it->second->streamCameras(cam_mask)) {
LogMessage("SpotConnection::createCamStream: Failed to start streaming cameras with mask {:#x}",
cam_mask);
return false;
}
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function returns int32_t but uses 'return false;' (value 0) on failure; calling code/tests treat any value < 0 as failure, so a failure may be misinterpreted as success, and 0 could be a valid stream ID later. Return a negative error code (e.g. -1) consistently and ensure failed insertion/stream start cleans up the partially added entry.

Copilot uses AI. Check for mistakes.

Comment on lines 691 to 693
if (cam_streams_.find(cam_mask) != cam_streams_.end()) {
LogMessage("SpotConnection::createCamStream: Note that a camera stream with mask {:#x} already exists",
cam_mask);
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cam_streams_ is keyed by stream_id, not by cam_mask, so this lookup will almost never detect an existing stream for the same camera mask and provides a false sense of duplication checking. To detect duplicates, either maintain a separate map keyed by cam_mask or iterate cam_streams_ and compare each stream's current_cam_mask_.

Suggested change
if (cam_streams_.find(cam_mask) != cam_streams_.end()) {
LogMessage("SpotConnection::createCamStream: Note that a camera stream with mask {:#x} already exists",
cam_mask);
bool duplicate_found = false;
for (const auto& [id, stream] : cam_streams_) {
if (stream && stream->current_cam_mask_ == cam_mask) {
LogMessage("SpotConnection::createCamStream: Note that a camera stream with mask {:#x} already exists",
cam_mask);
duplicate_found = true;
break;
}

Copilot uses AI. Check for mistakes.

if (!it->second->streamCameras(cam_mask)) {
LogMessage("SpotConnection::createCamStream: Failed to start streaming cameras with mask {:#x}",
cam_mask);
return false;
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On streaming failure the newly inserted (but non-functional) SpotCamStream remains in cam_streams_, leaving stale state. Erase the entry before returning (and return a negative error code) to avoid leaking an uninitialized stream object.

Suggested change
return false;
cam_streams_.erase(it);
return -1;

Copilot uses AI. Check for mistakes.


bool connected_;

int32_t next_stream_id_{0xee1};
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 0xee1 as the initial stream ID reduces clarity and may collide with caller assumptions; replace with 0 or a named constexpr (e.g. kInitialStreamId) with a comment explaining any non-zero start.

Suggested change
int32_t next_stream_id_{0xee1};
// Initial stream ID. If changed from 0, document the reason here.
static constexpr int32_t kInitialStreamId = 0xee1; // Non-zero start to avoid reserved IDs (document reason if needed)
int32_t next_stream_id_{kInitialStreamId};

Copilot uses AI. Check for mistakes.

Comment on lines +187 to +188
LogMessage("Copying RGB image of size {} bytes to write pointer at index {}, {:#x} rgb_write_ptr",
image_size, write_idx, size_t(rgb_write_ptr));
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-image verbose logging plus unconditional dumping (DumpRGBImageFromCuda) in the hot path will drastically reduce throughput and increase I/O load when debug dumps are enabled; wrap these in a conditional (e.g. debug/trace flag) or remove for release builds.

Copilot uses AI. Check for mistakes.

Comment on lines 201 to 208
DumpRGBImageFromCuda(
rgb_write_ptr,
cv_img.cols,
cv_img.rows,
cv_img.depth(),
"rgb",
n_rgbs_written + write_idx * n_images_per_response_
);
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-image verbose logging plus unconditional dumping (DumpRGBImageFromCuda) in the hot path will drastically reduce throughput and increase I/O load when debug dumps are enabled; wrap these in a conditional (e.g. debug/trace flag) or remove for release builds.

Copilot uses AI. Check for mistakes.

Comment on lines +228 to +229
LogMessage("Copying depth image of size {} bytes to write pointer at index {}, {:#x} rgb_write_ptr",
depth_size, write_idx, size_t(depth_write_ptr));
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-image verbose logging plus unconditional dumping (DumpRGBImageFromCuda) in the hot path will drastically reduce throughput and increase I/O load when debug dumps are enabled; wrap these in a conditional (e.g. debug/trace flag) or remove for release builds.

Copilot uses AI. Check for mistakes.

std::string password = argv[4];

//SOb_ToggleDebugDumps("./spot_dump");
SOb_ToggleDebugDumps("./spot_dump");
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling debug dumps unconditionally in the integration test will produce substantial disk I/O and may mask performance regressions; gate this behind a command-line flag or environment variable so normal test runs stay fast.

Copilot uses AI. Check for mistakes.

Comment on lines 453 to +482
// Grab the latest image set using the provided function
uint8_t* rgb_images[NUM_CAMERAS];
float* depth_images[NUM_CAMERAS];
bool ret = getImageSetFunc(robot_id, num_textures, rgb_images, depth_images);
bool ret = getImageSetFunc(robot_id, cam_stream_id, num_textures, rgb_images, depth_images);
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

num_textures is size_t but implicitly converts to int32_t parameter; add an explicit static_cast<int32_t>(num_textures) and (optionally) a bounds check to prevent silent truncation if ever > INT32_MAX.

Copilot uses AI. Check for mistakes.

@fmz fmz requested a review from Copilot October 4, 2025 20:34
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

src/spot-connection.cpp:1

  • The streaming thread is never started: _startStreamingThread() is not called in streamCameras, so after the initial probe request no further images will arrive. Call startStreamingThread() after successfully validating image sizes and before setting streaming = true (or immediately after setting it).
//

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +189 to +206
cudaMemcpyAsync(
image_cpu_buffer.data(),
images_set[i],
4 * 640 * 480,
cudaMemcpyDeviceToHost
);
cudaMemcpyAsync(
depth_cpu_buffer.data(),
depths_set[i],
640 * 480 * sizeof(float),
cudaMemcpyDeviceToHost
);
cudaStreamSynchronize(0); // Wait for the copy to complete

cv::Mat image(480, 640, CV_8UC4, image_cpu_buffer.data());
cv::Mat depth(480, 640, CV_32FC1, depth_cpu_buffer.data());

cv::cvtColor(image, image, cv::COLOR_RGBA2BGR);
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asynchronous cudaMemcpyAsync calls are issued on the default stream but the code immediately constructs cv::Mat and processes the data without synchronizing; this can lead to reading incomplete data. Insert a cudaStreamSynchronize(0) (or use a dedicated stream handle) after the copies and before using image_cpu_buffer/depth_cpu_buffer.

Copilot uses AI. Check for mistakes.

Comment on lines +32 to +40
static int32_t start_cam_stream(int32_t spot_id, uint32_t cam_bitmask) {
int32_t cam_stream_id = SOb_CreateCameraStream(spot_id, cam_bitmask);
if (cam_stream_id < 0) {
std::cerr << "Failed to start reading camera feeds" << std::endl;
SOb_DisconnectFromSpot(spot_id);
return -1;
}
std::cout << "Started camera stream with ID: " << cam_stream_id << std::endl;
return cam_stream_id;
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On failure you disconnect the robot but the caller (main) retains the original spot_id and later calls disconnect_from_spots, causing a second disconnect attempt on an already-closed connection. Either (a) do not disconnect here and let the caller handle cleanup, or (b) update the caller to mark spot_ids[i] = -1 when start_cam_stream returns < 0.

Copilot uses AI. Check for mistakes.

bool SpotConnection::streamCameras(uint32_t cam_mask) {
if (!connected_) {
LogMessage("SpotConnection::streamCameras: Not connected to robot");
bool SpotCamStream::streamCameras(uint32_t cam_mask) {
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The streaming thread is never started: _startStreamingThread() is not called in streamCameras, so after the initial probe request no further images will arrive. Call startStreamingThread() after successfully validating image sizes and before setting streaming = true (or immediately after setting it).

Copilot uses AI. Check for mistakes.

Comment on lines 242 to 248
DumpDepthImageFromCuda(
depth_write_ptr,
cv_img.cols,
cv_img.rows,
"post-depth-cache",
"depth",
n_depths_written + write_idx * n_images_per_response_
);
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-image logging plus unconditional DumpRGBImageFromCuda / DumpDepthImageFromCuda inside the hot push loop will significantly degrade throughput and generate large I/O. Gate these calls behind a debug flag or remove them for production paths.

Copilot uses AI. Check for mistakes.

Comment on lines +429 to +435
ThrowIfFailed(robot_resources[spot_id][stream][i].image_texture->Map(
0, &image_read_range, reinterpret_cast<void**>(&image_data)
), "Map image readback");

ThrowIfFailed(robot_resources[spot_id][stream][i].depth_texture->Map(
0, &depth_read_range, reinterpret_cast<void**>(&depth_data)
), "Map depth readback");
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Mapping and unmapping each resource every frame per image can be expensive; if these are readback buffers whose contents you fully overwrite, consider persistent mapping (Map once at creation and reuse pointers) to reduce CPU overhead.

Copilot uses AI. Check for mistakes.

Comment on lines +452 to +453
robot_resources[spot_id][stream][i].image_texture->Unmap(0, &no_write);
robot_resources[spot_id][stream][i].depth_texture->Unmap(0, &no_write);
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Mapping and unmapping each resource every frame per image can be expensive; if these are readback buffers whose contents you fully overwrite, consider persistent mapping (Map once at creation and reuse pointers) to reduce CPU overhead.

Copilot uses AI. Check for mistakes.

Comment on lines +354 to +355
s_OutputRGBTextures[robot_id][cam_stream_id].push_back(rgb_resource);
s_OutputDepthTextures[robot_id][cam_stream_id].push_back(depth_resource);
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using push_back without ensuring insertion order matches camera bit positions (cam_bit) can cause mismatches if registration order differs; consider indexing by the actual camera bit (e.g., position derived from least significant bit index) to make retrieval order deterministic.

Copilot uses AI. Check for mistakes.

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