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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to this project are documented here.

### Added

- Owned, recoverable tile-spool workspaces through
`PointCloudPayloadOptions::spoolDirectory`; interrupted marked workspaces
are recovered on the next tiled authoring run, and the converter scopes its
workspace to the conversion transaction.
- Local `.copc` and `.copc.laz` inputs in `usd-pointcloud-convert`, including
deterministic generated-cache publication and reuse.
- `PointCloudPayloadOptions::owner`, `PointCloudPayloadOwner`, and a
Expand Down
7 changes: 4 additions & 3 deletions docs/roadmap/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,10 @@ remain be explicit.
- [x] Materialize generated-cache hits as a generation named by the cache
entry, reusing an intact copy without writing and replacing a damaged
one
- [ ] Give tile spools an owned, recoverable working location instead of
timestamped directories in the system temporary directory, which an
interrupted process leaves behind
- [x] Give tile spools an owned, recoverable working location instead of
timestamped directories in the system temporary directory. The authoring
options select the workspace, marked leftovers are recovered on the next
invocation, and conversion ties the workspace to its transaction marker.

#### Research - runtime streaming (no release gate)

Expand Down
8 changes: 6 additions & 2 deletions docs/roadmap/streaming-and-tiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,12 @@ The spool system must define:
- detection of incomplete spools;
- deterministic iteration order.

Temporary output is isolated in a dedicated working directory and removed on
successful completion unless a debug-retention option is enabled.
Temporary output is isolated in a caller-owned dedicated working directory and
removed on completion. The authoring path writes a marker before creating tile
spools; a marked directory left by an interrupted process is removed before
the next run, while an unmarked existing directory is refused. The converter
places this workspace below its transaction marker so transaction recovery
owns it as well.

## 7. Payload output requirements

Expand Down
12 changes: 12 additions & 0 deletions libs/usd-pointcloud-authoring/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ beside it before the superseded one is removed. Nothing outside the owner's
directory is touched. The layout is stated in the
[file-format argument contract](../../docs/architecture/FILE_FORMAT_ARGUMENTS.md#generated-payload-ownership).

`PointCloudPayloadOptions::spoolDirectory` optionally names the exclusive
working directory for tiled stream spools. When it is empty, the authoring
library creates a `.spool` directory below the payload directory, scoped by
the payload owner when one is present. The directory contains a marker before
spooling starts; a later invocation removes a marked directory left by an
interrupted run, and successful or failed authoring removes it on exit. An
existing directory without the marker is refused so authoring never deletes
unrelated files.

## Coordinate-space assumptions

Input positions are **source-space** `usdgeo::Vec3d`. This module applies
Expand Down Expand Up @@ -212,6 +221,9 @@ ctest --test-dir build/cy2026-windows-x86_64-py313-usd -C Release `
by source tile, and reconstructs one tile at a time before payload authoring.
The router overload also accepts a planned `PointBudgetTileRouter`, allowing
adaptive leaf plans to use the same bounded spool and payload path.
Spools live in the caller-owned `spoolDirectory` or an owner-scoped
`.spool` directory below the payload directory; marked leftovers are
recovered on the next invocation.
- Large-corpus RSS measurement for generated, LAS, and LAZ inputs is available
through the explicit `usdPointCloudAuthoring_stream_benchmark` target. It is
disabled by default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ struct PointCloudPayloadOptions {
// superseded generations of the same owner are removed. Nothing outside
// the owner's directory is touched, and the value is never persisted.
std::string owner;
std::string spoolDirectory;
};

// The payload owner for a FileFormat read: which source the layer reads and
Expand Down
89 changes: 74 additions & 15 deletions libs/usd-pointcloud-authoring/src/PointCloudStreaming.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
#include "usdgeo/PointCloudLayer.h"

#include "TiledPayloadAuthoring.h"
#include "usdgeo/CacheKey.h"

#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <map>
#include <memory>
#include <optional>
Expand Down Expand Up @@ -208,23 +209,66 @@ bool PrepareData(const usdpointcloud::SpoolSchema& schema,
return true;
}

constexpr char kSpoolMarker[] = "USDGEO_SPOOL_WORKSPACE_V1";

std::filesystem::path MakeSpoolDirectory(
const PointCloudPayloadOptions& options,
std::vector<Diagnostic>& diagnostics) {
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
const auto base = std::filesystem::temp_directory_path() /
("usdgeo_point_spool_" + std::to_string(stamp));
auto directory = options.spoolDirectory.empty()
? std::filesystem::path(options.directory) / ".spool"
: std::filesystem::path(options.spoolDirectory);
if (options.spoolDirectory.empty() && !options.owner.empty()) {
directory /= usdgeo::StableCacheKey({{"payloadOwner", options.owner}});
}
if (directory.empty()) {
AddError(diagnostics, DiagnosticCode::DecodeFailure,
"unable to determine point spool directory");
return {};
}
std::error_code error;
for (std::uint32_t suffix = 0; suffix < 1000; ++suffix) {
const auto directory = suffix == 0
? base
: std::filesystem::path(
base.string() + "_" + std::to_string(suffix));
if (std::filesystem::create_directory(directory, error)) return directory;
error.clear();
if (std::filesystem::exists(directory, error)) {
if (error || !std::filesystem::is_directory(directory, error)) {
AddError(diagnostics, DiagnosticCode::DecodeFailure,
"point spool directory is not a directory");
return {};
}
const auto marker = directory / ".usdgeo-spool";
std::ifstream markerFile(marker, std::ios::binary);
std::string markerValue;
const auto markerIsFile =
std::filesystem::is_regular_file(marker, error) && !error;
const auto markerIsValid = markerIsFile &&
static_cast<bool>(markerFile) &&
static_cast<bool>(std::getline(
markerFile, markerValue)) &&
markerValue == kSpoolMarker;
markerFile.close();
if (!markerIsValid) {
AddError(diagnostics, DiagnosticCode::DecodeFailure,
"point spool directory is not recoverable");
return {};
}
std::filesystem::remove_all(directory, error);
if (error) {
AddError(diagnostics, DiagnosticCode::DecodeFailure,
"unable to recover point spool directory");
return {};
}
}
std::filesystem::create_directories(directory, error);
if (error) {
AddError(diagnostics, DiagnosticCode::DecodeFailure,
"unable to create point spool directory");
return {};
}
std::ofstream marker(directory / ".usdgeo-spool", std::ios::binary);
if (!marker || !(marker << kSpoolMarker << '\n')) {
std::filesystem::remove_all(directory, error);
AddError(diagnostics, DiagnosticCode::DecodeFailure,
"unable to initialize point spool directory");
return {};
}
AddError(diagnostics, DiagnosticCode::DecodeFailure,
"unable to create point spool directory");
return {};
return directory;
}

struct TileSpool {
Expand Down Expand Up @@ -252,7 +296,15 @@ bool AuthorPointCloudTiledAssetFromStream(
return false;
}

const auto spoolDirectory = MakeSpoolDirectory(diagnostics);
std::error_code payloadDirectoryError;
const auto payloadDirectoryExisted = std::filesystem::exists(
std::filesystem::path(options.directory), payloadDirectoryError);
if (payloadDirectoryError) {
AddError(diagnostics, DiagnosticCode::DecodeFailure,
"unable to inspect point payload directory");
return false;
}
const auto spoolDirectory = MakeSpoolDirectory(options, diagnostics);
if (spoolDirectory.empty()) return false;
std::map<std::string, TileSpool> spools;
std::size_t bufferedBytes = 0;
Expand All @@ -263,6 +315,13 @@ bool AuthorPointCloudTiledAssetFromStream(
std::vector<Diagnostic> cleanupDiagnostics;
usdpointcloud::RemoveSpoolDirectory(spoolDirectory, cleanupDiagnostics);
payloads.Rollback();
if (!payloadDirectoryExisted) {
std::error_code removeError;
std::filesystem::remove(
std::filesystem::path(options.directory) / ".spool",
removeError);
std::filesystem::remove(options.directory, removeError);
}
};
const auto isCancelled = [&]() {
return options.isCancelled && options.isCancelled();
Expand Down
82 changes: 34 additions & 48 deletions libs/usd-pointcloud-authoring/tests/test_usd_authoring.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,47 +125,24 @@ std::map<std::string, std::string> ReadDirectoryFiles(
return files;
}

std::set<std::filesystem::path> ListPointSpoolDirectories() {
std::set<std::filesystem::path> directories;
bool HasPointSpoolFile(const std::filesystem::path& directory) {
std::error_code error;
const auto temporaryDirectory = std::filesystem::temp_directory_path(error);
Check(!error);
if (!std::filesystem::is_directory(directory, error)) {
Check(!error);
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(
temporaryDirectory, error)) {
directory, error)) {
Check(!error);
const auto name = entry.path().filename().string();
const auto isDirectory = entry.is_directory(error);
Check(!error);
if (isDirectory &&
name.rfind("usdgeo_point_spool_", 0) == 0) {
directories.insert(entry.path());
if (entry.is_regular_file(error) && name.rfind("tile_", 0) == 0 &&
entry.path().extension() == ".bin") {
return true;
}
Check(!error);
error.clear();
}
Check(!error);
return directories;
}

bool HasNewPointSpoolFile(
const std::set<std::filesystem::path>& existingDirectories) {
for (const auto& directory : ListPointSpoolDirectories()) {
if (existingDirectories.count(directory) != 0) continue;
std::error_code error;
for (const auto& entry : std::filesystem::directory_iterator(
directory, error)) {
Check(!error);
const auto name = entry.path().filename().string();
const auto isRegularFile = entry.is_regular_file(error);
Check(!error);
if (isRegularFile &&
name.rfind("tile_", 0) == 0 &&
entry.path().extension() == ".bin") {
return true;
}
error.clear();
}
Check(!error);
}
return false;
}

Expand Down Expand Up @@ -200,9 +177,9 @@ class GeneratedPointStream final : public usdpointcloud::PointStream {
public:
GeneratedPointStream(
std::size_t pointCount,
std::set<std::filesystem::path> existingSpoolDirectories)
std::filesystem::path spoolDirectory)
: pointCount_(pointCount),
existingSpoolDirectories_(std::move(existingSpoolDirectories)) {}
spoolDirectory_(std::move(spoolDirectory)) {}

usdpointcloud::PointStreamStatus ReadNext(
usdpointcloud::PointChunk& chunk,
Expand All @@ -212,8 +189,7 @@ class GeneratedPointStream final : public usdpointcloud::PointStream {
if (index_ == pointCount_) {
return usdpointcloud::PointStreamStatus::End;
}
sawSpoolFile_ = sawSpoolFile_ ||
HasNewPointSpoolFile(existingSpoolDirectories_);
sawSpoolFile_ = sawSpoolFile_ || HasPointSpoolFile(spoolDirectory_);
const auto tileIndex = index_ % 32;
const auto pointInTile = index_ / 32;
data.positions = {{static_cast<double>(tileIndex * 128 + 1),
Expand All @@ -231,7 +207,7 @@ class GeneratedPointStream final : public usdpointcloud::PointStream {
private:
std::size_t pointCount_ = 0;
std::size_t index_ = 0;
std::set<std::filesystem::path> existingSpoolDirectories_;
std::filesystem::path spoolDirectory_;
bool sawSpoolFile_ = false;
};

Expand Down Expand Up @@ -1080,17 +1056,25 @@ void TestGeneratedStreamTiledPayloadAuthoring() {
usdgeo::GeoReference reference;
reference.epsgCode = 26910;

const auto spoolDirectoriesBefore = ListPointSpoolDirectories();
GeneratedPointStream stream(131072, spoolDirectoriesBefore);
const auto payloadDirectory =
std::filesystem::temp_directory_path() / "usd_geo_generated_payloads";
const auto rootLayerPath = payloadDirectory / "PointCloud.usda";
const auto spoolDirectory = payloadDirectory / "spool-work";
std::filesystem::remove_all(payloadDirectory);
std::filesystem::create_directories(spoolDirectory);
{
std::ofstream marker(spoolDirectory / ".usdgeo-spool", std::ios::binary);
marker << "USDGEO_SPOOL_WORKSPACE_V1\n";
std::ofstream stale(spoolDirectory / "tile_stale.bin");
stale << "stale";
}
GeneratedPointStream stream(131072, spoolDirectory);
std::vector<usdgeo::Diagnostic> diagnostics;
std::vector<usdpointcloud::PointTileManifestEntry> manifestEntries;
const usdgeo::PointCloudPayloadOptions options{
usdgeo::PointCloudPayloadOptions options{
payloadDirectory.string(), rootLayerPath.string(), 1024, {}, {},
&manifestEntries};
options.spoolDirectory = spoolDirectory.string();
Check(usdgeo::AuthorPointCloudTiledAssetFromStream(
layer.operator->(), "/PointCloud", stream, reference, {128.0, 0},
options, diagnostics));
Expand All @@ -1112,7 +1096,7 @@ void TestGeneratedStreamTiledPayloadAuthoring() {
Check(layer->GetPrimAtPath(pxr::SdfPath(
"/PointCloud/Tiles/Tile_L0_p31_p0_p0")) != nullptr);
Check(stream.SawSpoolFile());
Check(ListPointSpoolDirectories() == spoolDirectoriesBefore);
Check(!std::filesystem::exists(spoolDirectory));
std::filesystem::remove_all(payloadDirectory);
}

Expand All @@ -1131,20 +1115,21 @@ void TestStreamCancellationCleansSpools() {
const auto payloadDirectory =
std::filesystem::temp_directory_path() / "usd_geo_cancelled_payloads";
std::filesystem::remove_all(payloadDirectory);
const auto spoolDirectoriesBefore = ListPointSpoolDirectories();
const auto spoolDirectory = payloadDirectory / "spool-work";
int cancellationChecks = 0;
std::vector<usdgeo::Diagnostic> diagnostics;
const usdgeo::PointCloudPayloadOptions options{
usdgeo::PointCloudPayloadOptions options{
payloadDirectory.string(),
(payloadDirectory / "PointCloud.usda").string(),
1,
[&cancellationChecks]() { return ++cancellationChecks >= 3; }};
options.spoolDirectory = spoolDirectory.string();

Check(!usdgeo::AuthorPointCloudTiledAssetFromStream(
layer.operator->(), "/PointCloud", stream, reference, {1.0, 0},
options, diagnostics));
Check(!diagnostics.empty());
Check(ListPointSpoolDirectories() == spoolDirectoriesBefore);
Check(!std::filesystem::exists(spoolDirectory));
Check(!std::filesystem::exists(payloadDirectory));
}

Expand All @@ -1165,20 +1150,21 @@ void TestStreamCancellationDuringSpoolReadCleansSpools() {
std::filesystem::temp_directory_path() /
"usd_geo_cancelled_spool_read_payloads";
std::filesystem::remove_all(payloadDirectory);
const auto spoolDirectoriesBefore = ListPointSpoolDirectories();
const auto spoolDirectory = payloadDirectory / "spool-work";
int cancellationChecks = 0;
std::vector<usdgeo::Diagnostic> diagnostics;
const usdgeo::PointCloudPayloadOptions options{
usdgeo::PointCloudPayloadOptions options{
payloadDirectory.string(),
(payloadDirectory / "PointCloud.usda").string(),
1,
[&cancellationChecks]() { return ++cancellationChecks >= 6; }};
options.spoolDirectory = spoolDirectory.string();

Check(!usdgeo::AuthorPointCloudTiledAssetFromStream(
layer.operator->(), "/PointCloud", stream, reference, {1.0, 0},
options, diagnostics));
Check(!diagnostics.empty());
Check(ListPointSpoolDirectories() == spoolDirectoriesBefore);
Check(!std::filesystem::exists(spoolDirectory));
Check(!std::filesystem::exists(payloadDirectory));
}

Expand Down
6 changes: 5 additions & 1 deletion libs/usd-pointcloud-tiling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ reject missing or truncated footers. Attribute values are serialized using the
scalar types declared by the schema. The writer buffers records up to its
configured byte threshold before flushing; streaming callers may combine
multiple writers under a total working-set budget. Callers own the working
directory and can remove it with `RemoveSpoolDirectory`.
directory and can remove it with `RemoveSpoolDirectory`. The OpenUSD
authoring bridge places stream spools in a caller-selected directory or an
owner-scoped `.spool` directory, marks it before writing, and removes a
marked leftover before the next run so an interrupted process can be
recovered without scanning the system temporary directory.

`PointTileManifest` records the tile ID, LOD level, source bounds, point count,
and portable payload path for generated tile assets. Its v1 serializer sorts
Expand Down
Loading
Loading