- StereoRectifier wrapping cv::stereoRectify + cv::initUndistortRectifyMap (CV_16SC2 maps) - PointCloudBuilder with cv::reprojectImageTo3D and depth/NaN filtering - CloudPointClient high-level facade: connect(), compute_cloud() -> std::expected<PointCloud, Error> - write_ply() ASCII PLY export helper - CLI options 4 (compute-cloud) and 5 (compute-cloud + save PLY) - Fix TcpServer to loop over multiple requests per connection - Expose num_disparities parameter through CloudPointClient and StereoMatcherFactory - Unity C# integration design spec - Sync README, AGENTS, openwiki with stereo pipeline (C++23) - E2E synthetic-scene test with constant-disparity stereo pair TG-9 #ready-for-test TG-4 #ready-for-test TG-2 #in-progress
10 KiB
Architecture
Overview
The system follows a layered architecture for JSON-RPC 2.0 communication over TCP:
┌──────────────────────────────────────────────────────┐
│ Application / Unity │
│ (registers callbacks via C API or server_main.cpp) │
├──────────────────────────────────────────────────────┤
│ OpenCV Compute Layer (optional — requires opencv4) │
│ CloudPointClient (facade: connect, compute_cloud, │
│ write_ply) │
│ StereoRectifier (cv::stereoRectify + remap, Q mat) │
│ PointCloudBuilder (SGBM disparity, reproject, NaN │
│ filter) │
├──────────────────────────────────────────────────────┤
│ RPC Layer │
│ RpcServer (method dispatch, JSON-RPC 2.0 handling) │
│ RpcClient (typed method calls via jsonrpccxx) │
├──────────────────────────────────────────────────────┤
│ Service Layer │
│ Service (mock camera data: intrinsics, extrinsics, │
│ stereo calibration, image pairs) │
├──────────────────────────────────────────────────────┤
│ Transport Layer │
│ TcpServer (accept loop, per-client threads — loops) │
│ TCPConnector (client-side connector for jsonrpccxx) │
│ tcp_read (framed read with size prefix) │
├──────────────────────────────────────────────────────┤
│ Serialization │
│ serialize.hpp (size embedding, numeric (de)serialize)│
│ rpc_coder (Base64 encode/decode) │
├──────────────────────────────────────────────────────┤
│ Config │
│ ConfigLoader (YAML → ServerConfig + TestData) │
└──────────────────────────────────────────────────────┘
All C++ code lives in the score namespace. The rpc/ git submodule provides jsonrpcxx headers used by the client side.
Communication model
The PlantUML diagram at docs/communication_model.pu (rendered as docs/cm.png) describes the interaction flow:
- Unity side initializes a
CloudPointServerviacrpc_init(), which starts aTcpServerawaiting connections. - Client side calls
CloudPointClient::connect(), which opens a TCP connection and immediately fetchesget-stereo-calibrationonce, initialisingStereoRectifierwith the returned intrinsics and geometry. - On each
compute_cloud()call, the client fetches a synchronised image pair viaget-image-pair, passes the decoded images throughStereoRectifier::rectify(), thenPointCloudBuilder::build()(SGBM disparity at 1/16 scale →cv::reprojectImageTo3D→ NaN filter), and returnsstd::expected<PointCloud, Error>to the caller. - The server reads each request, dispatches it to
RpcServer, which calls the registered handler (a C callback from Unity). - The Unity integration uses a main-thread dispatcher: RPC callbacks arrive on a C++ per-client thread and enqueue a
TaskCompletionSourceonto aConcurrentQueue; the UnityUpdate()loop drains the queue and completes the task. A bounded timeout (~5000 ms) prevents deadlocks when the editor is paused. - The handler result is serialized back as a JSON-RPC response and sent over TCP; the per-client handler thread loops back to await the next request on the same connection.
See docs/unity-integration.md for the full Unity C# design spec.
Wire framing
Every TCP message (both directions) uses a simple length-prefixed framing protocol:
- Header: 8 bytes — a
uint64_tin native byte order containing the payload size. - Payload: exactly
payload_sizebytes of JSON-RPC text.
This is implemented in:
inplace_size_embedding()(include/cloud_point_rpc/serialize.hpp): Prepends the serializeduint64_tsize to astd::string. Used byTcpServer::handle_clientandTCPConnector::Send.tcp_read()(include/cloud_point_rpc/tcp_read.hpp): Reads the 8-byte header, deserializes the size, then reads the full payload — continuing to read if the payload arrives in multiple TCP segments.
// Sending: embed size before the JSON payload
std::string response = processor_(payload);
response += "\n";
inplace_size_embedding(response);
asio::write(*socket, asio::buffer(response));
// Receiving: read 8-byte header, then full payload
std::array<char, 8> header;
asio::read(socket, asio::buffer(header, header.size()));
uint64_t packet_size = deserialize<uint64_t>(v);
std::vector<char> payload(packet_size);
asio::read(socket, asio::buffer(payload));
TcpServer
include/cloud_point_rpc/tcp_server.hpp
- Accepts connections on a configurable IP/port using asio.
- Runs an accept thread (
std::jthread) that blocks onacceptor_.accept(). - Each client connection is handled in its own
std::jthread; finished threads are cleaned up via astd::list<pair<jthread, future<bool>>>with periodicremove_if. - The
RequestProcessor(astd::function<std::string(const std::string&)>) is called for each incoming request — typicallyRpcServer::process. stop()unblocks the accept loop by first connecting a dummy socket to the listening endpoint (to avoid a race withclose()), then closing the acceptor.- Thread safety:
cliThrMtx_guards the client thread list;acceptorMtx_guards the acceptor.
Key source: include/cloud_point_rpc/tcp_server.hpp
RpcServer
include/cloud_point_rpc/rpc_server.hpp, src/rpc_server.cpp
- Maintains a
std::map<std::string, Handler>of registered methods. - Handler type:
std::function<std::variant<nlohmann::json, std::string>(const nlohmann::json&)>— handlers can return either a JSON object or a raw string. - Supports two registration overloads:
register_method(name, Handler)— for C++ lambdas/functors.register_method(name, callback_t)— for C function pointers (rpc_string* (*)(rpc_string*)). The C callback receives the JSON params as a string and returns a string that is parsed as JSON if possible, or kept as a raw string otherwise.
process(request_str)parses the JSON-RPC 2.0 request, validates required fields (jsonrpc,method,id), dispatches to the handler, and builds the response. Returns JSON-RPC error objects for parse errors (-32700), invalid requests (-32600), method not found (-32601), and server errors (-32000).
Known issue
Non-object JSON requests (arrays, strings, numbers, null) throw nlohmann::json::type_error instead of returning a -32600 Invalid Request error. This is documented in tests/test_rpc_edge_cases.cpp.
RpcClient
include/cloud_point_rpc/rpc_client.hpp
- Extends
jsonrpccxx::JsonRpcClientfrom the json-rpc-cxx submodule. - Wraps a
TCPConnectoras the transport connector. - Provides typed methods:
get_intrinsic_params(),get_extrinsic_params(),get_cloud_point(). - Template
call<ReturnType>(name)wrapsCallMethodwith an auto-incrementing request ID.
TCPConnector
include/cloud_point_rpc/tcp_connector.hpp
- Implements
jsonrpccxx::IClientConnector. - On construction, opens a TCP connection to the server.
Send(request)embeds the size prefix, writes the full message, then callstcp_read()to receive the response.
Service
include/cloud_point_rpc/service.hpp, src/service.cpp
- Holds
TestData(intrinsic params, extrinsic params, cloud point). - Returns configured data if available, otherwise returns identity-matrix fallbacks.
- Used by
server_main.cppfor the standalone mock server. The C API path does not useService— Unity provides its own handlers via callbacks.
Config
include/cloud_point_rpc/config.hpp
ConfigLoader::load(path)parses a YAML file into aConfigstruct.ConfigcontainsServerConfig(ip, port) andTestData(camera parameters).- Falls back to
127.0.0.1:8080if noserversection is present. - Sample config:
config.yml(server IP127.0.0.1, port9095).
Executables
| Executable | Source | Description |
|---|---|---|
cloud_point_rpc_server |
src/server_main.cpp |
Standalone server with mock Service data |
cloud_point_rpc_cli |
src/main.cpp |
Interactive CLI client (menu-driven) |
minimal_client |
src/minimal_client.cpp |
Minimal client that sends a hardcoded ping request |
Shared libraries
| Library | Sources | Description |
|---|---|---|
libcloud_point_rpc |
rpc_coder.cpp, rpc_server.cpp, server_api.cpp, service.cpp |
Core RPC + server + config |
libcloud_point_rpc_cli |
cli.cpp |
CLI client logic (links against core lib) |
test_cloud_point |
test_api.cpp |
Test API library for method scheduling and auto-calling |
Threading model
- Server: Accept thread + per-client handler threads (all
std::jthread). Each client handler loops: it reads one request, processes it, sends the response, and then loops back to read the next request on the same connection. The connection remains open until the client disconnects (EOF) or an error occurs. - C API server (
server_api.cpp): The globalRpcServerandTcpServerare guarded byserver_mtx. Therpc_stringgarbage collector is guarded bygc_mtx. - Test API (
test_api.cpp): ATestThreadruns astd::jthreadwith a condition-variable-driven loop that can auto-call registered methods at a configurable interval or process queued one-shot calls.