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: 6 additions & 3 deletions docs/roadmap/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions docs/roadmap/infrastructure-maturity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions docs/roadmap/streaming-and-tiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions libs/usd-copc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
18 changes: 18 additions & 0 deletions libs/usd-copc/benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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()

if(USDGEO_BUILD_TESTS)
add_dependencies(usdCopc_runtime_streaming_probe usdCopc_tests)
add_test(NAME usdCopc_runtime_streaming_probe
COMMAND ${CMAKE_COMMAND}
-Dprobe=$<TARGET_FILE:usdCopc_runtime_streaming_probe>
-Dfixture=${CMAKE_CURRENT_BINARY_DIR}/runtime-streaming.copc
-Dfixture_generator=$<TARGET_FILE:usdCopc_tests>
-P ${CMAKE_CURRENT_SOURCE_DIR}/test_runtime_streaming_probe.cmake)
endif()
180 changes: 180 additions & 0 deletions libs/usd-copc/benchmarks/runtime_streaming_probe.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#include "usdgeo/RandomAccessSource.h"
#include "usdcopc/Copc.h"

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <limits>
#include <memory>
#include <string>
#include <vector>

namespace {

struct ProbeOptions {
std::string inputPath;
std::int32_t level = 0;
};

void PrintUsage() {
std::cerr << "Usage: usdCopc_runtime_streaming_probe <input.copc|input.copc.laz> "
"--level <non-negative 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<std::int32_t>::max()) {
return false;
}
level = static_cast<std::int32_t>(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<usdgeo::Diagnostic>& 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<std::int32_t>::min() ||
tile.tile.id.level > std::numeric_limits<std::int32_t>::max() ||
tile.tile.id.x < std::numeric_limits<std::int32_t>::min() ||
tile.tile.id.x > std::numeric_limits<std::int32_t>::max() ||
tile.tile.id.y < std::numeric_limits<std::int32_t>::min() ||
tile.tile.id.y > std::numeric_limits<std::int32_t>::max() ||
tile.tile.id.z < std::numeric_limits<std::int32_t>::min() ||
tile.tile.id.z > std::numeric_limits<std::int32_t>::max() ||
tile.tile.lod.items.empty() ||
tile.tile.lod.items.front().pointCount >
static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max()) ||
tile.pointDataSize >
static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max())) {
return false;
}
entry.level = static_cast<std::int32_t>(tile.tile.id.level);
entry.x = static_cast<std::int32_t>(tile.tile.id.x);
entry.y = static_cast<std::int32_t>(tile.tile.id.y);
entry.z = static_cast<std::int32_t>(tile.tile.id.z);
entry.pointCount = static_cast<std::int32_t>(
tile.tile.lod.items.front().pointCount);
entry.offset = tile.pointDataOffset;
entry.byteSize = static_cast<std::int32_t>(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<usdgeo::RandomAccessSource>(
std::move(localSource));

usdcopc::CopcReader reader(source);
usdcopc::CopcHeader header;
std::vector<usdgeo::Diagnostic> diagnostics;
if (!reader.ReadMetadata(header, diagnostics)) {
PrintDiagnostics(diagnostics);
return 1;
}

std::vector<usdcopc::CopcHierarchyEntry> 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<usdcopc::CopcPointTile> tiles;
if (!reader.BuildPointTiles(hierarchy, tiles, diagnostics)) {
PrintDiagnostics(diagnostics);
return 1;
}
std::sort(tiles.begin(), tiles.end(), TileOrder);

std::vector<const usdcopc::CopcPointTile*> 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<usdlas::LasPoint> points;
if (!reader.ReadPoints(header, entry, points, diagnostics)) {
PrintDiagnostics(diagnostics);
return 1;
}
selectedPointRangeBytes += tile->pointDataSize;
decodedPoints += static_cast<std::uint64_t>(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;
}
36 changes: 36 additions & 0 deletions libs/usd-copc/benchmarks/test_runtime_streaming_probe.cmake
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion tools/usd-pointcloud-convert/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void HandleInterrupt(int) {

void PrintUsage() {
std::cerr
<< "Usage: usd-pointcloud-convert <input.las|input.laz|input.copc> "
<< "Usage: usd-pointcloud-convert <input.las|input.laz|input.copc|input.copc.laz> "
"<output.usda> "
"(--tile-size <source-units> | --max-points-per-tile <count>) [options]\n"
<< "Options:\n"
Expand Down
Loading