From eeae548a490f1d2c86c618124c10a63fa965c6cb Mon Sep 17 00:00:00 2001 From: snkmcb Date: Sat, 5 Sep 2026 00:12:23 +0900 Subject: [PATCH 1/2] feat: add COPC runtime streaming research probe --- docs/roadmap/implementation-status.md | 9 +- docs/roadmap/infrastructure-maturity.md | 7 + docs/roadmap/streaming-and-tiling.md | 15 ++ libs/usd-copc/CMakeLists.txt | 4 + libs/usd-copc/benchmarks/CMakeLists.txt | 8 + .../benchmarks/runtime_streaming_probe.cpp | 180 ++++++++++++++++++ tools/usd-pointcloud-convert/main.cpp | 2 +- 7 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 libs/usd-copc/benchmarks/CMakeLists.txt create mode 100644 libs/usd-copc/benchmarks/runtime_streaming_probe.cpp diff --git a/docs/roadmap/implementation-status.md b/docs/roadmap/implementation-status.md index 2b6ebb5..fc027e6 100644 --- a/docs/roadmap/implementation-status.md +++ b/docs/roadmap/implementation-status.md @@ -403,9 +403,12 @@ Phase 6 — validation and baselines: #### Research - runtime streaming (no release gate) -- [ ] Experiment with host LOD decision, COPC node identity, range reads, and - payload loading without adding runtime abstractions to the conversion - pipeline +- [x] Add an opt-in COPC runtime-streaming probe that takes a requested node + level, selects deterministic COPC node identities, decodes only those + point-data ranges, and reports source bytes read without adding runtime + abstractions to the conversion pipeline +- [ ] Measure host LOD decisions, OpenUSD payload composition, and renderer + loading behavior; these remain host-dependent research E57, delimited text, and other point-cloud formats follow these infrastructure milestones. A public custom USD schema remains deferred until the documented diff --git a/docs/roadmap/infrastructure-maturity.md b/docs/roadmap/infrastructure-maturity.md index 9ce9e8a..3443957 100644 --- a/docs/roadmap/infrastructure-maturity.md +++ b/docs/roadmap/infrastructure-maturity.md @@ -512,6 +512,13 @@ experiments. Findings may inform the conversion pipeline; they do not get to complicate it before the evidence exists. No runtime abstraction is added to the FileFormat or converter architecture on speculation. +The first experiment is the opt-in `usdCopc_runtime_streaming_probe`. It uses +the existing COPC hierarchy and `RandomAccessSource` contracts to select a +requested hierarchy level, decode only its point-data ranges, and report +actual source bytes read. It establishes source-side range behavior without +claiming to model host LOD policy, OpenUSD payload composition, or renderer +loading. + ### Later - E57 and Other Point-Cloud Formats E57 is valuable but is not the immediate next milestone. Its decoder should diff --git a/docs/roadmap/streaming-and-tiling.md b/docs/roadmap/streaming-and-tiling.md index fb47c39..b0dd9d6 100644 --- a/docs/roadmap/streaming-and-tiling.md +++ b/docs/roadmap/streaming-and-tiling.md @@ -34,6 +34,21 @@ implements today is in [implementation status](implementation-status.md) and [capability matrix](../reference/CAPABILITY_MATRIX.md). +The opt-in `usdCopc_runtime_streaming_probe` provides the first runtime +streaming experiment without changing the production pipeline. It accepts a +local COPC and a requested hierarchy level, selects point-data nodes in stable +tile order, decodes only those ranges, and reports hierarchy size, selected +points, selected range bytes, and actual source bytes read as TSV. Build it +with `USDGEO_BUILD_BENCHMARKS=ON` and run: + +```powershell +usdCopc_runtime_streaming_probe C:\path\to\sample.copc --level 3 +``` + +This measures the source-side portion of the runtime-streaming question. +Host LOD policy, OpenUSD payload composition, and renderer loading remain +outside the probe and are still research work. + ## 1. Problem statement Chunked decoding reduces decoder buffer size. The tiled path additionally diff --git a/libs/usd-copc/CMakeLists.txt b/libs/usd-copc/CMakeLists.txt index 91ca733..a6b8f01 100644 --- a/libs/usd-copc/CMakeLists.txt +++ b/libs/usd-copc/CMakeLists.txt @@ -36,3 +36,7 @@ install(DIRECTORY include/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") if(USDGEO_BUILD_TESTS) add_subdirectory(tests) endif() + +if(USDGEO_BUILD_BENCHMARKS) + add_subdirectory(benchmarks) +endif() diff --git a/libs/usd-copc/benchmarks/CMakeLists.txt b/libs/usd-copc/benchmarks/CMakeLists.txt new file mode 100644 index 0000000..8f832f3 --- /dev/null +++ b/libs/usd-copc/benchmarks/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(usdCopc_runtime_streaming_probe + runtime_streaming_probe.cpp) +target_link_libraries(usdCopc_runtime_streaming_probe + PRIVATE usdcopc::core) + +if(MSVC) + target_compile_options(usdCopc_runtime_streaming_probe PRIVATE /utf-8) +endif() \ No newline at end of file diff --git a/libs/usd-copc/benchmarks/runtime_streaming_probe.cpp b/libs/usd-copc/benchmarks/runtime_streaming_probe.cpp new file mode 100644 index 0000000..5f5cf59 --- /dev/null +++ b/libs/usd-copc/benchmarks/runtime_streaming_probe.cpp @@ -0,0 +1,180 @@ +#include "usdgeo/RandomAccessSource.h" +#include "usdcopc/Copc.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct ProbeOptions { + std::string inputPath; + std::int32_t level = 0; +}; + +void PrintUsage() { + std::cerr << "Usage: usdCopc_runtime_streaming_probe " + "--level \n"; +} + +bool ParseLevel(const std::string& value, std::int32_t& level) { + try { + std::size_t consumed = 0; + const auto parsed = std::stoll(value, &consumed); + if (consumed != value.size() || parsed < 0 || + parsed > std::numeric_limits::max()) { + return false; + } + level = static_cast(parsed); + return true; + } catch (...) { + return false; + } +} + +bool ParseOptions(int argc, char** argv, ProbeOptions& options) { + if (argc == 2 && std::string(argv[1]) == "--help") { + PrintUsage(); + return false; + } + if (argc < 4) { + PrintUsage(); + return false; + } + options.inputPath = argv[1]; + for (int index = 2; index < argc; ++index) { + if (std::string(argv[index]) != "--level" || index + 1 >= argc || + !ParseLevel(argv[++index], options.level)) { + PrintUsage(); + return false; + } + } + return true; +} + +void PrintDiagnostics(const std::vector& diagnostics) { + for (const auto& diagnostic : diagnostics) { + std::cerr << diagnostic.message << "\n"; + } +} + +bool TileOrder(const usdcopc::CopcPointTile& left, + const usdcopc::CopcPointTile& right) { + const auto& leftId = left.tile.id; + const auto& rightId = right.tile.id; + if (leftId.level != rightId.level) return leftId.level < rightId.level; + if (leftId.x != rightId.x) return leftId.x < rightId.x; + if (leftId.y != rightId.y) return leftId.y < rightId.y; + return leftId.z < rightId.z; +} + +bool MakeEntry(const usdcopc::CopcPointTile& tile, + usdcopc::CopcHierarchyEntry& entry) { + if (tile.tile.id.level < std::numeric_limits::min() || + tile.tile.id.level > std::numeric_limits::max() || + tile.tile.id.x < std::numeric_limits::min() || + tile.tile.id.x > std::numeric_limits::max() || + tile.tile.id.y < std::numeric_limits::min() || + tile.tile.id.y > std::numeric_limits::max() || + tile.tile.id.z < std::numeric_limits::min() || + tile.tile.id.z > std::numeric_limits::max() || + tile.tile.lod.items.empty() || + tile.tile.lod.items.front().pointCount > + static_cast(std::numeric_limits::max()) || + tile.pointDataSize > + static_cast(std::numeric_limits::max())) { + return false; + } + entry.level = static_cast(tile.tile.id.level); + entry.x = static_cast(tile.tile.id.x); + entry.y = static_cast(tile.tile.id.y); + entry.z = static_cast(tile.tile.id.z); + entry.pointCount = static_cast( + tile.tile.lod.items.front().pointCount); + entry.offset = tile.pointDataOffset; + entry.byteSize = static_cast(tile.pointDataSize); + return entry.IsValid() && entry.IsPointData(); +} + +} // namespace + +int main(int argc, char** argv) { + ProbeOptions options; + if (!ParseOptions(argc, argv, options)) { + return argc == 2 && std::string(argv[1]) == "--help" ? 0 : 2; + } + + auto localSource = usdgeo::OpenLocalRandomAccessSource(options.inputPath); + if (!localSource) { + std::cerr << "Unable to open COPC source\n"; + return 1; + } + auto source = std::shared_ptr( + std::move(localSource)); + + usdcopc::CopcReader reader(source); + usdcopc::CopcHeader header; + std::vector diagnostics; + if (!reader.ReadMetadata(header, diagnostics)) { + PrintDiagnostics(diagnostics); + return 1; + } + + std::vector entries; + if (!reader.ReadHierarchy(header, entries, diagnostics)) { + PrintDiagnostics(diagnostics); + return 1; + } + usdcopc::CopcHierarchy hierarchy; + if (!reader.BuildHierarchy(header, entries, hierarchy, diagnostics)) { + PrintDiagnostics(diagnostics); + return 1; + } + std::vector tiles; + if (!reader.BuildPointTiles(hierarchy, tiles, diagnostics)) { + PrintDiagnostics(diagnostics); + return 1; + } + std::sort(tiles.begin(), tiles.end(), TileOrder); + + std::vector selectedTiles; + for (const auto& tile : tiles) { + if (tile.tile.id.level == options.level) { + selectedTiles.push_back(&tile); + } + } + if (selectedTiles.empty()) { + std::cerr << "Requested COPC level has no point-data tiles\n"; + return 2; + } + + std::uint64_t selectedPointRangeBytes = 0; + std::uint64_t decodedPoints = 0; + for (const auto* tile : selectedTiles) { + usdcopc::CopcHierarchyEntry entry; + if (!MakeEntry(*tile, entry)) { + std::cerr << "COPC point tile cannot be represented as a hierarchy entry\n"; + return 1; + } + std::vector points; + if (!reader.ReadPoints(header, entry, points, diagnostics)) { + PrintDiagnostics(diagnostics); + return 1; + } + selectedPointRangeBytes += tile->pointDataSize; + decodedPoints += static_cast(points.size()); + } + + std::cout << "input\tlevel\thierarchy_nodes\tpoint_tiles\tselected_tiles\t" + "decoded_points\tselected_point_range_bytes\tsource_bytes_read\n" + << options.inputPath << '\t' << options.level << '\t' + << hierarchy.nodes.size() << '\t' << tiles.size() << '\t' + << selectedTiles.size() << '\t' << decodedPoints << '\t' + << selectedPointRangeBytes << '\t' << source->BytesRead() + << '\n'; + return 0; +} \ No newline at end of file diff --git a/tools/usd-pointcloud-convert/main.cpp b/tools/usd-pointcloud-convert/main.cpp index d439bb4..0eed9b1 100644 --- a/tools/usd-pointcloud-convert/main.cpp +++ b/tools/usd-pointcloud-convert/main.cpp @@ -33,7 +33,7 @@ void HandleInterrupt(int) { void PrintUsage() { std::cerr - << "Usage: usd-pointcloud-convert " + << "Usage: usd-pointcloud-convert " " " "(--tile-size | --max-points-per-tile ) [options]\n" << "Options:\n" From a37cb7d423704ce8ff6c46685ada6e2619f9e658 Mon Sep 17 00:00:00 2001 From: snkmcb Date: Sat, 5 Sep 2026 00:16:15 +0900 Subject: [PATCH 2/2] test: cover runtime streaming probe --- libs/usd-copc/benchmarks/CMakeLists.txt | 10 ++++++ .../test_runtime_streaming_probe.cmake | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 libs/usd-copc/benchmarks/test_runtime_streaming_probe.cmake diff --git a/libs/usd-copc/benchmarks/CMakeLists.txt b/libs/usd-copc/benchmarks/CMakeLists.txt index 8f832f3..5836abf 100644 --- a/libs/usd-copc/benchmarks/CMakeLists.txt +++ b/libs/usd-copc/benchmarks/CMakeLists.txt @@ -5,4 +5,14 @@ target_link_libraries(usdCopc_runtime_streaming_probe if(MSVC) target_compile_options(usdCopc_runtime_streaming_probe PRIVATE /utf-8) +endif() + +if(USDGEO_BUILD_TESTS) + add_dependencies(usdCopc_runtime_streaming_probe usdCopc_tests) + add_test(NAME usdCopc_runtime_streaming_probe + COMMAND ${CMAKE_COMMAND} + -Dprobe=$ + -Dfixture=${CMAKE_CURRENT_BINARY_DIR}/runtime-streaming.copc + -Dfixture_generator=$ + -P ${CMAKE_CURRENT_SOURCE_DIR}/test_runtime_streaming_probe.cmake) endif() \ No newline at end of file diff --git a/libs/usd-copc/benchmarks/test_runtime_streaming_probe.cmake b/libs/usd-copc/benchmarks/test_runtime_streaming_probe.cmake new file mode 100644 index 0000000..d3a925f --- /dev/null +++ b/libs/usd-copc/benchmarks/test_runtime_streaming_probe.cmake @@ -0,0 +1,36 @@ +if(NOT DEFINED probe OR NOT DEFINED fixture OR + NOT DEFINED fixture_generator) + message(FATAL_ERROR "runtime streaming probe test arguments are incomplete") +endif() + +file(REMOVE_RECURSE "${fixture}") +execute_process( + COMMAND "${fixture_generator}" --write-fixture "${fixture}" + RESULT_VARIABLE fixture_result + OUTPUT_VARIABLE fixture_output + ERROR_VARIABLE fixture_error) +if(NOT fixture_result EQUAL 0) + message(FATAL_ERROR + "runtime streaming fixture generation failed: " + "${fixture_output}${fixture_error}") +endif() + +execute_process( + COMMAND "${probe}" "${fixture}" --level 0 + RESULT_VARIABLE probe_result + OUTPUT_VARIABLE probe_output + ERROR_VARIABLE probe_error) +if(NOT probe_result EQUAL 0) + message(FATAL_ERROR + "runtime streaming probe failed: ${probe_output}${probe_error}") +endif() +if(NOT probe_output MATCHES "hierarchy_nodes") + message(FATAL_ERROR "probe output is missing its TSV header: ${probe_output}") +endif() +if(NOT probe_output MATCHES "selected_point_range_bytes") + message(FATAL_ERROR "probe output is missing range accounting: ${probe_output}") +endif() +if(NOT probe_output MATCHES "\\t0\\t[0-9]+\\t1\\t1\\t3\\t[1-9][0-9]*\\t[1-9][0-9]*") + message(FATAL_ERROR + "probe output has unexpected level-0 results: ${probe_output}") +endif() \ No newline at end of file