From b1b8ef987f86c4c660d7b98b5c9cefbf4e4b8c25 Mon Sep 17 00:00:00 2001 From: snkmcb Date: Sat, 5 Sep 2026 00:57:09 +0900 Subject: [PATCH 1/2] research: measure OpenUSD LOD payload composition --- docs/roadmap/implementation-status.md | 8 +- docs/roadmap/infrastructure-maturity.md | 7 +- docs/roadmap/streaming-and-tiling.md | 16 ++ .../benchmarks/CMakeLists.txt | 12 + .../benchmarks/runtime_lod_payload_probe.cpp | 235 ++++++++++++++++++ .../test_runtime_lod_payload_probe.cmake | 46 ++++ 6 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp create mode 100644 libs/usd-pointcloud-authoring/benchmarks/test_runtime_lod_payload_probe.cmake diff --git a/docs/roadmap/implementation-status.md b/docs/roadmap/implementation-status.md index fc027e6..28a3c7f 100644 --- a/docs/roadmap/implementation-status.md +++ b/docs/roadmap/implementation-status.md @@ -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 diff --git a/docs/roadmap/infrastructure-maturity.md b/docs/roadmap/infrastructure-maturity.md index 3443957..8127603 100644 --- a/docs/roadmap/infrastructure-maturity.md +++ b/docs/roadmap/infrastructure-maturity.md @@ -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 diff --git a/docs/roadmap/streaming-and-tiling.md b/docs/roadmap/streaming-and-tiling.md index 819e2da..904871e 100644 --- a/docs/roadmap/streaming-and-tiling.md +++ b/docs/roadmap/streaming-and-tiling.md @@ -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 diff --git a/libs/usd-pointcloud-authoring/benchmarks/CMakeLists.txt b/libs/usd-pointcloud-authoring/benchmarks/CMakeLists.txt index e59ab7f..b23a581 100644 --- a/libs/usd-pointcloud-authoring/benchmarks/CMakeLists.txt +++ b/libs/usd-pointcloud-authoring/benchmarks/CMakeLists.txt @@ -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 @@ -50,4 +55,11 @@ if(USDGEO_BUILD_TESTS) -Dfixture_generator=$ -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=$ + -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() \ No newline at end of file diff --git a/libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp b/libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp new file mode 100644 index 0000000..c4b8ad4 --- /dev/null +++ b/libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp @@ -0,0 +1,235 @@ +#include "usdgeo/PointCloudLayer.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +struct ProbeOptions { + std::filesystem::path outputPath; +}; + +void PrintUsage() { + std::cerr << "Usage: usdPointCloudAuthoring_runtime_lod_payload_probe " + "--output \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(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; + std::filesystem::create_directories(outputPath.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(index); + item.pointCount = 3 - index; + item.bounds = bounds; + item.sourceRange = {0, item.pointCount}; + item.spacing = static_cast(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 = outputPath.parent_path() / "payloads"; + const auto stage = usdgeo::PointCloudLayer::CreateStage(); + if (!stage) { + return false; + } + usdgeo::PointCloudPayloadOptions options; + options.directory = payloadDirectory.string(); + options.rootLayerPath = outputPath.string(); + if (!usdgeo::AuthorPointCloudTiledAssetWithPayloads( + stage, "/PointCloud", {tile}, options)) { + return false; + } + stage->GetRootLayer()->SetIdentifier(outputPath.generic_string()); + if (!stage->GetRootLayer()->Export(outputPath.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 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 || CountLoadedLods(stage) != 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' << 0 << '\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; +} diff --git a/libs/usd-pointcloud-authoring/benchmarks/test_runtime_lod_payload_probe.cmake b/libs/usd-pointcloud-authoring/benchmarks/test_runtime_lod_payload_probe.cmake new file mode 100644 index 0000000..5da0dea --- /dev/null +++ b/libs/usd-pointcloud-authoring/benchmarks/test_runtime_lod_payload_probe.cmake @@ -0,0 +1,46 @@ +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() From a9deb98fcffc03aeabf0fa92ecba09bbaa006b1d Mon Sep 17 00:00:00 2001 From: snkmcb Date: Sat, 5 Sep 2026 01:01:09 +0900 Subject: [PATCH 2/2] fix: harden runtime LOD payload probe paths --- .../benchmarks/runtime_lod_payload_probe.cpp | 26 ++++++++++++++----- .../test_runtime_lod_payload_probe.cmake | 14 ++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp b/libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp index c4b8ad4..c9ac22c 100644 --- a/libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp +++ b/libs/usd-pointcloud-authoring/benchmarks/runtime_lod_payload_probe.cpp @@ -104,10 +104,17 @@ std::size_t AuthoredPayloadCount(const pxr::SdfLayerHandle& rootLayer) { bool CreateFixture(const std::filesystem::path& outputPath) { std::error_code error; - std::filesystem::create_directories(outputPath.parent_path(), 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; @@ -139,20 +146,20 @@ bool CreateFixture(const std::filesystem::path& outputPath) { tile.levels.push_back(MakeAsset(reference, bounds, 3 - index)); } - const auto payloadDirectory = outputPath.parent_path() / "payloads"; + 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 = outputPath.string(); + options.rootLayerPath = absoluteOutputPath.string(); if (!usdgeo::AuthorPointCloudTiledAssetWithPayloads( stage, "/PointCloud", {tile}, options)) { return false; } - stage->GetRootLayer()->SetIdentifier(outputPath.generic_string()); - if (!stage->GetRootLayer()->Export(outputPath.string())) { + stage->GetRootLayer()->SetIdentifier(absoluteOutputPath.generic_string()); + if (!stage->GetRootLayer()->Export(absoluteOutputPath.string())) { return false; } return true; @@ -185,7 +192,11 @@ bool RunProbe(const std::filesystem::path& outputPath) { const auto stage = pxr::UsdStage::Open( outputPath.string(), pxr::UsdStage::LoadNone); - if (!stage || CountLoadedLods(stage) != 0) { + if (!stage) { + return false; + } + const auto loadNoneLoadedLods = CountLoadedLods(stage); + if (loadNoneLoadedLods != 0) { return false; } @@ -207,7 +218,8 @@ bool RunProbe(const std::filesystem::path& outputPath) { "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' << 0 << '\t' << selectiveLoadedLods << '\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) { diff --git a/libs/usd-pointcloud-authoring/benchmarks/test_runtime_lod_payload_probe.cmake b/libs/usd-pointcloud-authoring/benchmarks/test_runtime_lod_payload_probe.cmake index 5da0dea..a65c884 100644 --- a/libs/usd-pointcloud-authoring/benchmarks/test_runtime_lod_payload_probe.cmake +++ b/libs/usd-pointcloud-authoring/benchmarks/test_runtime_lod_payload_probe.cmake @@ -44,3 +44,17 @@ if(NOT authored_lods STREQUAL "3" OR 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()