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
8 changes: 6 additions & 2 deletions docs/roadmap/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,12 @@ Phase 6 — validation and baselines:
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
- [x] Add a research-only OpenUSD LOD payload composition probe that records
authored payload arcs, `LoadNone`, selective payload loading, and
`LoadAll` point counts on a deterministic three-LOD fixture
- [ ] Measure host LOD decisions and renderer loading behavior; these remain
host- and render-delegate-dependent research, and payload composition
results must not be presented as renderer memory savings

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: 6 additions & 1 deletion docs/roadmap/infrastructure-maturity.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,12 @@ 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.
loading. The separate
`usdPointCloudAuthoring_runtime_lod_payload_probe` establishes the OpenUSD
composition baseline with a deterministic three-LOD payload fixture: authored
payload arcs, `LoadNone`, selective payload loading, and `LoadAll` point counts.
It still does not establish host LOD decisions or renderer loading behavior,
and neither probe is a production runtime-streaming abstraction.

### Later - E57 and Other Point-Cloud Formats

Expand Down
16 changes: 16 additions & 0 deletions docs/roadmap/streaming-and-tiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ available for comparison with host observations. Host LOD policy, OpenUSD
payload composition, and renderer loading remain outside the probe and are
still research work.

The OpenUSD composition portion is measured separately by the
`usdPointCloudAuthoring_runtime_lod_payload_probe` target. It creates a
deterministic three-LOD payload-backed fixture and reports authored payload
arcs, loaded LODs after `LoadNone`, a selective LOD load, and `LoadAll` point
counts as TSV. Build it with `USDGEO_BUILD_BENCHMARKS=ON` and run:

```powershell
usdPointCloudAuthoring_runtime_lod_payload_probe `
--output C:\path\to\runtime-lod-payload\PointCloud.usda
```

The probe verifies OpenUSD stage and payload-loading semantics only. It does
not identify a host's camera-driven LOD decision, renderer visibility, frame
latency, or renderer memory savings. Those measurements remain host- and
render-delegate-dependent research.

## 1. Problem statement

Chunked decoding reduces decoder buffer size. The tiled path additionally
Expand Down
12 changes: 12 additions & 0 deletions libs/usd-pointcloud-authoring/benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ if(WIN32)
target_link_libraries(usdPointCloudAuthoring_stream_benchmark PRIVATE Psapi)
endif()

add_executable(usdPointCloudAuthoring_runtime_lod_payload_probe
runtime_lod_payload_probe.cpp)
target_link_libraries(usdPointCloudAuthoring_runtime_lod_payload_probe
PRIVATE usdpointcloud::authoring)

if(USDGEO_BUILD_TESTS)
add_dependencies(usdPointCloudAuthoring_stream_benchmark usdCopc_tests)
add_test(NAME usdPointCloudAuthoring_stream_benchmark_las
Expand Down Expand Up @@ -50,4 +55,11 @@ if(USDGEO_BUILD_TESTS)
-Dfixture_generator=$<TARGET_FILE:usdCopc_tests>
-Dtest_root=${CMAKE_CURRENT_BINARY_DIR}/benchmark-cross-format
-P ${CMAKE_CURRENT_SOURCE_DIR}/compare_stream_benchmark.cmake)
add_test(NAME usdPointCloudAuthoring_runtime_lod_payload_probe
COMMAND ${CMAKE_COMMAND}
-Dprobe=$<TARGET_FILE:usdPointCloudAuthoring_runtime_lod_payload_probe>
-Doutput=${CMAKE_CURRENT_BINARY_DIR}/runtime-lod-payload
-P ${CMAKE_CURRENT_SOURCE_DIR}/test_runtime_lod_payload_probe.cmake)
usdgeo_configure_openusd_test_runtime(
usdPointCloudAuthoring_runtime_lod_payload_probe)
endif()
247 changes: 247 additions & 0 deletions libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
#include "usdgeo/PointCloudLayer.h"

#include <pxr/base/gf/vec3f.h>
#include <pxr/base/vt/array.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/sdf/primSpec.h>
#include <pxr/usd/usdGeom/points.h>
#include <pxr/usd/usd/stage.h>

#include <cstdint>
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>

namespace {

struct ProbeOptions {
std::filesystem::path outputPath;
};

void PrintUsage() {
std::cerr << "Usage: usdPointCloudAuthoring_runtime_lod_payload_probe "
"--output <root.usda>\n";
}

bool ParseOptions(int argc, char** argv, ProbeOptions& options) {
if (argc != 3 || std::string(argv[1]) != "--output" ||
std::string(argv[2]).empty()) {
PrintUsage();
return false;
}
options.outputPath = argv[2];
return true;
}

usdgeo::PointCloudLayer::Data MakeData(std::size_t pointCount) {
usdgeo::PointCloudLayer::Data data;
for (std::size_t index = 0; index < pointCount; ++index) {
data.positions.push_back({static_cast<double>(index), 0.0, 0.0});
}
return data;
}

usdpointcloud::PointCloudAsset MakeAsset(
const usdgeo::GeoReference& reference,
const usdgeo::SpatialBounds& bounds,
std::size_t pointCount) {
usdpointcloud::PointCloudAsset asset;
asset.reference = reference;
asset.bounds = bounds;
asset.data = MakeData(pointCount);
asset.chunk = usdpointcloud::MakePointChunk(asset.data, bounds);
return asset;
}

std::string TileName() {
return "Tile_L0_p0_p0_p0";
}

std::string LodPath(std::size_t index) {
return "/PointCloud/Tiles/" + TileName() + "/LOD" +
std::to_string(index);
}

std::size_t CountLoadedLods(const pxr::UsdStageRefPtr& stage) {
std::size_t count = 0;
for (std::size_t index = 0; index < 3; ++index) {
if (stage->GetPrimAtPath(pxr::SdfPath(LodPath(index))).IsLoaded()) {
++count;
}
}
return count;
}

std::size_t LoadedPointCount(const pxr::UsdStageRefPtr& stage) {
std::size_t count = 0;
for (std::size_t index = 0; index < 3; ++index) {
const auto points = pxr::UsdGeomPoints::Get(
stage, pxr::SdfPath(LodPath(index) + "/Points"));
if (!points) {
continue;
}
pxr::VtVec3fArray positions;
if (points.GetPointsAttr().Get(&positions)) {
count += positions.size();
}
}
return count;
}

std::size_t AuthoredPayloadCount(const pxr::SdfLayerHandle& rootLayer) {
std::size_t count = 0;
for (std::size_t index = 0; index < 3; ++index) {
const auto primSpec = rootLayer->GetPrimAtPath(
pxr::SdfPath(LodPath(index)));
if (!primSpec) {
continue;
}
count += primSpec->GetPayloadList().GetPrependedItems().size();
}
return count;
}

bool CreateFixture(const std::filesystem::path& outputPath) {
std::error_code error;
const auto absoluteOutputPath = std::filesystem::absolute(outputPath, error);
if (error) {
return false;
}
if (!absoluteOutputPath.parent_path().empty()) {
std::filesystem::create_directories(
absoluteOutputPath.parent_path(), error);
if (error) {
return false;
}
}

usdgeo::GeoReference reference;
reference.epsgCode = 26910;
reference.wkt = "WKT";
reference.stageUpAxis = "Y";
usdgeo::SpatialBounds bounds;
bounds.Expand({0.0, 0.0, 0.0});
bounds.Expand({2.0, 1.0, 1.0});

usdpointcloud::PointLodHierarchy hierarchy;
hierarchy.bounds = bounds;
hierarchy.defaultIndex = 2;
hierarchy.screenSizeThresholds = {0.25F, 0.10F};
for (std::size_t index = 0; index < 3; ++index) {
usdpointcloud::PointLodItem item;
item.index = static_cast<std::uint32_t>(index);
item.pointCount = 3 - index;
item.bounds = bounds;
item.sourceRange = {0, item.pointCount};
item.spacing = static_cast<double>(index + 1);
hierarchy.items.push_back(item);
}

usdgeo::PointCloudTileAsset tile;
tile.tile.id = {0, 0, 0, 0};
tile.tile.bounds = bounds;
tile.tile.lod = hierarchy;
for (std::size_t index = 0; index < 3; ++index) {
tile.levels.push_back(MakeAsset(reference, bounds, 3 - index));
}

const auto payloadDirectory = absoluteOutputPath.parent_path() / "payloads";
const auto stage = usdgeo::PointCloudLayer::CreateStage();
if (!stage) {
return false;
}
usdgeo::PointCloudPayloadOptions options;
options.directory = payloadDirectory.string();
options.rootLayerPath = absoluteOutputPath.string();
if (!usdgeo::AuthorPointCloudTiledAssetWithPayloads(
stage, "/PointCloud", {tile}, options)) {
return false;
}
stage->GetRootLayer()->SetIdentifier(absoluteOutputPath.generic_string());
if (!stage->GetRootLayer()->Export(absoluteOutputPath.string())) {
return false;
}
return true;
}

bool RunProbe(const std::filesystem::path& outputPath) {
const auto authoredStage = pxr::UsdStage::Open(outputPath.string());
if (!authoredStage) {
return false;
}
const auto authoredPayloads = AuthoredPayloadCount(
authoredStage->GetRootLayer());
const auto authoredLods = std::size_t{3};
const auto defaultIndex = authoredStage->GetPrimAtPath(
pxr::SdfPath("/PointCloud/Tiles/" + TileName()))
.GetAttribute(pxr::TfToken("lod:default:index"));
int defaultValue = -1;
if (!defaultIndex || !defaultIndex.Get(&defaultValue)) {
return false;
}

const auto lodHeuristic = authoredStage->GetPrimAtPath(pxr::SdfPath(
"/PointCloud/LodHeuristics/" + TileName() + "/ScreenSize"));
pxr::VtArray<float> thresholds;
if (!lodHeuristic ||
!lodHeuristic.GetAttribute(pxr::TfToken("thresholds"))
.Get(&thresholds)) {
return false;
}

const auto stage = pxr::UsdStage::Open(
outputPath.string(), pxr::UsdStage::LoadNone);
if (!stage) {
return false;
}
const auto loadNoneLoadedLods = CountLoadedLods(stage);
if (loadNoneLoadedLods != 0) {
return false;
}

stage->Load(pxr::SdfPath(LodPath(1)));
const auto selectiveLoadedLods = CountLoadedLods(stage);
const auto selectiveLoadedPoints = LoadedPointCount(stage);
if (selectiveLoadedLods != 1 || selectiveLoadedPoints != 2) {
return false;
}

stage->Load();
const auto allLoadedLods = CountLoadedLods(stage);
const auto allLoadedPoints = LoadedPointCount(stage);
if (allLoadedLods != 3 || allLoadedPoints != 6) {
return false;
}

std::cout << "root\tauthored_lods\tauthored_payloads\tload_none_loaded_lods\t"
"selective_loaded_lods\tselective_loaded_points\t"
"load_all_loaded_lods\tload_all_points\tdefault_index\tthresholds\n"
<< outputPath.string() << '\t' << authoredLods << '\t'
<< authoredPayloads << '\t' << loadNoneLoadedLods << '\t'
<< selectiveLoadedLods << '\t'
<< selectiveLoadedPoints << '\t' << allLoadedLods << '\t'
<< allLoadedPoints << '\t' << defaultValue << '\t';
for (std::size_t index = 0; index < thresholds.size(); ++index) {
if (index != 0) {
std::cout << ',';
}
std::cout << thresholds[index];
}
std::cout << '\n';
return true;
}

} // namespace

int main(int argc, char** argv) {
ProbeOptions options;
if (!ParseOptions(argc, argv, options)) {
return 2;
}
if (!CreateFixture(options.outputPath) || !RunProbe(options.outputPath)) {
std::cerr << "runtime LOD payload probe failed\n";
return 1;
}
return 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
if(NOT DEFINED probe OR NOT DEFINED output)
message(FATAL_ERROR "runtime LOD payload probe test arguments are incomplete")
endif()

file(REMOVE_RECURSE "${output}")
execute_process(
COMMAND "${probe}" --output "${output}/PointCloud.usda"
RESULT_VARIABLE probe_result
OUTPUT_VARIABLE probe_output
ERROR_VARIABLE probe_error)
if(NOT probe_result EQUAL 0)
message(FATAL_ERROR
"runtime LOD payload probe failed: ${probe_output}${probe_error}")
endif()

string(REPLACE "\r\n" "\n" normalized_probe_output "${probe_output}")
string(STRIP "${normalized_probe_output}" normalized_probe_output)
string(REPLACE "\n" ";" probe_lines "${normalized_probe_output}")
list(GET probe_lines -1 probe_row)
string(REPLACE "\t" ";" probe_fields "${probe_row}")
list(LENGTH probe_fields probe_field_count)
if(NOT probe_field_count EQUAL 10)
message(FATAL_ERROR
"probe output has unexpected field count: ${probe_row}")
endif()
list(GET probe_fields 1 authored_lods)
list(GET probe_fields 2 authored_payloads)
list(GET probe_fields 3 load_none_loaded_lods)
list(GET probe_fields 4 selective_loaded_lods)
list(GET probe_fields 5 selective_loaded_points)
list(GET probe_fields 6 load_all_loaded_lods)
list(GET probe_fields 7 load_all_points)
list(GET probe_fields 8 default_index)
list(GET probe_fields 9 thresholds)
if(NOT authored_lods STREQUAL "3" OR
NOT authored_payloads STREQUAL "3" OR
NOT load_none_loaded_lods STREQUAL "0" OR
NOT selective_loaded_lods STREQUAL "1" OR
NOT selective_loaded_points STREQUAL "2" OR
NOT load_all_loaded_lods STREQUAL "3" OR
NOT load_all_points STREQUAL "6" OR
NOT default_index STREQUAL "2" OR
NOT thresholds STREQUAL "0.25,0.1")
message(FATAL_ERROR
"probe output has unexpected composition results: ${probe_row}")
endif()

file(MAKE_DIRECTORY "${output}/relative")
execute_process(
COMMAND "${probe}" --output PointCloud.usda
WORKING_DIRECTORY "${output}/relative"
RESULT_VARIABLE relative_probe_result
OUTPUT_VARIABLE relative_probe_output
ERROR_VARIABLE relative_probe_error)
if(NOT relative_probe_result EQUAL 0 OR
NOT EXISTS "${output}/relative/PointCloud.usda")
message(FATAL_ERROR
"relative output probe failed: "
"${relative_probe_output}${relative_probe_error}")
endif()
Loading