From ba023e3d26bb9a5492b5bc9b26c7adc956c102d5 Mon Sep 17 00:00:00 2001 From: snkmcb Date: Wed, 16 Sep 2026 01:00:46 +0900 Subject: [PATCH 1/2] test: cover hostile import limits --- .../tests/test_gaussian_sog_reader.cpp | 21 ++++++++++++++++++ .../tests/fixtures/shared-limit-v2.spz | Bin 0 -> 36 bytes .../tests/test_gaussian_spz_reader.cpp | 6 +++++ .../gaussian-spz/tools/generate_fixtures.py | 5 +++++ 4 files changed, 32 insertions(+) create mode 100644 plugins/gaussian-spz/tests/fixtures/shared-limit-v2.spz diff --git a/plugins/gaussian-sog/tests/test_gaussian_sog_reader.cpp b/plugins/gaussian-sog/tests/test_gaussian_sog_reader.cpp index b018c09..c2d4ec2 100644 --- a/plugins/gaussian-sog/tests/test_gaussian_sog_reader.cpp +++ b/plugins/gaussian-sog/tests/test_gaussian_sog_reader.cpp @@ -334,6 +334,26 @@ void TestJsonTokenLimit() CHECK(error.find("token limit") != std::string::npos); } +void TestJsonDepthLimit() +{ + std::string accepted; + accepted.append(gssog::kJsonMaxDepth, '['); + accepted += '0'; + accepted.append(gssog::kJsonMaxDepth, ']'); + + gssog::JsonValue value; + std::string error; + CHECK(gssog::ParseJson(accepted.data(), accepted.size(), &value, &error)); + + std::string rejected; + rejected.append(gssog::kJsonMaxDepth + 1, '['); + rejected += '0'; + rejected.append(gssog::kJsonMaxDepth + 1, ']'); + error.clear(); + CHECK(!gssog::ParseJson(rejected.data(), rejected.size(), &value, &error)); + CHECK(error.find("nesting deeper") != std::string::npos); +} + } // namespace int main() @@ -347,6 +367,7 @@ int main() TestMetadataOnly(); TestMalformedContainers(); TestJsonTokenLimit(); + TestJsonDepthLimit(); if (failures != 0) { std::cerr << failures << " SOG reader check(s) failed\n"; diff --git a/plugins/gaussian-spz/tests/fixtures/shared-limit-v2.spz b/plugins/gaussian-spz/tests/fixtures/shared-limit-v2.spz new file mode 100644 index 0000000000000000000000000000000000000000..2d351ac787dee13c2c994a82fcdb59022d4a8a9c GIT binary patch literal 36 ncmb2|=3oGW|DTKb`4W;65)wpKB_%vaOkiLrRMMLy0F(p(vsDS3 literal 0 HcmV?d00001 diff --git a/plugins/gaussian-spz/tests/test_gaussian_spz_reader.cpp b/plugins/gaussian-spz/tests/test_gaussian_spz_reader.cpp index 5a70ce0..9c9525d 100644 --- a/plugins/gaussian-spz/tests/test_gaussian_spz_reader.cpp +++ b/plugins/gaussian-spz/tests/test_gaussian_spz_reader.cpp @@ -204,6 +204,10 @@ void TestHeaderOnlySemantics() CHECK(!reader.ReadHeader( Fixture("count-exceeds-stream-v2.spz"), &header, &error)); CHECK(HasCode(error, gsspz::diag::kTruncatedContainer)); + error.clear(); + CHECK(!reader.ReadHeader( + Fixture("shared-limit-v2.spz"), &header, &error)); + CHECK(HasCode(error, gsspz::diag::kImportLimitExceeded)); } // CanRead is signature-only (§7.6): every structurally identifiable SPZ @@ -220,6 +224,7 @@ void TestCanRead() CHECK(reader.CanRead(Fixture("version-5.spz"))); CHECK(reader.CanRead(Fixture("empty-points-v2.spz"))); CHECK(reader.CanRead(Fixture("huge-count-v2.spz"))); + CHECK(reader.CanRead(Fixture("shared-limit-v2.spz"))); CHECK(reader.CanRead(Fixture("sh-degree-5-v2.spz"))); CHECK(reader.CanRead(Fixture("truncated-payload-v2.spz"))); CHECK(reader.CanRead(Fixture("truncated-deflate-v2.spz"))); @@ -266,6 +271,7 @@ int main() TestReadFailure("version-5.spz", gsspz::diag::kUnsupportedVersion); TestReadFailure("empty-points-v2.spz", gsspz::diag::kEmptyPointSet); TestReadFailure("huge-count-v2.spz", gsspz::diag::kInvalidPointCount); + TestReadFailure("shared-limit-v2.spz", gsspz::diag::kImportLimitExceeded); TestReadFailure("sh-degree-5-v2.spz", gsspz::diag::kInvalidShDegree); TestReadFailure( "count-exceeds-stream-v2.spz", gsspz::diag::kTruncatedContainer); diff --git a/plugins/gaussian-spz/tools/generate_fixtures.py b/plugins/gaussian-spz/tools/generate_fixtures.py index 90aca9a..7e39363 100644 --- a/plugins/gaussian-spz/tools/generate_fixtures.py +++ b/plugins/gaussian-spz/tools/generate_fixtures.py @@ -154,6 +154,11 @@ def invalid_fixtures() -> None: write("version-5.spz", gzip_member(spz_stream(5, 1, 0))) write("empty-points-v2.spz", gzip_member(spz_header(2, 0, 0))) write("huge-count-v2.spz", gzip_member(spz_header(2, 0x80000000, 0))) + # The count is below SPZ's format maximum but above the shared importer + # ceiling, so the reader must report the shared limit rather than a + # malformed container or a truncated payload. + write("shared-limit-v2.spz", gzip_member( + spz_header(2, 8_000_001, 0))) write("sh-degree-5-v2.spz", gzip_member( spz_header(2, 1, 5) + payload(19))) From b1d2966b3a801a65e0bb51c7b81ebcabc4137000 Mon Sep 17 00:00:00 2001 From: snkmcb Date: Wed, 16 Sep 2026 01:16:45 +0900 Subject: [PATCH 2/2] style: format C and C++ sources --- .../openstrata/gs/GaussianDiagnostics.h | 3 +- .../include/openstrata/gs/GaussianMath.h | 24 +-- .../include/openstrata/gs/GaussianSizeMath.h | 14 +- .../openstrata/gs/testing/CloudContract.h | 20 ++- .../openstrata/gs/testing/DecoderTestKit.h | 102 ++++++------ .../gaussian-core/src/GaussianImportStats.cpp | 27 +-- libs/gaussian-core/src/GaussianMath.cpp | 78 ++++----- libs/gaussian-core/tests/test_decoder_kit.cpp | 120 +++++++------ .../tests/test_gaussian_math.cpp | 29 ++-- .../openstrata/gs/usd/GaussianLayerWriter.h | 26 ++- libs/gaussian-usd/src/GaussianLayerWriter.cpp | 116 ++++++------- .../tests/test_gaussian_layer_writer.cpp | 20 +-- .../src/GaussianPlyFileFormat.cpp | 130 +++++++-------- .../gaussian-ply/src/GaussianPlyFileFormat.h | 27 ++- .../gaussian-ply/src/io/GaussianPlyDecoder.h | 18 +- .../src/io/GaussianPlyImportOptions.cpp | 49 +++--- .../src/io/GaussianPlyImportOptions.h | 17 +- plugins/gaussian-ply/src/io/PlyReader.cpp | 144 +++++++++------- plugins/gaussian-ply/src/io/PlyReader.h | 18 +- .../tests/test_gaussian_ply_decoder.cpp | 141 ++++++++-------- .../src/GaussianSogFileFormat.cpp | 157 +++++++++--------- .../gaussian-sog/src/GaussianSogFileFormat.h | 29 ++-- .../src/io/GaussianSogDecoder.cpp | 103 ++++++------ .../gaussian-sog/src/io/GaussianSogDecoder.h | 20 +-- plugins/gaussian-sog/src/io/SogJson.h | 43 +++-- plugins/gaussian-sog/src/io/SogReader.h | 27 ++- .../tests/test_gaussian_sog_decoder.cpp | 74 ++++----- .../tests/test_gaussian_sog_reader.cpp | 73 ++++---- .../src/GaussianSpzFileFormat.cpp | 120 +++++++------ .../gaussian-spz/src/GaussianSpzFileFormat.h | 27 ++- .../src/io/GaussianSpzDecoder.cpp | 122 +++++++------- .../gaussian-spz/src/io/GaussianSpzDecoder.h | 18 +- plugins/gaussian-spz/src/io/SpzReader.h | 24 +-- .../tests/test_gaussian_spz_decoder.cpp | 75 +++++---- .../tests/test_gaussian_spz_reader.cpp | 73 ++++---- tests/equivalence/equivalence_common.h | 34 ++-- tests/equivalence/test_equivalence.cpp | 91 +++++----- tests/equivalence/test_sog_equivalence.cpp | 74 ++++----- 38 files changed, 1121 insertions(+), 1186 deletions(-) diff --git a/libs/gaussian-core/include/openstrata/gs/GaussianDiagnostics.h b/libs/gaussian-core/include/openstrata/gs/GaussianDiagnostics.h index 8f740c8..bef43b9 100644 --- a/libs/gaussian-core/include/openstrata/gs/GaussianDiagnostics.h +++ b/libs/gaussian-core/include/openstrata/gs/GaussianDiagnostics.h @@ -17,7 +17,8 @@ namespace openstrata::gs { // and makes the missing code obvious in a bug report. inline constexpr const char* kUnspecifiedDiagnosticCode = "GS-E000"; -inline std::string FormatDiagnostic(const char* code, const std::string& message) +inline std::string FormatDiagnostic(const char* code, + const std::string& message) { if (!code) { code = kUnspecifiedDiagnosticCode; diff --git a/libs/gaussian-core/include/openstrata/gs/GaussianMath.h b/libs/gaussian-core/include/openstrata/gs/GaussianMath.h index 7534c1b..10057a3 100644 --- a/libs/gaussian-core/include/openstrata/gs/GaussianMath.h +++ b/libs/gaussian-core/include/openstrata/gs/GaussianMath.h @@ -14,11 +14,9 @@ bool DecodeLogScale(const Float3& stored, Float3* actual) noexcept; // Returns false only for non-finite input. A near-zero quaternion is replaced // with identity and reported through replacedWithIdentity. -bool NormalizeQuaternion( - const Quaternion& stored, - Quaternion* normalized, - bool* replacedWithIdentity = nullptr, - bool* changed = nullptr) noexcept; +bool NormalizeQuaternion(const Quaternion& stored, Quaternion* normalized, + bool* replacedWithIdentity = nullptr, + bool* changed = nullptr) noexcept; // coefficientCount includes the DC term. Valid layouts are 1, 4, 9, 16, ... bool InferShDegree(std::size_t coefficientCount, int* degree) noexcept; @@ -38,15 +36,11 @@ void FlipYZAxes(GaussianCloudData* cloud) noexcept; // kit compares against, so a stage need not be authored to know the extent a // cloud will produce. Returns false when count is zero or a bound leaves // float range; the outputs are untouched on failure. -bool ComputeCloudExtent( - const Float3* positions, - const Float3* scales, - std::size_t count, - Float3* outMinimum, - Float3* outMaximum) noexcept; - -bool ValidateGaussianCloud( - const GaussianCloudData& cloud, - std::string* error = nullptr) noexcept; +bool ComputeCloudExtent(const Float3* positions, const Float3* scales, + std::size_t count, Float3* outMinimum, + Float3* outMaximum) noexcept; + +bool ValidateGaussianCloud(const GaussianCloudData& cloud, + std::string* error = nullptr) noexcept; } // namespace openstrata::gs diff --git a/libs/gaussian-core/include/openstrata/gs/GaussianSizeMath.h b/libs/gaussian-core/include/openstrata/gs/GaussianSizeMath.h index e21b47b..1fa56fc 100644 --- a/libs/gaussian-core/include/openstrata/gs/GaussianSizeMath.h +++ b/libs/gaussian-core/include/openstrata/gs/GaussianSizeMath.h @@ -26,8 +26,8 @@ namespace openstrata::gs { // True and writes `a * b` unless the product overflows std::size_t. -inline bool CheckedMulSize( - std::size_t a, std::size_t b, std::size_t* product) noexcept +inline bool CheckedMulSize(std::size_t a, std::size_t b, + std::size_t* product) noexcept { if (!product) { return false; @@ -40,8 +40,8 @@ inline bool CheckedMulSize( } // True and writes `a + b` unless the sum overflows std::size_t. -inline bool CheckedAddSize( - std::size_t a, std::size_t b, std::size_t* sum) noexcept +inline bool CheckedAddSize(std::size_t a, std::size_t b, + std::size_t* sum) noexcept { if (!sum) { return false; @@ -57,10 +57,8 @@ inline bool CheckedAddSize( // (GAUSSIAN_MODEL_CONTRACT.md §3), overflow-checked. Fails on a degree // outside the supported 0..kMaxShDegree range rather than computing a length // the shared gate would reject anyway. -inline bool ComputeRestCoefficientCount( - std::size_t gaussianCount, - int shDegree, - std::size_t* restCount) noexcept +inline bool ComputeRestCoefficientCount(std::size_t gaussianCount, int shDegree, + std::size_t* restCount) noexcept { if (!restCount || shDegree < 0 || shDegree > kMaxShDegree) { return false; diff --git a/libs/gaussian-core/include/openstrata/gs/testing/CloudContract.h b/libs/gaussian-core/include/openstrata/gs/testing/CloudContract.h index 68d1a77..2f040d7 100644 --- a/libs/gaussian-core/include/openstrata/gs/testing/CloudContract.h +++ b/libs/gaussian-core/include/openstrata/gs/testing/CloudContract.h @@ -34,7 +34,8 @@ namespace openstrata::gs::testing { // Returns one message per violated rule; an empty result means the cloud // conforms. Returning messages rather than asserting keeps the checker // independent of any bundle's test harness. -inline std::vector CheckCloudContract(const GaussianCloudData& cloud) +inline std::vector +CheckCloudContract(const GaussianCloudData& cloud) { std::vector violations; const auto fail = [&violations](std::string message) { @@ -59,7 +60,8 @@ inline std::vector CheckCloudContract(const GaussianCloudData& clou const auto checkLength = [&](const char* name, std::size_t actual) { if (actual != count) { fail(std::string("§3: ") + name + " has length " + - std::to_string(actual) + ", expected " + std::to_string(count)); + std::to_string(actual) + ", expected " + + std::to_string(count)); } }; checkLength("positions", cloud.positions.size()); @@ -78,8 +80,8 @@ inline std::vector CheckCloudContract(const GaussianCloudData& clou const std::size_t expectedRest = count * (perGaussian - 1); if (cloud.restCoefficients.size() != expectedRest) { fail("§3: restCoefficients has length " + - std::to_string(cloud.restCoefficients.size()) + ", expected " + - std::to_string(expectedRest)); + std::to_string(cloud.restCoefficients.size()) + ", expected " + + std::to_string(expectedRest)); } const auto finite3 = [](const Float3& v) { @@ -100,20 +102,20 @@ inline std::vector CheckCloudContract(const GaussianCloudData& clou fail("§3: non-finite scale" + at); } else if (scale.x <= 0.0f || scale.y <= 0.0f || scale.z <= 0.0f) { fail("§3: scale is not strictly positive" + at + - " (log-encoded scales reaching the model?)"); + " (log-encoded scales reaching the model?)"); } // §3: opacity is already through sigmoid, never a logit. const float opacity = cloud.opacities[i]; if (!std::isfinite(opacity) || opacity < 0.0f || opacity > 1.0f) { fail("§3: opacity outside [0, 1]" + at + - " (a logit reaching the model?)"); + " (a logit reaching the model?)"); } // §3: quaternions reach the model normalized. const Quaternion& q = cloud.rotations[i]; - const float norm = std::sqrt( - q.real * q.real + q.i * q.i + q.j * q.j + q.k * q.k); + const float norm = + std::sqrt(q.real * q.real + q.i * q.i + q.j * q.j + q.k * q.k); if (!std::isfinite(norm) || std::fabs(norm - 1.0f) > 1.0e-4f) { fail("§3: quaternion is not normalized" + at); } @@ -126,7 +128,7 @@ inline std::vector CheckCloudContract(const GaussianCloudData& clou for (std::size_t i = 0; i < cloud.restCoefficients.size(); ++i) { if (!finite3(cloud.restCoefficients[i])) { fail("§3: non-finite rest coefficient at index " + - std::to_string(i)); + std::to_string(i)); } } diff --git a/libs/gaussian-core/include/openstrata/gs/testing/DecoderTestKit.h b/libs/gaussian-core/include/openstrata/gs/testing/DecoderTestKit.h index 95ac083..cc8268f 100644 --- a/libs/gaussian-core/include/openstrata/gs/testing/DecoderTestKit.h +++ b/libs/gaussian-core/include/openstrata/gs/testing/DecoderTestKit.h @@ -104,14 +104,14 @@ struct CloudTolerances { // q and -q denote the same rotation and both are admissible // (GAUSSIAN_MODEL_CONTRACT.md §3), so equality holds under whichever sign // matches better. -inline bool QuaternionsEquivalent( - const Quaternion& a, const Quaternion& b, float tolerance) noexcept +inline bool QuaternionsEquivalent(const Quaternion& a, const Quaternion& b, + float tolerance) noexcept { const auto close = [tolerance](const Quaternion& p, const Quaternion& q) { return std::fabs(p.real - q.real) <= tolerance && - std::fabs(p.i - q.i) <= tolerance && - std::fabs(p.j - q.j) <= tolerance && - std::fabs(p.k - q.k) <= tolerance; + std::fabs(p.i - q.i) <= tolerance && + std::fabs(p.j - q.j) <= tolerance && + std::fabs(p.k - q.k) <= tolerance; }; const Quaternion negated = {-b.real, -b.i, -b.j, -b.k}; return close(a, b) || close(a, negated); @@ -121,10 +121,10 @@ inline bool QuaternionsEquivalent( // within the tolerances. Compares structure (count, degree, lengths), every // field per Gaussian, every rest coefficient by (gaussian, coefficient) // index, and the deterministic derived extent -- without any USD stage. -inline std::vector CompareClouds( - const GaussianCloudData& actual, - const GaussianCloudData& expected, - const CloudTolerances& tolerances = {}) +inline std::vector +CompareClouds(const GaussianCloudData& actual, + const GaussianCloudData& expected, + const CloudTolerances& tolerances = {}) { std::vector mismatches; const auto fail = [&mismatches](std::string message) { @@ -133,79 +133,78 @@ inline std::vector CompareClouds( if (actual.gaussianCount != expected.gaussianCount) { fail("gaussianCount " + std::to_string(actual.gaussianCount) + - " != expected " + std::to_string(expected.gaussianCount)); + " != expected " + std::to_string(expected.gaussianCount)); return mismatches; } if (actual.shDegree != expected.shDegree) { - fail("shDegree " + std::to_string(actual.shDegree) + - " != expected " + std::to_string(expected.shDegree)); + fail("shDegree " + std::to_string(actual.shDegree) + " != expected " + + std::to_string(expected.shDegree)); return mismatches; } - const auto checkLength = [&]( - const char* name, std::size_t got, std::size_t want) { + const auto checkLength = [&](const char* name, std::size_t got, + std::size_t want) { if (got != want) { fail(std::string(name) + " has length " + std::to_string(got) + - ", expected " + std::to_string(want)); + ", expected " + std::to_string(want)); return false; } return true; }; if (!checkLength("positions", actual.positions.size(), - expected.positions.size()) || - !checkLength("scales", actual.scales.size(), - expected.scales.size()) || + expected.positions.size()) || + !checkLength("scales", actual.scales.size(), expected.scales.size()) || !checkLength("rotations", actual.rotations.size(), - expected.rotations.size()) || + expected.rotations.size()) || !checkLength("opacities", actual.opacities.size(), - expected.opacities.size()) || + expected.opacities.size()) || !checkLength("dcCoefficients", actual.dcCoefficients.size(), - expected.dcCoefficients.size()) || + expected.dcCoefficients.size()) || !checkLength("restCoefficients", actual.restCoefficients.size(), - expected.restCoefficients.size())) { + expected.restCoefficients.size())) { return mismatches; } const auto close = [](float a, float b, float tolerance) { return std::fabs(a - b) <= tolerance; }; - const auto checkFloat3 = [&]( - const char* name, std::size_t index, - const Float3& got, const Float3& want, float tolerance) { + const auto checkFloat3 = [&](const char* name, std::size_t index, + const Float3& got, const Float3& want, + float tolerance) { if (!close(got.x, want.x, tolerance) || !close(got.y, want.y, tolerance) || !close(got.z, want.z, tolerance)) { fail(std::string(name) + "[" + std::to_string(index) + "] (" + - std::to_string(got.x) + ", " + std::to_string(got.y) + ", " + - std::to_string(got.z) + ") != expected (" + - std::to_string(want.x) + ", " + std::to_string(want.y) + - ", " + std::to_string(want.z) + ")"); + std::to_string(got.x) + ", " + std::to_string(got.y) + ", " + + std::to_string(got.z) + ") != expected (" + + std::to_string(want.x) + ", " + std::to_string(want.y) + ", " + + std::to_string(want.z) + ")"); } }; for (std::size_t i = 0; i < actual.gaussianCount; ++i) { - checkFloat3("positions", i, actual.positions[i], - expected.positions[i], tolerances.position); - checkFloat3("scales", i, actual.scales[i], - expected.scales[i], tolerances.scale); - if (!QuaternionsEquivalent(actual.rotations[i], - expected.rotations[i], tolerances.rotation)) { + checkFloat3("positions", i, actual.positions[i], expected.positions[i], + tolerances.position); + checkFloat3("scales", i, actual.scales[i], expected.scales[i], + tolerances.scale); + if (!QuaternionsEquivalent(actual.rotations[i], expected.rotations[i], + tolerances.rotation)) { fail("rotations[" + std::to_string(i) + - "] differs beyond tolerance under either sign"); + "] differs beyond tolerance under either sign"); } if (!close(actual.opacities[i], expected.opacities[i], - tolerances.opacity)) { + tolerances.opacity)) { fail("opacities[" + std::to_string(i) + "] " + - std::to_string(actual.opacities[i]) + " != expected " + - std::to_string(expected.opacities[i])); + std::to_string(actual.opacities[i]) + " != expected " + + std::to_string(expected.opacities[i])); } checkFloat3("dcCoefficients", i, actual.dcCoefficients[i], - expected.dcCoefficients[i], tolerances.shCoefficient); + expected.dcCoefficients[i], tolerances.shCoefficient); } const std::size_t restPerGaussian = - expected.gaussianCount == 0 - ? 0 - : expected.restCoefficients.size() / expected.gaussianCount; + expected.gaussianCount == 0 ? + 0 : + expected.restCoefficients.size() / expected.gaussianCount; for (std::size_t i = 0; i < actual.restCoefficients.size(); ++i) { const std::size_t gaussian = restPerGaussian == 0 ? 0 : i / restPerGaussian; @@ -217,11 +216,11 @@ inline std::vector CompareClouds( !close(got.y, want.y, tolerances.shCoefficient) || !close(got.z, want.z, tolerances.shCoefficient)) { fail("restCoefficients[gaussian " + std::to_string(gaussian) + - ", coefficient " + std::to_string(coefficient) + "] (" + - std::to_string(got.x) + ", " + std::to_string(got.y) + ", " + - std::to_string(got.z) + ") != expected (" + - std::to_string(want.x) + ", " + std::to_string(want.y) + - ", " + std::to_string(want.z) + ")"); + ", coefficient " + std::to_string(coefficient) + "] (" + + std::to_string(got.x) + ", " + std::to_string(got.y) + ", " + + std::to_string(got.z) + ") != expected (" + + std::to_string(want.x) + ", " + std::to_string(want.y) + ", " + + std::to_string(want.z) + ")"); } } @@ -241,8 +240,8 @@ inline std::vector CompareClouds( } else if (actualHasExtent) { const auto extentClose = [&](const Float3& a, const Float3& b) { return close(a.x, b.x, tolerances.extent) && - close(a.y, b.y, tolerances.extent) && - close(a.z, b.z, tolerances.extent); + close(a.y, b.y, tolerances.extent) && + close(a.z, b.z, tolerances.extent); }; if (!extentClose(actualMinimum, expectedMinimum) || !extentClose(actualMaximum, expectedMaximum)) { @@ -325,8 +324,7 @@ inline std::vector MakeInvalidCloudCases() // Degree above the supported ceiling, rest sized consistently for it. GaussianCloudData cloud = base; cloud.shDegree = kMaxShDegree + 1; - cloud.restCoefficients.resize( - cloud.CoefficientsPerGaussian() - 1); + cloud.restCoefficients.resize(cloud.CoefficientsPerGaussian() - 1); add("unsupported-sh-degree", std::move(cloud)); } { diff --git a/libs/gaussian-core/src/GaussianImportStats.cpp b/libs/gaussian-core/src/GaussianImportStats.cpp index 7e1e72f..db54425 100644 --- a/libs/gaussian-core/src/GaussianImportStats.cpp +++ b/libs/gaussian-core/src/GaussianImportStats.cpp @@ -38,11 +38,14 @@ CoordinateConversionName(GaussianCoordinateConversion conversion) noexcept std::uint64_t ComputeDecodedByteSize(const GaussianCloudData& cloud) noexcept { return static_cast(cloud.positions.size()) * sizeof(Float3) + - static_cast(cloud.scales.size()) * sizeof(Float3) + - static_cast(cloud.rotations.size()) * sizeof(Quaternion) + - static_cast(cloud.opacities.size()) * sizeof(float) + - static_cast(cloud.dcCoefficients.size()) * sizeof(Float3) + - static_cast(cloud.restCoefficients.size()) * sizeof(Float3); + static_cast(cloud.scales.size()) * sizeof(Float3) + + static_cast(cloud.rotations.size()) * + sizeof(Quaternion) + + static_cast(cloud.opacities.size()) * sizeof(float) + + static_cast(cloud.dcCoefficients.size()) * + sizeof(Float3) + + static_cast(cloud.restCoefficients.size()) * + sizeof(Float3); } std::string FormatImportStats(const GaussianImportStats& stats) @@ -73,14 +76,12 @@ std::string FormatImportStats(const GaussianImportStats& stats) add("sourceBytes", std::to_string(stats.sourceBytes)); add("decodedBytes", std::to_string(stats.decodedBytes)); if (stats.hasBounds) { - add("boundsMin", - FormatDouble(stats.boundsMinimum.x) + ',' + - FormatDouble(stats.boundsMinimum.y) + ',' + - FormatDouble(stats.boundsMinimum.z)); - add("boundsMax", - FormatDouble(stats.boundsMaximum.x) + ',' + - FormatDouble(stats.boundsMaximum.y) + ',' + - FormatDouble(stats.boundsMaximum.z)); + add("boundsMin", FormatDouble(stats.boundsMinimum.x) + ',' + + FormatDouble(stats.boundsMinimum.y) + ',' + + FormatDouble(stats.boundsMinimum.z)); + add("boundsMax", FormatDouble(stats.boundsMaximum.x) + ',' + + FormatDouble(stats.boundsMaximum.y) + ',' + + FormatDouble(stats.boundsMaximum.z)); } add("readSeconds", FormatDouble(stats.readSeconds)); add("decodeSeconds", FormatDouble(stats.decodeSeconds)); diff --git a/libs/gaussian-core/src/GaussianMath.cpp b/libs/gaussian-core/src/GaussianMath.cpp index e91eac7..e1f04c2 100644 --- a/libs/gaussian-core/src/GaussianMath.cpp +++ b/libs/gaussian-core/src/GaussianMath.cpp @@ -12,7 +12,7 @@ namespace { bool IsFinite(const Float3& value) noexcept { return std::isfinite(value.x) && std::isfinite(value.y) && - std::isfinite(value.z); + std::isfinite(value.z); } void SetError(std::string* error, const char* message) noexcept @@ -50,14 +50,11 @@ bool DecodeLogScale(const Float3& stored, Float3* actual) noexcept std::exp(stored.z), }; return IsFinite(*actual) && actual->x > 0.0f && actual->y > 0.0f && - actual->z > 0.0f; + actual->z > 0.0f; } -bool NormalizeQuaternion( - const Quaternion& stored, - Quaternion* normalized, - bool* replacedWithIdentity, - bool* changed) noexcept +bool NormalizeQuaternion(const Quaternion& stored, Quaternion* normalized, + bool* replacedWithIdentity, bool* changed) noexcept { if (replacedWithIdentity) { *replacedWithIdentity = false; @@ -124,9 +121,9 @@ namespace { // reference flipSh basis {y, z, x, xy, yz, zz, xz, xx-yy, ...} evaluated at // (x, y, z) = (+1, -1, -1); the derivation is recorded in ADR 0001. constexpr float kShFlipYZ[15] = { - -1.0f, -1.0f, +1.0f, // band 1: y, z, x - -1.0f, +1.0f, +1.0f, -1.0f, +1.0f, // band 2: xy, yz, zz, xz, xx-yy - -1.0f, +1.0f, -1.0f, -1.0f, +1.0f, -1.0f, // band 3 + -1.0f, -1.0f, +1.0f, // band 1: y, z, x + -1.0f, +1.0f, +1.0f, -1.0f, +1.0f, // band 2: xy, yz, zz, xz, xx-yy + -1.0f, +1.0f, -1.0f, -1.0f, +1.0f, -1.0f, // band 3 +1.0f, }; @@ -155,9 +152,9 @@ void FlipYZAxes(GaussianCloudData* cloud) noexcept rotation.k = -rotation.k; } const std::size_t restPerGaussian = - cloud->gaussianCount == 0 || cloud->restCoefficients.empty() - ? 0 - : cloud->restCoefficients.size() / cloud->gaussianCount; + cloud->gaussianCount == 0 || cloud->restCoefficients.empty() ? + 0 : + cloud->restCoefficients.size() / cloud->gaussianCount; // Defensive: a rest layout the table cannot index (empty, or wider than // kMaxShDegree admits) is left unflipped, while positions and rotations // are already negated. That half-converted cloud is safe only because @@ -177,29 +174,24 @@ void FlipYZAxes(GaussianCloudData* cloud) noexcept } } -bool ComputeCloudExtent( - const Float3* positions, - const Float3* scales, - std::size_t count, - Float3* outMinimum, - Float3* outMaximum) noexcept +bool ComputeCloudExtent(const Float3* positions, const Float3* scales, + std::size_t count, Float3* outMinimum, + Float3* outMaximum) noexcept { if (!positions || !scales || count == 0 || !outMinimum || !outMaximum) { return false; } - Float3 minimum = { - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()}; - Float3 maximum = { - -std::numeric_limits::max(), - -std::numeric_limits::max(), - -std::numeric_limits::max()}; + Float3 minimum = {std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + Float3 maximum = {-std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max()}; for (std::size_t i = 0; i < count; ++i) { const Float3& p = positions[i]; const Float3& s = scales[i]; - const double radius = 3.0 * static_cast( - std::max({s.x, s.y, s.z})); + const double radius = + 3.0 * static_cast(std::max({s.x, s.y, s.z})); if (!std::isfinite(radius) || radius > std::numeric_limits::max()) { return false; @@ -217,9 +209,8 @@ bool ComputeCloudExtent( return true; } -bool ValidateGaussianCloud( - const GaussianCloudData& cloud, - std::string* error) noexcept +bool ValidateGaussianCloud(const GaussianCloudData& cloud, + std::string* error) noexcept { const std::size_t count = cloud.gaussianCount; if (count == 0) { @@ -244,10 +235,12 @@ bool ValidateGaussianCloud( // The message spells the ceiling out because this function is // noexcept and composing a string could throw; the assert keeps the // text tied to the constant. - static_assert(kMaxShDegree == 3, + static_assert( + kMaxShDegree == 3, "update the SH degree validation message when kMaxShDegree " "changes"); - SetError(error, "Gaussian SH degree exceeds the supported maximum of 3."); + SetError(error, + "Gaussian SH degree exceeds the supported maximum of 3."); return false; } const std::size_t restPerGaussian = cloud.CoefficientsPerGaussian() - 1; @@ -261,17 +254,17 @@ bool ValidateGaussianCloud( if (!IsFinite(cloud.positions[i]) || !IsFinite(cloud.scales[i]) || cloud.scales[i].x <= 0.0f || cloud.scales[i].y <= 0.0f || cloud.scales[i].z <= 0.0f || !std::isfinite(q.real) || - !std::isfinite(q.i) || !std::isfinite(q.j) || - !std::isfinite(q.k) || !std::isfinite(cloud.opacities[i]) || - cloud.opacities[i] < 0.0f || cloud.opacities[i] > 1.0f || - !IsFinite(cloud.dcCoefficients[i])) { - SetError(error, "Gaussian cloud contains an invalid numeric value."); + !std::isfinite(q.i) || !std::isfinite(q.j) || !std::isfinite(q.k) || + !std::isfinite(cloud.opacities[i]) || cloud.opacities[i] < 0.0f || + cloud.opacities[i] > 1.0f || !IsFinite(cloud.dcCoefficients[i])) { + SetError(error, + "Gaussian cloud contains an invalid numeric value."); return false; } // Decoders normalize (GAUSSIAN_MODEL_CONTRACT.md §3); the gate holds // them to it. The tolerance mirrors testing::CheckCloudContract. - const float norm = std::sqrt( - q.real * q.real + q.i * q.i + q.j * q.j + q.k * q.k); + const float norm = + std::sqrt(q.real * q.real + q.i * q.i + q.j * q.j + q.k * q.k); if (std::fabs(norm - 1.0f) > 1.0e-4f) { SetError(error, "Gaussian rotation quaternion is not normalized."); return false; @@ -279,7 +272,8 @@ bool ValidateGaussianCloud( } for (const Float3& coefficient : cloud.restCoefficients) { if (!IsFinite(coefficient)) { - SetError(error, "Gaussian SH coefficients contain a non-finite value."); + SetError(error, + "Gaussian SH coefficients contain a non-finite value."); return false; } } diff --git a/libs/gaussian-core/tests/test_decoder_kit.cpp b/libs/gaussian-core/tests/test_decoder_kit.cpp index e1927eb..6af00b7 100644 --- a/libs/gaussian-core/tests/test_decoder_kit.cpp +++ b/libs/gaussian-core/tests/test_decoder_kit.cpp @@ -34,18 +34,22 @@ namespace { int failures = 0; -#define CHECK(expr) \ - do { if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ - ++failures; \ - } } while (false) - -#define CHECK_MSG(expr, context) \ - do { if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr \ - << " [" << (context) << "]\n"; \ - ++failures; \ - } } while (false) +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ + ++failures; \ + } \ + } while (false) + +#define CHECK_MSG(expr, context) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr << " [" \ + << (context) << "]\n"; \ + ++failures; \ + } \ + } while (false) // --- The mock format -------------------------------------------------------- @@ -89,12 +93,11 @@ MockSplatDocument EncodeMockSplat(const gs::GaussianCloudData& model) MockSplatDocument document; document.count = source.gaussianCount; document.shDegree = source.shDegree; - const std::size_t restPerGaussian = - source.CoefficientsPerGaussian() - 1; + const std::size_t restPerGaussian = source.CoefficientsPerGaussian() - 1; for (std::size_t i = 0; i < source.gaussianCount; ++i) { const gs::Float3& p = source.positions[i]; - document.positionsRdf.insert( - document.positionsRdf.end(), {p.x, p.y, p.z}); + document.positionsRdf.insert(document.positionsRdf.end(), + {p.x, p.y, p.z}); const gs::Float3& s = source.scales[i]; document.logScales.insert( document.logScales.end(), @@ -118,8 +121,9 @@ MockSplatDocument EncodeMockSplat(const gs::GaussianCloudData& model) for (std::size_t c = 0; c < restPerGaussian; ++c) { const gs::Float3& value = source.restCoefficients[i * restPerGaussian + c]; - document.restChannelMajor.push_back( - channel == 0 ? value.x : channel == 1 ? value.y : value.z); + document.restChannelMajor.push_back(channel == 0 ? value.x : + channel == 1 ? value.y : + value.z); } } } @@ -130,10 +134,8 @@ MockSplatDocument EncodeMockSplat(const gs::GaussianCloudData& model) // the contract alone. Structure mirrors what the guide asks of a real // decoder: overflow-checked allocation, shared conversion helpers, frame // conversion after arrays are built, shared validation last. -bool DecodeMockSplat( - const MockSplatDocument& document, - gs::GaussianCloudData* cloud, - std::string* error) +bool DecodeMockSplat(const MockSplatDocument& document, + gs::GaussianCloudData* cloud, std::string* error) { const auto fail = [error](const char* message) { if (error) { @@ -149,8 +151,8 @@ bool DecodeMockSplat( // Contract §3, maximum count and overflow: derived sizes go through the // shared checked arithmetic before any allocation. std::size_t restCount = 0; - if (!gs::ComputeRestCoefficientCount( - document.count, document.shDegree, &restCount)) { + if (!gs::ComputeRestCoefficientCount(document.count, document.shDegree, + &restCount)) { return fail("rest-coefficient count overflows"); } if (!gs::TryResize(&result.positions, document.count) || @@ -162,32 +164,27 @@ bool DecodeMockSplat( return fail("model arrays could not be allocated"); } - const std::size_t restPerGaussian = - result.CoefficientsPerGaussian() - 1; + const std::size_t restPerGaussian = result.CoefficientsPerGaussian() - 1; for (std::size_t i = 0; i < document.count; ++i) { - result.positions[i] = { - document.positionsRdf[i * 3], - document.positionsRdf[i * 3 + 1], - document.positionsRdf[i * 3 + 2]}; - if (!gs::DecodeLogScale( - {document.logScales[i * 3], - document.logScales[i * 3 + 1], - document.logScales[i * 3 + 2]}, - &result.scales[i])) { + result.positions[i] = {document.positionsRdf[i * 3], + document.positionsRdf[i * 3 + 1], + document.positionsRdf[i * 3 + 2]}; + if (!gs::DecodeLogScale({document.logScales[i * 3], + document.logScales[i * 3 + 1], + document.logScales[i * 3 + 2]}, + &result.scales[i])) { return fail("log scale does not decode"); } // Reorder vector-first to the model's scalar-first convention, then // normalize through the shared helper. const float* r = document.rotationsVectorFirst.data() + i * 4; - if (!gs::NormalizeQuaternion( - {r[3], r[0], r[1], r[2]}, &result.rotations[i])) { + if (!gs::NormalizeQuaternion({r[3], r[0], r[1], r[2]}, + &result.rotations[i])) { return fail("quaternion is not normalizable"); } result.opacities[i] = gs::Sigmoid(document.opacityLogits[i]); - result.dcCoefficients[i] = { - document.dc[i * 3], - document.dc[i * 3 + 1], - document.dc[i * 3 + 2]}; + result.dcCoefficients[i] = {document.dc[i * 3], document.dc[i * 3 + 1], + document.dc[i * 3 + 2]}; } // Channel-major to Gaussian-major transpose. const std::size_t channelStride = document.count * restPerGaussian; @@ -225,16 +222,15 @@ bool DecodeMockSplat( void TestMockDecoderRoundTrip() { for (const bool multi : {false, true}) { - const gs::GaussianCloudData expected = multi - ? kit::MakeCanonicalMultiGaussianCloud() - : kit::MakeCanonicalOneGaussianCloud(); + const gs::GaussianCloudData expected = + multi ? kit::MakeCanonicalMultiGaussianCloud() : + kit::MakeCanonicalOneGaussianCloud(); const MockSplatDocument document = EncodeMockSplat(expected); gs::GaussianCloudData decoded; std::string error; CHECK_MSG(DecodeMockSplat(document, &decoded, &error), error); - for (const std::string& violation : - kit::CheckCloudContract(decoded)) { + for (const std::string& violation : kit::CheckCloudContract(decoded)) { CHECK_MSG(false, violation); } for (const std::string& mismatch : @@ -253,9 +249,9 @@ void TestInvalidCasesAreRejected() for (const auto& invalid : cases) { std::string error; CHECK_MSG(!gs::ValidateGaussianCloud(invalid.cloud, &error), - invalid.name); + invalid.name); CHECK_MSG(!kit::CheckCloudContract(invalid.cloud).empty(), - invalid.name); + invalid.name); } } @@ -266,8 +262,7 @@ void TestComparisonDistinguishesOrderings() { const gs::GaussianCloudData expected = kit::MakeCanonicalMultiGaussianCloud(); - const std::size_t restPerGaussian = - expected.CoefficientsPerGaussian() - 1; + const std::size_t restPerGaussian = expected.CoefficientsPerGaussian() - 1; CHECK(kit::CompareClouds(expected, expected).empty()); @@ -275,7 +270,7 @@ void TestComparisonDistinguishesOrderings() gs::GaussianCloudData pointSwapped = expected; for (std::size_t c = 0; c < restPerGaussian; ++c) { std::swap(pointSwapped.restCoefficients[c], - pointSwapped.restCoefficients[restPerGaussian + c]); + pointSwapped.restCoefficients[restPerGaussian + c]); } CHECK(!kit::CompareClouds(pointSwapped, expected).empty()); @@ -312,13 +307,12 @@ void TestQuaternionSignEquivalence() // extent must match ComputeCloudExtent exactly. void TestExtentComparison() { - const gs::GaussianCloudData expected = - kit::MakeCanonicalOneGaussianCloud(); + const gs::GaussianCloudData expected = kit::MakeCanonicalOneGaussianCloud(); gs::Float3 minimum, maximum; - CHECK(gs::ComputeCloudExtent( - expected.positions.data(), expected.scales.data(), - expected.gaussianCount, &minimum, &maximum)); + CHECK(gs::ComputeCloudExtent(expected.positions.data(), + expected.scales.data(), expected.gaussianCount, + &minimum, &maximum)); const float radius = 3.0f * 0.04f; CHECK(std::fabs(minimum.x - (0.5f - radius)) <= 1.0e-6f); CHECK(std::fabs(maximum.z - (2.0f + radius)) <= 1.0e-6f); @@ -328,9 +322,9 @@ void TestExtentComparison() CHECK(!kit::CompareClouds(widened, expected).empty()); // Zero count and non-finite bounds are not computable. - CHECK(!gs::ComputeCloudExtent( - expected.positions.data(), expected.scales.data(), 0, - &minimum, &maximum)); + CHECK(!gs::ComputeCloudExtent(expected.positions.data(), + expected.scales.data(), 0, &minimum, + &maximum)); } void TestSizeMath() @@ -359,8 +353,7 @@ void TestSizeMath() void TestImportStatsSeam() { - const gs::GaussianCloudData cloud = - kit::MakeCanonicalMultiGaussianCloud(); + const gs::GaussianCloudData cloud = kit::MakeCanonicalMultiGaussianCloud(); // 3 Gaussians, degree 3: exact semantic bytes, independent of capacity. const std::uint64_t expectedBytes = 3ull * (12 + 12 + 16 + 4 + 12) + 45ull * 12; @@ -397,14 +390,13 @@ void TestImportStatsSeam() CHECK(line.find("warnings=1") != std::string::npos); CHECK(line.find("sourceBytes=1234") != std::string::npos); CHECK(line.find("decodedBytes=" + std::to_string(expectedBytes)) != - std::string::npos); + std::string::npos); CHECK(line.find("boundsMin=") != std::string::npos); CHECK(line.find("readSeconds=0.25") != std::string::npos); // Without bounds the bounds keys are absent, not zero-filled. stats.hasBounds = false; - CHECK(gs::FormatImportStats(stats).find("boundsMin=") == - std::string::npos); + CHECK(gs::FormatImportStats(stats).find("boundsMin=") == std::string::npos); } } // namespace diff --git a/libs/gaussian-core/tests/test_gaussian_math.cpp b/libs/gaussian-core/tests/test_gaussian_math.cpp index 3f8027d..8e790cf 100644 --- a/libs/gaussian-core/tests/test_gaussian_math.cpp +++ b/libs/gaussian-core/tests/test_gaussian_math.cpp @@ -14,11 +14,13 @@ namespace { int failures = 0; -#define CHECK(expr) \ - do { if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ - ++failures; \ - } } while (false) +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ + ++failures; \ + } \ + } while (false) bool Close(float a, float b, float epsilon = 1.0e-6f) { @@ -45,11 +47,11 @@ void TestTransforms() gs::Quaternion q; bool identity = false; bool changed = false; - CHECK(gs::NormalizeQuaternion({2.0f, 0.0f, 0.0f, 0.0f}, - &q, &identity, &changed)); + CHECK(gs::NormalizeQuaternion({2.0f, 0.0f, 0.0f, 0.0f}, &q, &identity, + &changed)); CHECK(!identity && changed && Close(q.real, 1.0f)); - CHECK(gs::NormalizeQuaternion({0.0f, 0.0f, 0.0f, 0.0f}, - &q, &identity, &changed)); + CHECK(gs::NormalizeQuaternion({0.0f, 0.0f, 0.0f, 0.0f}, &q, &identity, + &changed)); CHECK(identity && changed && Close(q.real, 1.0f)); } @@ -100,8 +102,7 @@ void TestValidation() // even when the rest array is sized consistently for it. gs::GaussianCloudData degree4 = oneGaussian(); degree4.shDegree = gs::kMaxShDegree + 1; - degree4.restCoefficients.resize( - degree4.CoefficientsPerGaussian() - 1); + degree4.restCoefficients.resize(degree4.CoefficientsPerGaussian() - 1); CHECK(!gs::ValidateGaussianCloud(degree4, &error)); // §4: the gate rejects an unnormalized quaternion, while one within the @@ -120,9 +121,9 @@ void TestValidation() // itself. Derived from the reference flipSh basis // {y, z, x, xy, yz, zz, xz, xx-yy, band 3...} at (x, y, z) = (+1, -1, -1). constexpr float kExpectedShFlip[15] = { - -1.0f, -1.0f, +1.0f, // band 1 - -1.0f, +1.0f, +1.0f, -1.0f, +1.0f, // band 2 - -1.0f, +1.0f, -1.0f, -1.0f, +1.0f, -1.0f, // band 3 + -1.0f, -1.0f, +1.0f, // band 1 + -1.0f, +1.0f, +1.0f, -1.0f, +1.0f, // band 2 + -1.0f, +1.0f, -1.0f, -1.0f, +1.0f, -1.0f, // band 3 +1.0f, }; diff --git a/libs/gaussian-usd/include/openstrata/gs/usd/GaussianLayerWriter.h b/libs/gaussian-usd/include/openstrata/gs/usd/GaussianLayerWriter.h index fda09b0..2cf49a2 100644 --- a/libs/gaussian-usd/include/openstrata/gs/usd/GaussianLayerWriter.h +++ b/libs/gaussian-usd/include/openstrata/gs/usd/GaussianLayerWriter.h @@ -46,29 +46,25 @@ struct LayerWriterDiagnosticCodes { // schema, metadata policy, stage metrics, and default-prim behavior are // identical across formats by construction rather than by convention. class GaussianLayerWriter { -public: + public: explicit GaussianLayerWriter(const LayerWriterDiagnosticCodes& codes) : _codes(codes) - { - } + {} - bool WriteToLayer( - GaussianCloudData&& cloud, - const std::string& sourceFormat, - PXR_NS::SdfLayerRefPtr* outLayer, - std::string* error = nullptr) const; + bool WriteToLayer(GaussianCloudData&& cloud, + const std::string& sourceFormat, + PXR_NS::SdfLayerRefPtr* outLayer, + std::string* error = nullptr) const; // Metadata-only authoring (design policy §12.3): the same /Asset and // /Asset/Splat structure, stage metrics, custom data, and SH degree, with // no per-Gaussian arrays and no extent. - bool WriteMetadataToLayer( - std::size_t gaussianCount, - int shDegree, - const std::string& sourceFormat, - PXR_NS::SdfLayerRefPtr* outLayer, - std::string* error = nullptr) const; + bool WriteMetadataToLayer(std::size_t gaussianCount, int shDegree, + const std::string& sourceFormat, + PXR_NS::SdfLayerRefPtr* outLayer, + std::string* error = nullptr) const; -private: + private: LayerWriterDiagnosticCodes _codes; }; diff --git a/libs/gaussian-usd/src/GaussianLayerWriter.cpp b/libs/gaussian-usd/src/GaussianLayerWriter.cpp index 1d9b82f..35d2aca 100644 --- a/libs/gaussian-usd/src/GaussianLayerWriter.cpp +++ b/libs/gaussian-usd/src/GaussianLayerWriter.cpp @@ -31,8 +31,8 @@ namespace openstrata::gs::usd { namespace { static_assert(sizeof(Float3) == sizeof(PXR_NS::GfVec3f) && - std::is_trivially_copyable_v, - "Float3 must be byte-compatible with GfVec3f for bulk copies"); + std::is_trivially_copyable_v, + "Float3 must be byte-compatible with GfVec3f for bulk copies"); PXR_NS::GfVec3f ToGf(const Float3& value) { @@ -43,9 +43,8 @@ PXR_NS::VtArray TakeVec3fArray(std::vector* source) { PXR_NS::VtArray array(source->size()); if (!source->empty()) { - std::memcpy( - array.data(), source->data(), - source->size() * sizeof(PXR_NS::GfVec3f)); + std::memcpy(array.data(), source->data(), + source->size() * sizeof(PXR_NS::GfVec3f)); } std::vector().swap(*source); return array; @@ -62,21 +61,19 @@ void SetError(std::string* error, const char* code, const std::string& message) // kind, default prim, stage metrics, and source custom data, plus the // /Asset/Splat particle-field prim with its SH degree. The caller must keep // the returned stage alive while it authors through the returned splat prim. -bool AuthorScaffold( - const LayerWriterDiagnosticCodes& codes, - std::size_t gaussianCount, - int shDegree, - const std::string& sourceFormat, - PXR_NS::UsdStageRefPtr* stageOut, - PXR_NS::SdfLayerRefPtr* layerOut, - PXR_NS::UsdVolParticleField3DGaussianSplat* splatOut, - std::string* error) +bool AuthorScaffold(const LayerWriterDiagnosticCodes& codes, + std::size_t gaussianCount, int shDegree, + const std::string& sourceFormat, + PXR_NS::UsdStageRefPtr* stageOut, + PXR_NS::SdfLayerRefPtr* layerOut, + PXR_NS::UsdVolParticleField3DGaussianSplat* splatOut, + std::string* error) { PXR_NS::SdfLayerRefPtr layer = PXR_NS::SdfLayer::CreateAnonymous(".usda"); PXR_NS::UsdStageRefPtr stage = PXR_NS::UsdStage::Open(layer); if (!stage) { SetError(error, codes.stageCreationFailed, - "Could not create an in-memory USD stage."); + "Could not create an in-memory USD stage."); return false; } @@ -89,29 +86,29 @@ bool AuthorScaffold( PXR_NS::UsdGeomXform::Define(stage, assetPath).GetPrim(); if (!asset) { SetError(error, codes.scaffoldAuthoringFailed, - "Could not define /Asset."); + "Could not define /Asset."); return false; } PXR_NS::UsdModelAPI(asset).SetKind(PXR_NS::KindTokens->component); stage->SetDefaultPrim(asset); - asset.SetCustomDataByKey( - PXR_NS::TfToken("gs:sourceFormat"), PXR_NS::VtValue(sourceFormat)); + asset.SetCustomDataByKey(PXR_NS::TfToken("gs:sourceFormat"), + PXR_NS::VtValue(sourceFormat)); asset.SetCustomDataByKey( PXR_NS::TfToken("gs:gaussianCount"), PXR_NS::VtValue(static_cast(gaussianCount))); - asset.SetCustomDataByKey( - PXR_NS::TfToken("gs:shDegree"), PXR_NS::VtValue(shDegree)); + asset.SetCustomDataByKey(PXR_NS::TfToken("gs:shDegree"), + PXR_NS::VtValue(shDegree)); PXR_NS::UsdVolParticleField3DGaussianSplat splat = PXR_NS::UsdVolParticleField3DGaussianSplat::Define(stage, splatPath); if (!splat) { SetError(error, codes.scaffoldAuthoringFailed, - "Could not define /Asset/Splat as a Gaussian particle field."); + "Could not define /Asset/Splat as a Gaussian particle field."); return false; } if (!splat.CreateRadianceSphericalHarmonicsDegreeAttr().Set(shDegree)) { SetError(error, codes.attributeAuthoringFailed, - "Could not author the Gaussian SH degree."); + "Could not author the Gaussian SH degree."); return false; } @@ -123,15 +120,14 @@ bool AuthorScaffold( } // namespace -bool GaussianLayerWriter::WriteToLayer( - GaussianCloudData&& cloud, - const std::string& sourceFormat, - PXR_NS::SdfLayerRefPtr* outLayer, - std::string* error) const +bool GaussianLayerWriter::WriteToLayer(GaussianCloudData&& cloud, + const std::string& sourceFormat, + PXR_NS::SdfLayerRefPtr* outLayer, + std::string* error) const { if (!outLayer) { SetError(error, _codes.internalError, - "Gaussian writer received a null layer output."); + "Gaussian writer received a null layer output."); return false; } std::string validationError; @@ -143,9 +139,8 @@ bool GaussianLayerWriter::WriteToLayer( PXR_NS::UsdStageRefPtr stage; PXR_NS::SdfLayerRefPtr layer; PXR_NS::UsdVolParticleField3DGaussianSplat splat; - if (!AuthorScaffold( - _codes, cloud.gaussianCount, cloud.shDegree, sourceFormat, - &stage, &layer, &splat, error)) { + if (!AuthorScaffold(_codes, cloud.gaussianCount, cloud.shDegree, + sourceFormat, &stage, &layer, &splat, error)) { return false; } @@ -164,9 +159,8 @@ bool GaussianLayerWriter::WriteToLayer( std::vector().swap(cloud.rotations); PXR_NS::VtArray opacities(cloud.gaussianCount); - std::memcpy( - opacities.data(), cloud.opacities.data(), - cloud.gaussianCount * sizeof(float)); + std::memcpy(opacities.data(), cloud.opacities.data(), + cloud.gaussianCount * sizeof(float)); std::vector().swap(cloud.opacities); if (!splat.CreatePositionsAttr().Set(positions) || @@ -174,27 +168,24 @@ bool GaussianLayerWriter::WriteToLayer( !splat.CreateOrientationsAttr().Set(rotations) || !splat.CreateOpacitiesAttr().Set(opacities)) { SetError(error, _codes.attributeAuthoringFailed, - "Could not author a required Gaussian attribute."); + "Could not author a required Gaussian attribute."); return false; } - const std::size_t coefficientsPerGaussian = - cloud.CoefficientsPerGaussian(); + const std::size_t coefficientsPerGaussian = cloud.CoefficientsPerGaussian(); const std::size_t restPerGaussian = coefficientsPerGaussian - 1; - PXR_NS::VtArray coefficients( - cloud.gaussianCount * coefficientsPerGaussian); + PXR_NS::VtArray coefficients(cloud.gaussianCount * + coefficientsPerGaussian); PXR_NS::GfVec3f* coefficientOut = coefficients.data(); - for (std::size_t gaussian = 0; - gaussian < cloud.gaussianCount; - ++gaussian) { - PXR_NS::GfVec3f* out = coefficientOut + - gaussian * coefficientsPerGaussian; + for (std::size_t gaussian = 0; gaussian < cloud.gaussianCount; ++gaussian) { + PXR_NS::GfVec3f* out = + coefficientOut + gaussian * coefficientsPerGaussian; out[0] = ToGf(cloud.dcCoefficients[gaussian]); const std::size_t base = gaussian * restPerGaussian; - for (std::size_t coefficient = 0; - coefficient < restPerGaussian; + for (std::size_t coefficient = 0; coefficient < restPerGaussian; ++coefficient) { - out[1 + coefficient] = ToGf(cloud.restCoefficients[base + coefficient]); + out[1 + coefficient] = + ToGf(cloud.restCoefficients[base + coefficient]); } } std::vector().swap(cloud.dcCoefficients); @@ -202,7 +193,7 @@ bool GaussianLayerWriter::WriteToLayer( if (!splat.CreateRadianceSphericalHarmonicsCoefficientsAttr().Set( coefficients)) { SetError(error, _codes.attributeAuthoringFailed, - "Could not author Gaussian SH coefficients."); + "Could not author Gaussian SH coefficients."); return false; } @@ -211,18 +202,17 @@ bool GaussianLayerWriter::WriteToLayer( // implementation by construction. Float3 and GfVec3f are byte-compatible // (static_assert above). Float3 minimum, maximum; - if (!ComputeCloudExtent( - reinterpret_cast(positions.cdata()), - reinterpret_cast(scales.cdata()), - cloud.gaussianCount, &minimum, &maximum)) { + if (!ComputeCloudExtent(reinterpret_cast(positions.cdata()), + reinterpret_cast(scales.cdata()), + cloud.gaussianCount, &minimum, &maximum)) { SetError(error, _codes.extentOverflow, - "Gaussian extent exceeds float range."); + "Gaussian extent exceeds float range."); return false; } PXR_NS::VtArray extent = {ToGf(minimum), ToGf(maximum)}; if (!splat.CreateExtentAttr().Set(extent)) { SetError(error, _codes.attributeAuthoringFailed, - "Could not author Gaussian extent."); + "Could not author Gaussian extent."); return false; } @@ -230,25 +220,23 @@ bool GaussianLayerWriter::WriteToLayer( return true; } -bool GaussianLayerWriter::WriteMetadataToLayer( - std::size_t gaussianCount, - int shDegree, - const std::string& sourceFormat, - PXR_NS::SdfLayerRefPtr* outLayer, - std::string* error) const +bool GaussianLayerWriter::WriteMetadataToLayer(std::size_t gaussianCount, + int shDegree, + const std::string& sourceFormat, + PXR_NS::SdfLayerRefPtr* outLayer, + std::string* error) const { if (!outLayer) { SetError(error, _codes.internalError, - "Gaussian writer received a null layer output."); + "Gaussian writer received a null layer output."); return false; } PXR_NS::UsdStageRefPtr stage; PXR_NS::SdfLayerRefPtr layer; PXR_NS::UsdVolParticleField3DGaussianSplat splat; - if (!AuthorScaffold( - _codes, gaussianCount, shDegree, sourceFormat, - &stage, &layer, &splat, error)) { + if (!AuthorScaffold(_codes, gaussianCount, shDegree, sourceFormat, &stage, + &layer, &splat, error)) { return false; } diff --git a/libs/gaussian-usd/tests/test_gaussian_layer_writer.cpp b/libs/gaussian-usd/tests/test_gaussian_layer_writer.cpp index 72d2974..d665514 100644 --- a/libs/gaussian-usd/tests/test_gaussian_layer_writer.cpp +++ b/libs/gaussian-usd/tests/test_gaussian_layer_writer.cpp @@ -13,11 +13,13 @@ namespace { int failures = 0; -#define CHECK(expr) \ - do { if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ - ++failures; \ - } } while (false) +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ + ++failures; \ + } \ + } while (false) // Distinct sentinels per member, so each test pins a failure to the exact // member it must come from rather than merely observing that some code was @@ -32,12 +34,8 @@ int failures = 0; // assigned by name at each bundle, which is what removes the swap hazard the // covered members are additionally tested for. constexpr gs::usd::LayerWriterDiagnosticCodes kCodes{ - "TEST-INTERNAL", - "TEST-VALIDATION", - "TEST-STAGE", - "TEST-SCAFFOLD", - "TEST-ATTRIBUTE", - "TEST-EXTENT", + "TEST-INTERNAL", "TEST-VALIDATION", "TEST-STAGE", + "TEST-SCAFFOLD", "TEST-ATTRIBUTE", "TEST-EXTENT", }; bool StartsWith(const std::string& value, const std::string& prefix) diff --git a/plugins/gaussian-ply/src/GaussianPlyFileFormat.cpp b/plugins/gaussian-ply/src/GaussianPlyFileFormat.cpp index dd5077f..aec8780 100644 --- a/plugins/gaussian-ply/src/GaussianPlyFileFormat.cpp +++ b/plugins/gaussian-ply/src/GaussianPlyFileFormat.cpp @@ -23,7 +23,8 @@ PXR_NAMESPACE_OPEN_SCOPE -TF_DEFINE_PUBLIC_TOKENS(GaussianPlyFileFormatTokens, GAUSSIANPLY_FILE_FORMAT_TOKENS); +TF_DEFINE_PUBLIC_TOKENS(GaussianPlyFileFormatTokens, + GAUSSIANPLY_FILE_FORMAT_TOKENS); // Register the format with USD's type system so the plug system can find it. TF_REGISTRY_FUNCTION(TfType) @@ -38,24 +39,22 @@ TF_DEBUG_CODES(GSPLY_IMPORT_STATS); TF_REGISTRY_FUNCTION(TfDebug) { - TF_DEBUG_ENVIRONMENT_SYMBOL(GSPLY_IMPORT_STATS, + TF_DEBUG_ENVIRONMENT_SYMBOL( + GSPLY_IMPORT_STATS, "gaussian-ply: one line of per-import statistics through the shared " "GaussianImportStats seam"); } GaussianPlyFileFormat::GaussianPlyFileFormat() - : SdfFileFormat( - GaussianPlyFileFormatTokens->Id, - GaussianPlyFileFormatTokens->Version, - GaussianPlyFileFormatTokens->Target, - GaussianPlyFileFormatTokens->Extension) -{ -} + : SdfFileFormat(GaussianPlyFileFormatTokens->Id, + GaussianPlyFileFormatTokens->Version, + GaussianPlyFileFormatTokens->Target, + GaussianPlyFileFormatTokens->Extension) +{} GaussianPlyFileFormat::~GaussianPlyFileFormat() = default; -bool -GaussianPlyFileFormat::CanRead(const std::string& file) const +bool GaussianPlyFileFormat::CanRead(const std::string& file) const { if (SdfFileFormat::GetFileExtension(file) != "ply") { return false; @@ -63,21 +62,18 @@ GaussianPlyFileFormat::CanRead(const std::string& file) const return openstrata::gs::ply::GaussianPlyDecoder().CanRead(file); } -bool -GaussianPlyFileFormat::Read( - SdfLayer* layer, - const std::string& resolvedPath, - bool metadataOnly) const +bool GaussianPlyFileFormat::Read(SdfLayer* layer, + const std::string& resolvedPath, + bool metadataOnly) const { namespace gsply = openstrata::gs::ply; std::string error; gsply::GaussianPlyImportOptions options; - if (!gsply::ParseImportOptions( - layer->GetFileFormatArguments(), &options, &error)) { - TF_RUNTIME_ERROR( - "gaussian-ply: failed to read '%s': %s", - resolvedPath.c_str(), error.c_str()); + if (!gsply::ParseImportOptions(layer->GetFileFormatArguments(), &options, + &error)) { + TF_RUNTIME_ERROR("gaussian-ply: failed to read '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } @@ -90,16 +86,19 @@ GaussianPlyFileFormat::Read( // one type, so a positional list lets a swapped pair compile silently and // emit the wrong stable code to users. Named assignment makes that // mistake impossible instead of leaving it to be caught in review. - static const openstrata::gs::usd::LayerWriterDiagnosticCodes kWriterCodes = [] { - openstrata::gs::usd::LayerWriterDiagnosticCodes codes; - codes.internalError = gsply::diag::kInternalError; - codes.cloudValidationFailed = gsply::diag::kCloudValidationFailed; - codes.stageCreationFailed = gsply::diag::kStageCreationFailed; - codes.scaffoldAuthoringFailed = gsply::diag::kScaffoldAuthoringFailed; - codes.attributeAuthoringFailed = gsply::diag::kAttributeAuthoringFailed; - codes.extentOverflow = gsply::diag::kExtentOverflow; - return codes; - }(); + static const openstrata::gs::usd::LayerWriterDiagnosticCodes kWriterCodes = + [] { + openstrata::gs::usd::LayerWriterDiagnosticCodes codes; + codes.internalError = gsply::diag::kInternalError; + codes.cloudValidationFailed = gsply::diag::kCloudValidationFailed; + codes.stageCreationFailed = gsply::diag::kStageCreationFailed; + codes.scaffoldAuthoringFailed = + gsply::diag::kScaffoldAuthoringFailed; + codes.attributeAuthoringFailed = + gsply::diag::kAttributeAuthoringFailed; + codes.extentOverflow = gsply::diag::kExtentOverflow; + return codes; + }(); const openstrata::gs::usd::GaussianLayerWriter writer(kWriterCodes); // Sdf reload executes under an outer SdfChangeBlock. Authoring a detached @@ -115,9 +114,8 @@ GaussianPlyFileFormat::Read( // requires a full read and is not applied to metadata. gsply::GaussianPlyMetadata metadata; if (!decoder.DecodeMetadata(resolvedPath, &metadata, &error)) { - TF_RUNTIME_ERROR( - "gaussian-ply: failed to read '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-ply: failed to read '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } const int effectiveDegree = @@ -128,9 +126,8 @@ GaussianPlyFileFormat::Read( gsply::kSourceFormatToken, &generated, &error); }); if (!task.get()) { - TF_RUNTIME_ERROR( - "gaussian-ply: failed to author USD for '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-ply: failed to author USD for '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } layer->TransferContent(generated); @@ -146,9 +143,8 @@ GaussianPlyFileFormat::Read( openstrata::gs::GaussianCloudData cloud; std::vector warnings; if (!decoder.Decode(resolvedPath, &cloud, &warnings, &error, statsOut)) { - TF_RUNTIME_ERROR( - "gaussian-ply: failed to read '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-ply: failed to read '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } for (const std::string& warning : warnings) { @@ -156,9 +152,8 @@ GaussianPlyFileFormat::Read( } if (!gsply::ApplyImportOptions(options, &cloud, &error)) { - TF_RUNTIME_ERROR( - "gaussian-ply: failed to read '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-ply: failed to read '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } @@ -182,50 +177,49 @@ GaussianPlyFileFormat::Read( // authored. const auto authorStart = std::chrono::steady_clock::now(); auto task = std::async(std::launch::async, [&]() { - return writer.WriteToLayer( - std::move(cloud), gsply::kSourceFormatToken, &generated, &error); + return writer.WriteToLayer(std::move(cloud), gsply::kSourceFormatToken, + &generated, &error); }); if (!task.get()) { - TF_RUNTIME_ERROR( - "gaussian-ply: failed to author USD for '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-ply: failed to author USD for '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } if (statsOut) { - stats.authorSeconds = std::chrono::duration( - std::chrono::steady_clock::now() - authorStart).count(); - TF_DEBUG(GSPLY_IMPORT_STATS).Msg("gaussian-ply: %s\n", - openstrata::gs::FormatImportStats(stats).c_str()); + stats.authorSeconds = + std::chrono::duration(std::chrono::steady_clock::now() - + authorStart) + .count(); + TF_DEBUG(GSPLY_IMPORT_STATS) + .Msg("gaussian-ply: %s\n", + openstrata::gs::FormatImportStats(stats).c_str()); } layer->TransferContent(generated); return true; } -bool -GaussianPlyFileFormat::WriteToFile( - const SdfLayer& layer, - const std::string& filePath, - const std::string& comment, - const FileFormatArguments& args) const +bool GaussianPlyFileFormat::WriteToFile(const SdfLayer& layer, + const std::string& filePath, + const std::string& comment, + const FileFormatArguments& args) const { (void)layer; (void)filePath; (void)comment; (void)args; - TF_RUNTIME_ERROR("%s", - openstrata::gs::ply::diag::Format( - openstrata::gs::ply::diag::kWriteUnsupported, - "gaussian-ply is read-only; USD to Gaussian PLY writing is " - "unsupported").c_str()); + TF_RUNTIME_ERROR( + "%s", openstrata::gs::ply::diag::Format( + openstrata::gs::ply::diag::kWriteUnsupported, + "gaussian-ply is read-only; USD to Gaussian PLY writing is " + "unsupported") + .c_str()); return false; } -bool -GaussianPlyFileFormat::WriteToString( - const SdfLayer& layer, - std::string* str, - const std::string& comment) const +bool GaussianPlyFileFormat::WriteToString(const SdfLayer& layer, + std::string* str, + const std::string& comment) const { SdfFileFormatConstPtr usda = SdfFileFormat::FindByExtension("usda"); if (usda) { diff --git a/plugins/gaussian-ply/src/GaussianPlyFileFormat.h b/plugins/gaussian-ply/src/GaussianPlyFileFormat.h index 9f04989..b982e58 100644 --- a/plugins/gaussian-ply/src/GaussianPlyFileFormat.h +++ b/plugins/gaussian-ply/src/GaussianPlyFileFormat.h @@ -8,31 +8,28 @@ PXR_NAMESPACE_OPEN_SCOPE // The tokens that identify this file format to USD's Sdf layer registry. -#define GAUSSIANPLY_FILE_FORMAT_TOKENS \ - ((Id, "ply")) \ - ((Version, "1.0")) \ - ((Target, "usd")) \ - ((Extension, "ply")) +#define GAUSSIANPLY_FILE_FORMAT_TOKENS \ + ((Id, "ply"))((Version, "1.0"))((Target, "usd"))((Extension, "ply")) -TF_DECLARE_PUBLIC_TOKENS(GaussianPlyFileFormatTokens, GAUSSIANPLY_FILE_FORMAT_TOKENS); +TF_DECLARE_PUBLIC_TOKENS(GaussianPlyFileFormatTokens, + GAUSSIANPLY_FILE_FORMAT_TOKENS); /// Read-only SdfFileFormat for Graphdeco-style Gaussian Splat PLY files. /// Parsed splats are authored as UsdVolParticleField3DGaussianSplat data. class GaussianPlyFileFormat : public SdfFileFormat { -public: + public: bool CanRead(const std::string& file) const override; - bool Read(SdfLayer* layer, const std::string& resolvedPath, bool metadataOnly) const override; + bool Read(SdfLayer* layer, const std::string& resolvedPath, + bool metadataOnly) const override; bool WriteToFile( - const SdfLayer& layer, - const std::string& filePath, + const SdfLayer& layer, const std::string& filePath, const std::string& comment = std::string(), const FileFormatArguments& args = FileFormatArguments()) const override; - bool WriteToString( - const SdfLayer& layer, - std::string* str, - const std::string& comment = std::string()) const override; + bool + WriteToString(const SdfLayer& layer, std::string* str, + const std::string& comment = std::string()) const override; -protected: + protected: SDF_FILE_FORMAT_FACTORY_ACCESS; GaussianPlyFileFormat(); diff --git a/plugins/gaussian-ply/src/io/GaussianPlyDecoder.h b/plugins/gaussian-ply/src/io/GaussianPlyDecoder.h index 28f809e..e7c8db5 100644 --- a/plugins/gaussian-ply/src/io/GaussianPlyDecoder.h +++ b/plugins/gaussian-ply/src/io/GaussianPlyDecoder.h @@ -23,26 +23,22 @@ struct GaussianPlyMetadata { }; class GaussianPlyDecoder { -public: + public: bool CanRead(const std::string& path) const noexcept; // Validates the header layout and derives count and SH degree without // reading vertex data. - bool DecodeMetadata( - const std::string& path, - GaussianPlyMetadata* metadata, - std::string* error = nullptr) const; + bool DecodeMetadata(const std::string& path, GaussianPlyMetadata* metadata, + std::string* error = nullptr) const; // On success, `stats` (optional) carries the decoder's half of the shared // import-statistics record: source format/version, count and degree, // byte sizes, and the read/decode timings. Bounds and the authoring time // stay with the caller. - bool Decode( - const std::string& path, - GaussianCloudData* cloud, - std::vector* warnings = nullptr, - std::string* error = nullptr, - GaussianImportStats* stats = nullptr) const; + bool Decode(const std::string& path, GaussianCloudData* cloud, + std::vector* warnings = nullptr, + std::string* error = nullptr, + GaussianImportStats* stats = nullptr) const; }; } // namespace openstrata::gs::ply diff --git a/plugins/gaussian-ply/src/io/GaussianPlyImportOptions.cpp b/plugins/gaussian-ply/src/io/GaussianPlyImportOptions.cpp index c2fb7f1..aa0cfd1 100644 --- a/plugins/gaussian-ply/src/io/GaussianPlyImportOptions.cpp +++ b/plugins/gaussian-ply/src/io/GaussianPlyImportOptions.cpp @@ -53,23 +53,21 @@ bool ParseFullInt(const std::string& text, int* value) return true; } -std::string BadArgument( - const char* name, const std::string& value, const char* expectation) +std::string BadArgument(const char* name, const std::string& value, + const char* expectation) { - return std::string("Gaussian PLY file-format argument '") + name + - "' = '" + value + "' is invalid; expected " + expectation + "."; + return std::string("Gaussian PLY file-format argument '") + name + "' = '" + + value + "' is invalid; expected " + expectation + "."; } } // namespace -bool ParseImportOptions( - const std::map& arguments, - GaussianPlyImportOptions* options, - std::string* error) +bool ParseImportOptions(const std::map& arguments, + GaussianPlyImportOptions* options, std::string* error) { if (!options) { SetError(error, diag::kInternalError, - "Import-option parsing received a null options output."); + "Import-option parsing received a null options output."); return false; } *options = GaussianPlyImportOptions(); @@ -77,11 +75,11 @@ bool ParseImportOptions( const auto shDegree = arguments.find("shDegree"); if (shDegree != arguments.end()) { int parsed = 0; - if (!ParseFullInt(shDegree->second, &parsed) || - parsed < 0 || parsed > 3) { + if (!ParseFullInt(shDegree->second, &parsed) || parsed < 0 || + parsed > 3) { SetError(error, diag::kInvalidFormatArgument, - BadArgument("shDegree", shDegree->second, - "an integer in [0, 3]")); + BadArgument("shDegree", shDegree->second, + "an integer in [0, 3]")); return false; } options->shDegree = parsed; @@ -93,8 +91,8 @@ bool ParseImportOptions( if (!ParseFullFloat(opacityThreshold->second, &parsed) || !std::isfinite(parsed) || parsed < 0.0f || parsed > 1.0f) { SetError(error, diag::kInvalidFormatArgument, - BadArgument("opacityThreshold", opacityThreshold->second, - "a number in [0, 1]")); + BadArgument("opacityThreshold", opacityThreshold->second, + "a number in [0, 1]")); return false; } options->opacityThreshold = parsed; @@ -106,8 +104,8 @@ bool ParseImportOptions( if (!ParseFullFloat(scaleMultiplier->second, &parsed) || !std::isfinite(parsed) || parsed <= 0.0f) { SetError(error, diag::kInvalidFormatArgument, - BadArgument("scaleMultiplier", scaleMultiplier->second, - "a finite number greater than 0")); + BadArgument("scaleMultiplier", scaleMultiplier->second, + "a finite number greater than 0")); return false; } options->scaleMultiplier = parsed; @@ -116,8 +114,7 @@ bool ParseImportOptions( return true; } -int EffectiveShDegree( - const GaussianPlyImportOptions& options, int sourceDegree) +int EffectiveShDegree(const GaussianPlyImportOptions& options, int sourceDegree) { if (options.shDegree < 0) { return sourceDegree; @@ -125,14 +122,12 @@ int EffectiveShDegree( return std::min(options.shDegree, sourceDegree); } -bool ApplyImportOptions( - const GaussianPlyImportOptions& options, - GaussianCloudData* cloud, - std::string* error) +bool ApplyImportOptions(const GaussianPlyImportOptions& options, + GaussianCloudData* cloud, std::string* error) { if (!cloud) { SetError(error, diag::kInternalError, - "Import-option application received a null cloud."); + "Import-option application received a null cloud."); return false; } @@ -167,9 +162,9 @@ bool ApplyImportOptions( } if (kept == 0) { SetError(error, diag::kAllGaussiansFiltered, - "opacityThreshold " + - std::to_string(options.opacityThreshold) + - " removed every Gaussian in the file."); + "opacityThreshold " + + std::to_string(options.opacityThreshold) + + " removed every Gaussian in the file."); return false; } cloud->gaussianCount = kept; diff --git a/plugins/gaussian-ply/src/io/GaussianPlyImportOptions.h b/plugins/gaussian-ply/src/io/GaussianPlyImportOptions.h index b228ac8..c61caea 100644 --- a/plugins/gaussian-ply/src/io/GaussianPlyImportOptions.h +++ b/plugins/gaussian-ply/src/io/GaussianPlyImportOptions.h @@ -28,20 +28,17 @@ struct GaussianPlyImportOptions { // Validates and extracts the options this plugin defines from a file-format // argument map. Unknown keys are ignored (USD and hosts may add their own); // known keys with unparseable or out-of-range values are errors. -bool ParseImportOptions( - const std::map& arguments, - GaussianPlyImportOptions* options, - std::string* error = nullptr); +bool ParseImportOptions(const std::map& arguments, + GaussianPlyImportOptions* options, + std::string* error = nullptr); // The SH degree the imported stage will carry for a given source degree. -int EffectiveShDegree( - const GaussianPlyImportOptions& options, int sourceDegree); +int EffectiveShDegree(const GaussianPlyImportOptions& options, + int sourceDegree); // Applies the options to a decoded cloud in place. Fails if opacityThreshold // removes every Gaussian. -bool ApplyImportOptions( - const GaussianPlyImportOptions& options, - GaussianCloudData* cloud, - std::string* error = nullptr); +bool ApplyImportOptions(const GaussianPlyImportOptions& options, + GaussianCloudData* cloud, std::string* error = nullptr); } // namespace openstrata::gs::ply diff --git a/plugins/gaussian-ply/src/io/PlyReader.cpp b/plugins/gaussian-ply/src/io/PlyReader.cpp index 3b9883b..a6db59b 100644 --- a/plugins/gaussian-ply/src/io/PlyReader.cpp +++ b/plugins/gaussian-ply/src/io/PlyReader.cpp @@ -20,25 +20,33 @@ namespace { PlyScalarType ConvertType(tinyply::Type type) noexcept { switch (type) { - case tinyply::Type::INT8: return PlyScalarType::Int8; - case tinyply::Type::UINT8: return PlyScalarType::UInt8; - case tinyply::Type::INT16: return PlyScalarType::Int16; - case tinyply::Type::UINT16: return PlyScalarType::UInt16; - case tinyply::Type::INT32: return PlyScalarType::Int32; - case tinyply::Type::UINT32: return PlyScalarType::UInt32; - case tinyply::Type::FLOAT32: return PlyScalarType::Float32; - case tinyply::Type::FLOAT64: return PlyScalarType::Float64; - default: return PlyScalarType::Invalid; + case tinyply::Type::INT8: + return PlyScalarType::Int8; + case tinyply::Type::UINT8: + return PlyScalarType::UInt8; + case tinyply::Type::INT16: + return PlyScalarType::Int16; + case tinyply::Type::UINT16: + return PlyScalarType::UInt16; + case tinyply::Type::INT32: + return PlyScalarType::Int32; + case tinyply::Type::UINT32: + return PlyScalarType::UInt32; + case tinyply::Type::FLOAT32: + return PlyScalarType::Float32; + case tinyply::Type::FLOAT64: + return PlyScalarType::Float64; + default: + return PlyScalarType::Invalid; } } -bool BuildHeader( - const tinyply::PlyFile& file, - PlyHeader* header, - std::string* error) +bool BuildHeader(const tinyply::PlyFile& file, PlyHeader* header, + std::string* error) { if (!header) { - if (error) *error = "PLY reader received a null header output."; + if (error) + *error = "PLY reader received a null header output."; return false; } @@ -47,14 +55,16 @@ bool BuildHeader( for (const tinyply::PlyElement& element : elements) { if (element.name == "vertex") { if (vertex) { - if (error) *error = "PLY header contains duplicate vertex elements."; + if (error) + *error = "PLY header contains duplicate vertex elements."; return false; } vertex = &element; } } if (!vertex) { - if (error) *error = "PLY header does not contain a vertex element."; + if (error) + *error = "PLY header does not contain a vertex element."; return false; } @@ -67,7 +77,7 @@ bool BuildHeader( if (!names.insert(property.name).second) { if (error) { *error = "PLY vertex element contains duplicate property '" + - property.name + "'."; + property.name + "'."; } return false; } @@ -94,8 +104,7 @@ float NarrowToFloat(double value) noexcept return static_cast(value); } -template -std::vector CopyValues(tinyply::PlyData& data) +template std::vector CopyValues(tinyply::PlyData& data) { std::vector result(data.count); const std::uint8_t* bytes = data.buffer.get_const(); @@ -115,31 +124,48 @@ std::vector CopyValues(tinyply::PlyData& data) return result; } -bool CopyValues( - tinyply::PlyData& data, - std::vector* values, - std::string* error) +bool CopyValues(tinyply::PlyData& data, std::vector* values, + std::string* error) { if (!values || data.isList) { - if (error) *error = "Requested PLY property is not a scalar array."; + if (error) + *error = "Requested PLY property is not a scalar array."; return false; } switch (data.t) { - case tinyply::Type::INT8: *values = CopyValues(data); return true; - case tinyply::Type::UINT8: *values = CopyValues(data); return true; - case tinyply::Type::INT16: *values = CopyValues(data); return true; - case tinyply::Type::UINT16: *values = CopyValues(data); return true; - case tinyply::Type::INT32: *values = CopyValues(data); return true; - case tinyply::Type::UINT32: *values = CopyValues(data); return true; - case tinyply::Type::FLOAT32: *values = CopyValues(data); return true; - case tinyply::Type::FLOAT64: *values = CopyValues(data); return true; + case tinyply::Type::INT8: + *values = CopyValues(data); + return true; + case tinyply::Type::UINT8: + *values = CopyValues(data); + return true; + case tinyply::Type::INT16: + *values = CopyValues(data); + return true; + case tinyply::Type::UINT16: + *values = CopyValues(data); + return true; + case tinyply::Type::INT32: + *values = CopyValues(data); + return true; + case tinyply::Type::UINT32: + *values = CopyValues(data); + return true; + case tinyply::Type::FLOAT32: + *values = CopyValues(data); + return true; + case tinyply::Type::FLOAT64: + *values = CopyValues(data); + return true; default: - if (error) *error = "Requested PLY property has an unsupported type."; + if (error) + *error = "Requested PLY property has an unsupported type."; return false; } } -std::string ExceptionMessage(const char* context, const std::exception& exception) +std::string ExceptionMessage(const char* context, + const std::exception& exception) { std::ostringstream message; message << context << ": " << exception.what(); @@ -148,49 +174,52 @@ std::string ExceptionMessage(const char* context, const std::exception& exceptio } // namespace -bool PlyReader::ReadHeader( - const std::string& path, - PlyHeader* header, - std::string* error) const +bool PlyReader::ReadHeader(const std::string& path, PlyHeader* header, + std::string* error) const { std::ifstream stream(path, std::ios::binary); if (!stream) { - if (error) *error = "Could not open PLY file '" + path + "'."; + if (error) + *error = "Could not open PLY file '" + path + "'."; return false; } try { tinyply::PlyFile file; if (!file.parse_header(stream)) { - if (error) *error = "Could not parse the PLY header."; + if (error) + *error = "Could not parse the PLY header."; return false; } return BuildHeader(file, header, error); } catch (const std::exception& exception) { - if (error) *error = ExceptionMessage("Could not parse the PLY header", exception); + if (error) + *error = + ExceptionMessage("Could not parse the PLY header", exception); return false; } } -bool PlyReader::Read( - const std::string& path, - const std::vector& requestedProperties, - PlyDocument* document, - std::string* error) const +bool PlyReader::Read(const std::string& path, + const std::vector& requestedProperties, + PlyDocument* document, std::string* error) const { if (!document) { - if (error) *error = "PLY reader received a null document output."; + if (error) + *error = "PLY reader received a null document output."; return false; } std::ifstream stream(path, std::ios::binary); if (!stream) { - if (error) *error = "Could not open PLY file '" + path + "'."; + if (error) + *error = "Could not open PLY file '" + path + "'."; return false; } try { tinyply::PlyFile file; if (!file.parse_header(stream)) { - if (error) *error = "Could not parse the PLY header."; + if (error) + *error = "Could not parse the PLY header."; return false; } @@ -202,20 +231,21 @@ bool PlyReader::Read( std::map> requested; for (const std::string& name : requestedProperties) { requested.emplace( - name, - file.request_properties_from_element("vertex", {name})); + name, file.request_properties_from_element("vertex", {name})); } file.read(stream); if (stream.fail() || stream.bad()) { - if (error) *error = "PLY payload is truncated or unreadable."; + if (error) + *error = "PLY payload is truncated or unreadable."; return false; } for (const auto& entry : requested) { const std::shared_ptr& data = entry.second; if (!data) { - if (error) *error = "tinyPLY returned no data for property '" + - entry.first + "'."; + if (error) + *error = "tinyPLY returned no data for property '" + + entry.first + "'."; return false; } std::vector values; @@ -229,7 +259,7 @@ bool PlyReader::Read( if (values.size() != result.header.vertexCount) { if (error) { *error = "PLY property '" + entry.first + - "' count does not match the vertex count."; + "' count does not match the vertex count."; } return false; } @@ -239,7 +269,9 @@ bool PlyReader::Read( *document = std::move(result); return true; } catch (const std::exception& exception) { - if (error) *error = ExceptionMessage("Could not read the PLY payload", exception); + if (error) + *error = + ExceptionMessage("Could not read the PLY payload", exception); return false; } } diff --git a/plugins/gaussian-ply/src/io/PlyReader.h b/plugins/gaussian-ply/src/io/PlyReader.h index 5ed5601..0f905b8 100644 --- a/plugins/gaussian-ply/src/io/PlyReader.h +++ b/plugins/gaussian-ply/src/io/PlyReader.h @@ -43,17 +43,13 @@ struct PlyDocument { // become +/-infinity so the decoder's finiteness validation still rejects // out-of-range source data. class PlyReader { -public: - bool ReadHeader( - const std::string& path, - PlyHeader* header, - std::string* error = nullptr) const; - - bool Read( - const std::string& path, - const std::vector& requestedProperties, - PlyDocument* document, - std::string* error = nullptr) const; + public: + bool ReadHeader(const std::string& path, PlyHeader* header, + std::string* error = nullptr) const; + + bool Read(const std::string& path, + const std::vector& requestedProperties, + PlyDocument* document, std::string* error = nullptr) const; }; } // namespace openstrata::gs::ply diff --git a/plugins/gaussian-ply/tests/test_gaussian_ply_decoder.cpp b/plugins/gaussian-ply/tests/test_gaussian_ply_decoder.cpp index 65c2149..870dee7 100644 --- a/plugins/gaussian-ply/tests/test_gaussian_ply_decoder.cpp +++ b/plugins/gaussian-ply/tests/test_gaussian_ply_decoder.cpp @@ -20,11 +20,13 @@ namespace { int failures = 0; -#define CHECK(expr) \ - do { if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ - ++failures; \ - } } while (false) +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ + ++failures; \ + } \ + } while (false) std::string Fixture(const char* name) { @@ -91,9 +93,9 @@ void TestShLayout() // decoder's use of the shared FlipYZAxes helper is checked against an // independent copy rather than against itself. constexpr float kExpectedShFlip[15] = { - -1.0f, -1.0f, +1.0f, // band 1 - -1.0f, +1.0f, +1.0f, -1.0f, +1.0f, // band 2 - -1.0f, +1.0f, -1.0f, -1.0f, +1.0f, -1.0f, // band 3 + -1.0f, -1.0f, +1.0f, // band 1 + -1.0f, +1.0f, +1.0f, -1.0f, +1.0f, // band 2 + -1.0f, +1.0f, -1.0f, -1.0f, +1.0f, -1.0f, // band 3 +1.0f, }; @@ -113,8 +115,8 @@ void TestHighDegreeShLayout(const char* fixture, int degree) CHECK(cloud.restCoefficients.size() == restCount); for (std::size_t c = 0; c < restCount; ++c) { const float flip = kExpectedShFlip[c]; - CHECK(CloseF3(cloud.restCoefficients[c], - flip * (1.0f + c), flip * (101.0f + c), flip * (201.0f + c))); + CHECK(CloseF3(cloud.restCoefficients[c], flip * (1.0f + c), + flip * (101.0f + c), flip * (201.0f + c))); } // The DC term is frame-flip invariant. CHECK(CloseF3(cloud.dcCoefficients[0], 0.1f, 0.2f, 0.3f)); @@ -128,21 +130,20 @@ void TestReorderedProperties() gs::GaussianCloudData canonical; gs::GaussianCloudData reordered; std::string error; - CHECK(decoder.Decode( - Fixture("degree-1-sh.ply"), &canonical, nullptr, &error)); - CHECK(decoder.Decode( - Fixture("reordered-properties.ply"), &reordered, nullptr, &error)); + CHECK(decoder.Decode(Fixture("degree-1-sh.ply"), &canonical, nullptr, + &error)); + CHECK(decoder.Decode(Fixture("reordered-properties.ply"), &reordered, + nullptr, &error)); CHECK(canonical.gaussianCount == reordered.gaussianCount); CHECK(canonical.shDegree == reordered.shDegree); - CHECK(canonical.restCoefficients.size() == reordered.restCoefficients.size()); + CHECK(canonical.restCoefficients.size() == + reordered.restCoefficients.size()); for (std::size_t i = 0; i < canonical.restCoefficients.size(); ++i) { const gs::Float3& a = canonical.restCoefficients[i]; CHECK(CloseF3(reordered.restCoefficients[i], a.x, a.y, a.z)); } - CHECK(CloseF3(reordered.positions[0], - canonical.positions[0].x, - canonical.positions[0].y, - canonical.positions[0].z)); + CHECK(CloseF3(reordered.positions[0], canonical.positions[0].x, + canonical.positions[0].y, canonical.positions[0].z)); CHECK(Close(reordered.opacities[0], canonical.opacities[0])); } @@ -151,8 +152,8 @@ void TestThreeGaussians() gs::ply::GaussianPlyDecoder decoder; gs::GaussianCloudData cloud; std::string error; - CHECK(decoder.Decode( - Fixture("three-gaussian-binary-le.ply"), &cloud, nullptr, &error)); + CHECK(decoder.Decode(Fixture("three-gaussian-binary-le.ply"), &cloud, + nullptr, &error)); CHECK(cloud.gaussianCount == 3); CHECK(cloud.shDegree == 1); @@ -160,12 +161,9 @@ void TestThreeGaussians() CHECK(CloseF3(cloud.positions[1], -4.0f, -5.0f, 6.0f)); CHECK(CloseF3(cloud.positions[2], 7.0f, 8.0f, -9.0f)); - CHECK(CloseF3(cloud.scales[0], - 1.0f, std::exp(0.5f), std::exp(1.0f))); - CHECK(CloseF3(cloud.scales[1], - std::exp(-1.0f), 1.0f, std::exp(0.25f))); - CHECK(CloseF3(cloud.scales[2], - std::exp(2.0f), std::exp(-0.5f), 1.0f)); + CHECK(CloseF3(cloud.scales[0], 1.0f, std::exp(0.5f), std::exp(1.0f))); + CHECK(CloseF3(cloud.scales[1], std::exp(-1.0f), 1.0f, std::exp(0.25f))); + CHECK(CloseF3(cloud.scales[2], std::exp(2.0f), std::exp(-0.5f), 1.0f)); // The frame flip negates the quaternion's j/k components only. CHECK(Close(cloud.rotations[1].real, 0.5f)); @@ -193,20 +191,20 @@ void TestMetadata() gs::ply::GaussianPlyMetadata metadata; std::string error; - CHECK(decoder.DecodeMetadata( - Fixture("degree-3-sh.ply"), &metadata, &error)); + CHECK( + decoder.DecodeMetadata(Fixture("degree-3-sh.ply"), &metadata, &error)); CHECK(metadata.gaussianCount == 1); CHECK(metadata.shDegree == 3); - CHECK(decoder.DecodeMetadata( - Fixture("three-gaussian-binary-le.ply"), &metadata, &error)); + CHECK(decoder.DecodeMetadata(Fixture("three-gaussian-binary-le.ply"), + &metadata, &error)); CHECK(metadata.gaussianCount == 3); CHECK(metadata.shDegree == 1); // Header validation applies unchanged on the metadata path. error.clear(); - CHECK(!decoder.DecodeMetadata( - Fixture("duplicate-sh-index.ply"), &metadata, &error)); + CHECK(!decoder.DecodeMetadata(Fixture("duplicate-sh-index.ply"), &metadata, + &error)); CHECK(HasCode(error, "GSPLY-E006")); } @@ -233,10 +231,11 @@ void TestFailures() for (const auto& [fixture, code] : expectedCodes) { gs::GaussianCloudData cloud; std::string error; - CHECK(!decoder.Decode(Fixture(fixture.c_str()), &cloud, nullptr, &error)); + CHECK( + !decoder.Decode(Fixture(fixture.c_str()), &cloud, nullptr, &error)); if (!HasCode(error, code)) { - std::cerr << fixture << ": expected " << code - << ", got: " << error << "\n"; + std::cerr << fixture << ": expected " << code << ", got: " << error + << "\n"; ++failures; } } @@ -247,22 +246,22 @@ void TestFailures() { gs::GaussianCloudData cloud; std::string error; - CHECK(!decoder.Decode( - Fixture("mesh-not-gaussian.ply"), &cloud, nullptr, &error)); + CHECK(!decoder.Decode(Fixture("mesh-not-gaussian.ply"), &cloud, nullptr, + &error)); CHECK(error.find("not a supported Gaussian") != std::string::npos); error.clear(); - CHECK(!decoder.Decode( - Fixture("missing-opacity.ply"), &cloud, nullptr, &error)); + CHECK(!decoder.Decode(Fixture("missing-opacity.ply"), &cloud, nullptr, + &error)); CHECK(error.find("opacity") != std::string::npos); error.clear(); // A truncated payload fails inside the container or column pass; the // exact stage is an implementation detail but the code family is not. - CHECK(!decoder.Decode( - Fixture("truncated-binary-le.ply"), &cloud, nullptr, &error)); + CHECK(!decoder.Decode(Fixture("truncated-binary-le.ply"), &cloud, + nullptr, &error)); CHECK(error.rfind("[GSPLY-E", 0) == 0); error.clear(); - CHECK(!decoder.Decode( - Fixture("out-of-range-double.ply"), &cloud, nullptr, &error)); + CHECK(!decoder.Decode(Fixture("out-of-range-double.ply"), &cloud, + nullptr, &error)); CHECK(error.find("non-finite or out-of-range") != std::string::npos); } } @@ -277,10 +276,11 @@ void TestImportOptionParsing() CHECK(options.opacityThreshold < 0.0f); CHECK(Close(options.scaleMultiplier, 1.0f)); - CHECK(gs::ply::ParseImportOptions( - {{"shDegree", "2"}, {"opacityThreshold", "0.25"}, - {"scaleMultiplier", "1.5"}, {"unrelatedHostArg", "x"}}, - &options, &error)); + CHECK(gs::ply::ParseImportOptions({{"shDegree", "2"}, + {"opacityThreshold", "0.25"}, + {"scaleMultiplier", "1.5"}, + {"unrelatedHostArg", "x"}}, + &options, &error)); CHECK(options.shDegree == 2); CHECK(Close(options.opacityThreshold, 0.25f)); CHECK(Close(options.scaleMultiplier, 1.5f)); @@ -301,10 +301,9 @@ void TestImportOptionParsing() error.clear(); if (gs::ply::ParseImportOptions(arguments, &options, &error) || !HasCode(error, "GSPLY-E201")) { - std::cerr << "expected GSPLY-E201 for '" - << arguments.begin()->first << "' = '" - << arguments.begin()->second << "', got: " - << error << "\n"; + std::cerr << "expected GSPLY-E201 for '" << arguments.begin()->first + << "' = '" << arguments.begin()->second + << "', got: " << error << "\n"; ++failures; } } @@ -317,19 +316,19 @@ void TestImportOptionApplication() { gs::GaussianCloudData cloud; - CHECK(decoder.Decode( - Fixture("three-gaussian-binary-le.ply"), &cloud, nullptr, &error)); + CHECK(decoder.Decode(Fixture("three-gaussian-binary-le.ply"), &cloud, + nullptr, &error)); gs::ply::GaussianPlyImportOptions options; options.scaleMultiplier = 2.0f; CHECK(gs::ply::ApplyImportOptions(options, &cloud, &error)); - CHECK(CloseF3(cloud.scales[0], - 2.0f, 2.0f * std::exp(0.5f), 2.0f * std::exp(1.0f))); + CHECK(CloseF3(cloud.scales[0], 2.0f, 2.0f * std::exp(0.5f), + 2.0f * std::exp(1.0f))); } { gs::GaussianCloudData cloud; - CHECK(decoder.Decode( - Fixture("three-gaussian-binary-le.ply"), &cloud, nullptr, &error)); + CHECK(decoder.Decode(Fixture("three-gaussian-binary-le.ply"), &cloud, + nullptr, &error)); gs::ply::GaussianPlyImportOptions options; options.opacityThreshold = 0.4f; CHECK(gs::ply::ApplyImportOptions(options, &cloud, &error)); @@ -345,8 +344,8 @@ void TestImportOptionApplication() { gs::GaussianCloudData cloud; - CHECK(decoder.Decode( - Fixture("three-gaussian-binary-le.ply"), &cloud, nullptr, &error)); + CHECK(decoder.Decode(Fixture("three-gaussian-binary-le.ply"), &cloud, + nullptr, &error)); gs::ply::GaussianPlyImportOptions options; options.shDegree = 0; CHECK(gs::ply::ApplyImportOptions(options, &cloud, &error)); @@ -358,8 +357,8 @@ void TestImportOptionApplication() { // Requesting a higher degree than the source never upsamples. gs::GaussianCloudData cloud; - CHECK(decoder.Decode( - Fixture("three-gaussian-binary-le.ply"), &cloud, nullptr, &error)); + CHECK(decoder.Decode(Fixture("three-gaussian-binary-le.ply"), &cloud, + nullptr, &error)); gs::ply::GaussianPlyImportOptions options; options.shDegree = 3; CHECK(gs::ply::ApplyImportOptions(options, &cloud, &error)); @@ -380,8 +379,8 @@ void TestImportOptionApplication() for (std::size_t i = 0; i < cloud.gaussianCount; ++i) { for (std::size_t c = 0; c < oldRest; ++c) { const float base = 100.0f * i + c; - cloud.restCoefficients[i * oldRest + c] = - {base, base + 0.25f, base + 0.5f}; + cloud.restCoefficients[i * oldRest + c] = {base, base + 0.25f, + base + 0.5f}; } } gs::ply::GaussianPlyImportOptions options; @@ -392,16 +391,16 @@ void TestImportOptionApplication() for (std::size_t i = 0; i < 2; ++i) { for (std::size_t c = 0; c < 3; ++c) { const float base = 100.0f * i + c; - CHECK(CloseF3(cloud.restCoefficients[i * 3 + c], - base, base + 0.25f, base + 0.5f)); + CHECK(CloseF3(cloud.restCoefficients[i * 3 + c], base, + base + 0.25f, base + 0.5f)); } } } { gs::GaussianCloudData cloud; - CHECK(decoder.Decode( - Fixture("three-gaussian-binary-le.ply"), &cloud, nullptr, &error)); + CHECK(decoder.Decode(Fixture("three-gaussian-binary-le.ply"), &cloud, + nullptr, &error)); gs::ply::GaussianPlyImportOptions options; options.opacityThreshold = 0.95f; error.clear(); @@ -439,7 +438,7 @@ void TestImportStats() CHECK(stats.opacityThresholdRejectedCount == 0); CHECK(stats.warningCount == warnings.size()); CHECK(stats.sourceBytes == - static_cast(std::filesystem::file_size(path))); + static_cast(std::filesystem::file_size(path))); CHECK(stats.decodedBytes == gs::ComputeDecodedByteSize(cloud)); CHECK(stats.readSeconds >= 0.0 && stats.decodeSeconds >= 0.0); // The decoder does not time authoring and does not compute bounds; both @@ -449,7 +448,7 @@ void TestImportStats() gs::GaussianImportStats binaryStats; CHECK(decoder.Decode(Fixture("one-gaussian-binary-le.ply"), &cloud, - &warnings, &error, &binaryStats)); + &warnings, &error, &binaryStats)); CHECK(binaryStats.sourceVersion == "binary_little_endian"); } @@ -478,7 +477,7 @@ void TestContractConformance() continue; } for (const std::string& violation : - gs::testing::CheckCloudContract(cloud)) { + gs::testing::CheckCloudContract(cloud)) { std::cerr << fixture << ": " << violation << '\n'; ++failures; } diff --git a/plugins/gaussian-sog/src/GaussianSogFileFormat.cpp b/plugins/gaussian-sog/src/GaussianSogFileFormat.cpp index 26185f4..9fc3d69 100644 --- a/plugins/gaussian-sog/src/GaussianSogFileFormat.cpp +++ b/plugins/gaussian-sog/src/GaussianSogFileFormat.cpp @@ -28,7 +28,8 @@ PXR_NAMESPACE_OPEN_SCOPE -TF_DEFINE_PUBLIC_TOKENS(GaussianSogFileFormatTokens, GAUSSIANSOG_FILE_FORMAT_TOKENS); +TF_DEFINE_PUBLIC_TOKENS(GaussianSogFileFormatTokens, + GAUSSIANSOG_FILE_FORMAT_TOKENS); namespace { @@ -45,11 +46,10 @@ constexpr const char* kSourceFormat = gssog::kSourceFormatToken; // path. That is what lets a search-path or packaged resolver find the planes of // a `meta.json` it resolved itself. The primary asset still arrives as a // resolved path and is read directly, exactly as in the PLY and SPZ bundles. -bool LoadCompanionThroughResolver( - const std::string& anchorPath, - const std::string& planeName, - std::vector* bytes, - std::string* error) +bool LoadCompanionThroughResolver(const std::string& anchorPath, + const std::string& planeName, + std::vector* bytes, + std::string* error) { const auto fail = [error](const char* code, const std::string& message) { if (error) { @@ -65,23 +65,28 @@ bool LoadCompanionThroughResolver( resolver.Resolve(identifier.empty() ? planeName : identifier); if (!resolved) { return fail(gssog::diag::kMissingPlane, - "The property plane '" + planeName + "' declared by meta.json " - "could not be resolved relative to '" + anchorPath + "'."); + "The property plane '" + planeName + + "' declared by meta.json " + "could not be resolved relative to '" + + anchorPath + "'."); } const std::shared_ptr asset = resolver.OpenAsset(resolved); if (!asset) { return fail(gssog::diag::kUnreadableFile, - "The property plane '" + planeName + "' resolved to '" + - resolved.GetPathString() + "' but could not be opened."); + "The property plane '" + planeName + "' resolved to '" + + resolved.GetPathString() + + "' but could not be opened."); } const std::size_t size = asset->GetSize(); try { bytes->resize(size); } catch (const std::exception&) { return fail(gssog::diag::kUnreadableFile, - "A " + std::to_string(size) + "-byte buffer for the property " - "plane '" + planeName + "' could not be allocated."); + "A " + std::to_string(size) + + "-byte buffer for the property " + "plane '" + + planeName + "' could not be allocated."); } if (size == 0) { return true; @@ -94,8 +99,9 @@ bool LoadCompanionThroughResolver( } if (asset->Read(bytes->data(), size, 0) != size) { return fail(gssog::diag::kUnreadableFile, - "The property plane '" + planeName + "' could not be read from '" + - resolved.GetPathString() + "'."); + "The property plane '" + planeName + + "' could not be read from '" + + resolved.GetPathString() + "'."); } return true; } @@ -123,26 +129,24 @@ TF_DEBUG_CODES(GSSOG_IMPORT_STATS); TF_REGISTRY_FUNCTION(TfDebug) { - TF_DEBUG_ENVIRONMENT_SYMBOL(GSSOG_IMPORT_STATS, + TF_DEBUG_ENVIRONMENT_SYMBOL( + GSSOG_IMPORT_STATS, "gaussian-sog: one line of per-import statistics through the shared " "GaussianImportStats seam"); } GaussianSogFileFormat::GaussianSogFileFormat() - : SdfFileFormat( - GaussianSogFileFormatTokens->Id, - GaussianSogFileFormatTokens->Version, - GaussianSogFileFormatTokens->Target, - std::vector{ - GaussianSogFileFormatTokens->Extension.GetString(), - GaussianSogFileFormatTokens->MetaExtension.GetString()}) -{ -} + : SdfFileFormat(GaussianSogFileFormatTokens->Id, + GaussianSogFileFormatTokens->Version, + GaussianSogFileFormatTokens->Target, + std::vector{ + GaussianSogFileFormatTokens->Extension.GetString(), + GaussianSogFileFormatTokens->MetaExtension.GetString()}) +{} GaussianSogFileFormat::~GaussianSogFileFormat() = default; -bool -GaussianSogFileFormat::CanRead(const std::string& file) const +bool GaussianSogFileFormat::CanRead(const std::string& file) const { // The two layouts have two different gates (SOG_FORMAT.md §6): a bundled // `.sog` is claimed by the ZIP signature, while the far broader `.json` @@ -161,11 +165,9 @@ GaussianSogFileFormat::CanRead(const std::string& file) const return false; } -bool -GaussianSogFileFormat::Read( - SdfLayer* layer, - const std::string& resolvedPath, - bool metadataOnly) const +bool GaussianSogFileFormat::Read(SdfLayer* layer, + const std::string& resolvedPath, + bool metadataOnly) const { const gssog::GaussianSogDecoder decoder = MakeDecoder(); std::string error; @@ -175,16 +177,19 @@ GaussianSogFileFormat::Read( // GSSOG-E1xx codes. The struct is six same-typed pointers, so it is // assigned by name: a positional list would let a swapped pair compile // silently and emit the wrong code to users (see GaussianLayerWriter.h). - static const openstrata::gs::usd::LayerWriterDiagnosticCodes kWriterCodes = [] { - openstrata::gs::usd::LayerWriterDiagnosticCodes codes; - codes.internalError = gssog::diag::kInternalError; - codes.cloudValidationFailed = gssog::diag::kCloudValidationFailed; - codes.stageCreationFailed = gssog::diag::kStageCreationFailed; - codes.scaffoldAuthoringFailed = gssog::diag::kScaffoldAuthoringFailed; - codes.attributeAuthoringFailed = gssog::diag::kAttributeAuthoringFailed; - codes.extentOverflow = gssog::diag::kExtentOverflow; - return codes; - }(); + static const openstrata::gs::usd::LayerWriterDiagnosticCodes kWriterCodes = + [] { + openstrata::gs::usd::LayerWriterDiagnosticCodes codes; + codes.internalError = gssog::diag::kInternalError; + codes.cloudValidationFailed = gssog::diag::kCloudValidationFailed; + codes.stageCreationFailed = gssog::diag::kStageCreationFailed; + codes.scaffoldAuthoringFailed = + gssog::diag::kScaffoldAuthoringFailed; + codes.attributeAuthoringFailed = + gssog::diag::kAttributeAuthoringFailed; + codes.extentOverflow = gssog::diag::kExtentOverflow; + return codes; + }(); const openstrata::gs::usd::GaussianLayerWriter writer(kWriterCodes); // Sdf reload executes under an outer SdfChangeBlock. Authoring a detached @@ -199,20 +204,18 @@ GaussianSogFileFormat::Read( // decoded. gssog::GaussianSogMetadata metadata; if (!decoder.DecodeMetadata(resolvedPath, &metadata, &error)) { - TF_RUNTIME_ERROR( - "gaussian-sog: failed to read '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-sog: failed to read '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } auto task = std::async(std::launch::async, [&]() { - return writer.WriteMetadataToLayer( - metadata.gaussianCount, metadata.shDegree, - kSourceFormat, &generated, &error); + return writer.WriteMetadataToLayer(metadata.gaussianCount, + metadata.shDegree, kSourceFormat, + &generated, &error); }); if (!task.get()) { - TF_RUNTIME_ERROR( - "gaussian-sog: failed to author USD for '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-sog: failed to author USD for '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } layer->TransferContent(generated); @@ -228,9 +231,8 @@ GaussianSogFileFormat::Read( openstrata::gs::GaussianCloudData cloud; std::vector warnings; if (!decoder.Decode(resolvedPath, &cloud, &warnings, &error, statsOut)) { - TF_RUNTIME_ERROR( - "gaussian-sog: failed to read '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-sog: failed to read '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } for (const std::string& warning : warnings) { @@ -248,50 +250,49 @@ GaussianSogFileFormat::Read( // authored. const auto authorStart = std::chrono::steady_clock::now(); auto task = std::async(std::launch::async, [&]() { - return writer.WriteToLayer( - std::move(cloud), kSourceFormat, &generated, &error); + return writer.WriteToLayer(std::move(cloud), kSourceFormat, &generated, + &error); }); if (!task.get()) { - TF_RUNTIME_ERROR( - "gaussian-sog: failed to author USD for '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-sog: failed to author USD for '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } if (statsOut) { - stats.authorSeconds = std::chrono::duration( - std::chrono::steady_clock::now() - authorStart).count(); - TF_DEBUG(GSSOG_IMPORT_STATS).Msg("gaussian-sog: %s\n", - openstrata::gs::FormatImportStats(stats).c_str()); + stats.authorSeconds = + std::chrono::duration(std::chrono::steady_clock::now() - + authorStart) + .count(); + TF_DEBUG(GSSOG_IMPORT_STATS) + .Msg("gaussian-sog: %s\n", + openstrata::gs::FormatImportStats(stats).c_str()); } layer->TransferContent(generated); return true; } -bool -GaussianSogFileFormat::WriteToFile( - const SdfLayer& layer, - const std::string& filePath, - const std::string& comment, - const FileFormatArguments& args) const +bool GaussianSogFileFormat::WriteToFile(const SdfLayer& layer, + const std::string& filePath, + const std::string& comment, + const FileFormatArguments& args) const { (void)layer; (void)filePath; (void)comment; (void)args; - TF_RUNTIME_ERROR("%s", - gssog::diag::Format( - gssog::diag::kWriteUnsupported, - "gaussian-sog is read-only; USD to SOG writing is " - "unsupported").c_str()); + TF_RUNTIME_ERROR( + "%s", + gssog::diag::Format(gssog::diag::kWriteUnsupported, + "gaussian-sog is read-only; USD to SOG writing is " + "unsupported") + .c_str()); return false; } -bool -GaussianSogFileFormat::WriteToString( - const SdfLayer& layer, - std::string* str, - const std::string& comment) const +bool GaussianSogFileFormat::WriteToString(const SdfLayer& layer, + std::string* str, + const std::string& comment) const { SdfFileFormatConstPtr usda = SdfFileFormat::FindByExtension("usda"); if (usda) { diff --git a/plugins/gaussian-sog/src/GaussianSogFileFormat.h b/plugins/gaussian-sog/src/GaussianSogFileFormat.h index e80e718..b378118 100644 --- a/plugins/gaussian-sog/src/GaussianSogFileFormat.h +++ b/plugins/gaussian-sog/src/GaussianSogFileFormat.h @@ -13,14 +13,12 @@ PXR_NAMESPACE_OPEN_SCOPE // `meta.json`. The broad `json` registration is deliberate and maintainer // ratified — it is the stock unbundled layout's own file name — and is kept // honest by a strict `CanRead()` gate rather than by the extension. -#define GAUSSIANSOG_FILE_FORMAT_TOKENS \ - ((Id, "sog")) \ - ((Version, "1.0")) \ - ((Target, "usd")) \ - ((Extension, "sog")) \ - ((MetaExtension, "json")) +#define GAUSSIANSOG_FILE_FORMAT_TOKENS \ + ((Id, "sog"))((Version, "1.0"))((Target, "usd"))((Extension, "sog"))( \ + (MetaExtension, "json")) -TF_DECLARE_PUBLIC_TOKENS(GaussianSogFileFormatTokens, GAUSSIANSOG_FILE_FORMAT_TOKENS); +TF_DECLARE_PUBLIC_TOKENS(GaussianSogFileFormatTokens, + GAUSSIANSOG_FILE_FORMAT_TOKENS); /// Reads SOG v2 — PlayCanvas "Splat Object Graphics" — into the shared Gaussian /// model and authors it through the one shared `GaussianLayerWriter`, so the @@ -28,20 +26,19 @@ TF_DECLARE_PUBLIC_TOKENS(GaussianSogFileFormatTokens, GAUSSIANSOG_FILE_FORMAT_TO /// import. Container work lives in `SogReader`, semantic decoding in /// `GaussianSogDecoder`; this class is only the `SdfFileFormat` integration. class GaussianSogFileFormat : public SdfFileFormat { -public: + public: bool CanRead(const std::string& file) const override; - bool Read(SdfLayer* layer, const std::string& resolvedPath, bool metadataOnly) const override; + bool Read(SdfLayer* layer, const std::string& resolvedPath, + bool metadataOnly) const override; bool WriteToFile( - const SdfLayer& layer, - const std::string& filePath, + const SdfLayer& layer, const std::string& filePath, const std::string& comment = std::string(), const FileFormatArguments& args = FileFormatArguments()) const override; - bool WriteToString( - const SdfLayer& layer, - std::string* str, - const std::string& comment = std::string()) const override; + bool + WriteToString(const SdfLayer& layer, std::string* str, + const std::string& comment = std::string()) const override; -protected: + protected: SDF_FILE_FORMAT_FACTORY_ACCESS; GaussianSogFileFormat(); diff --git a/plugins/gaussian-sog/src/io/GaussianSogDecoder.cpp b/plugins/gaussian-sog/src/io/GaussianSogDecoder.cpp index 63ad086..3757d30 100644 --- a/plugins/gaussian-sog/src/io/GaussianSogDecoder.cpp +++ b/plugins/gaussian-sog/src/io/GaussianSogDecoder.cpp @@ -59,27 +59,26 @@ float InverseLogTransform(float value) noexcept GaussianSogDecoder::GaussianSogDecoder(SogReader reader) : _reader(std::move(reader)) -{ -} +{} bool GaussianSogDecoder::CanReadBundled(const std::string& path) const noexcept { return _reader.CanReadBundled(path); } -bool GaussianSogDecoder::CanReadUnbundled(const std::string& path) const noexcept +bool GaussianSogDecoder::CanReadUnbundled( + const std::string& path) const noexcept { return _reader.CanReadUnbundled(path); } -bool GaussianSogDecoder::DecodeMetadata( - const std::string& path, - GaussianSogMetadata* metadata, - std::string* error) const +bool GaussianSogDecoder::DecodeMetadata(const std::string& path, + GaussianSogMetadata* metadata, + std::string* error) const { if (!metadata) { SetError(error, diag::kInternalError, - "Gaussian decoder received a null metadata output."); + "Gaussian decoder received a null metadata output."); return false; } @@ -95,16 +94,15 @@ bool GaussianSogDecoder::DecodeMetadata( return true; } -bool GaussianSogDecoder::Decode( - const std::string& path, - GaussianCloudData* cloud, - std::vector* warnings, - std::string* error, - GaussianImportStats* stats) const +bool GaussianSogDecoder::Decode(const std::string& path, + GaussianCloudData* cloud, + std::vector* warnings, + std::string* error, + GaussianImportStats* stats) const { if (!cloud) { SetError(error, diag::kInternalError, - "Gaussian decoder received a null cloud output."); + "Gaussian decoder received a null cloud output."); return false; } @@ -131,9 +129,10 @@ bool GaussianSogDecoder::Decode( document.scalesCodebook.size() != 256 || document.sh0Codebook.size() != 256 || (shDegree != 0 && - (!document.shCentroids.Present() || !document.shLabels.Present() || - document.shCodebook.size() != 256))) { - SetError(error, diag::kInternalError, + (!document.shCentroids.Present() || !document.shLabels.Present() || + document.shCodebook.size() != 256))) { + SetError( + error, diag::kInternalError, "The container document does not match the metadata it declares."); return false; } @@ -148,9 +147,9 @@ bool GaussianSogDecoder::Decode( const auto allocate = [&](auto* array, std::size_t elements) { if (!TryResize(array, elements)) { SetError(error, diag::kModelAllocationFailed, - "SOG model arrays for " + std::to_string(count) + - " Gaussians at SH degree " + std::to_string(shDegree) + - " could not be allocated."); + "SOG model arrays for " + std::to_string(count) + + " Gaussians at SH degree " + std::to_string(shDegree) + + " could not be allocated."); return false; } return true; @@ -182,8 +181,8 @@ bool GaussianSogDecoder::Decode( // range and inverse-log transformed (SOG_MAPPING.md §4). float span[3]; for (int axis = 0; axis < 3; ++axis) { - span[axis] = document.meansMaximum[axis] - - document.meansMinimum[axis]; + span[axis] = + document.meansMaximum[axis] - document.meansMinimum[axis]; } for (std::size_t i = 0; i < count; ++i) { const unsigned char* low = document.meansLow.Gaussian(i); @@ -195,14 +194,17 @@ bool GaussianSogDecoder::Decode( (static_cast(high[axis]) << 8); const float normalized = document.meansMinimum[axis] + - span[axis] * (static_cast(code) / - kPositionCodeMaximum); + span[axis] * + (static_cast(code) / kPositionCodeMaximum); decoded[axis] = InverseLogTransform(normalized); if (!std::isfinite(decoded[axis])) { - SetError(error, diag::kMalformedMetadata, + SetError( + error, diag::kMalformedMetadata, "The position range in meta.json decodes Gaussian " + - std::to_string(i) + " outside the range of a 32-bit " - "float; \"means\".mins/maxs are log-domain bounds."); + std::to_string(i) + + " outside the range of a 32-bit " + "float; \"means\".mins/maxs are log-domain " + "bounds."); return false; } } @@ -218,9 +220,10 @@ bool GaussianSogDecoder::Decode( const float z = linearScales[stored[2]]; if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) { SetError(error, diag::kInvalidCodebook, - "A scales codebook entry used by Gaussian " + - std::to_string(i) + " exponentiates outside the range of a " - "32-bit float; the codebook is log-domain."); + "A scales codebook entry used by Gaussian " + + std::to_string(i) + + " exponentiates outside the range of a " + "32-bit float; the codebook is log-domain."); return false; } result.scales[i] = {x, y, z}; @@ -231,10 +234,9 @@ bool GaussianSogDecoder::Decode( // raw band-0 coefficients, alpha is the already post-sigmoid opacity // (SOG_MAPPING.md §3). const unsigned char* stored = document.sh0.Gaussian(i); - result.dcCoefficients[i] = { - document.sh0Codebook[stored[0]], - document.sh0Codebook[stored[1]], - document.sh0Codebook[stored[2]]}; + result.dcCoefficients[i] = {document.sh0Codebook[stored[0]], + document.sh0Codebook[stored[1]], + document.sh0Codebook[stored[2]]}; result.opacities[i] = static_cast(stored[3]) / 255.0f; } @@ -244,10 +246,10 @@ bool GaussianSogDecoder::Decode( const unsigned char* stored = document.quats.Gaussian(i); if (stored[3] < kQuaternionTagBase) { SetError(error, diag::kMalformedRotation, - "The quaternion of Gaussian " + std::to_string(i) + - " carries the largest-component tag " + - std::to_string(static_cast(stored[3])) + - "; SOG v2 tags are 252-255."); + "The quaternion of Gaussian " + std::to_string(i) + + " carries the largest-component tag " + + std::to_string(static_cast(stored[3])) + + "; SOG v2 tags are 252-255."); return false; } const int dropped = static_cast(stored[3] - kQuaternionTagBase); @@ -271,7 +273,7 @@ bool GaussianSogDecoder::Decode( {components[0], components[1], components[2], components[3]}, &result.rotations[i])) { SetError(error, diag::kInternalError, - "A dequantized quaternion was not normalizable."); + "A dequantized quaternion was not normalizable."); return false; } } @@ -288,9 +290,9 @@ bool GaussianSogDecoder::Decode( std::size_t restLength = 0; if (!ComputeRestCoefficientCount(count, shDegree, &restLength)) { SetError(error, diag::kModelAllocationFailed, - "The model size for " + std::to_string(count) + - " Gaussians at SH degree " + std::to_string(shDegree) + - " overflows this platform's address space."); + "The model size for " + std::to_string(count) + + " Gaussians at SH degree " + std::to_string(shDegree) + + " overflows this platform's address space."); return false; } if (!allocate(&result.restCoefficients, restLength)) { @@ -313,8 +315,8 @@ bool GaussianSogDecoder::Decode( (label % kShCentroidsPerRow) * coefficients; for (std::size_t coefficient = 0; coefficient < coefficients; ++coefficient) { - const unsigned char* texel = document.shCentroids.Texel( - column + coefficient, row); + const unsigned char* texel = + document.shCentroids.Texel(column + coefficient, row); result.restCoefficients[i * coefficients + coefficient] = { document.shCodebook[texel[0]], document.shCodebook[texel[1]], @@ -336,12 +338,13 @@ bool GaussianSogDecoder::Decode( } if (warnings && labelsOutOfRange != 0) { - warnings->push_back(diag::Format(diag::kShLabelsOutOfRange, + warnings->push_back(diag::Format( + diag::kShLabelsOutOfRange, std::to_string(labelsOutOfRange) + " of " + std::to_string(count) + - " spherical-harmonic palette label(s) point past the " + - std::to_string(document.shPaletteCount) + - "-centroid palette; those Gaussians decoded with zero " - "higher-order coefficients.")); + " spherical-harmonic palette label(s) point past the " + + std::to_string(document.shPaletteCount) + + "-centroid palette; those Gaussians decoded with zero " + "higher-order coefficients.")); } if (stats) { diff --git a/plugins/gaussian-sog/src/io/GaussianSogDecoder.h b/plugins/gaussian-sog/src/io/GaussianSogDecoder.h index 44ebfc4..9c06935 100644 --- a/plugins/gaussian-sog/src/io/GaussianSogDecoder.h +++ b/plugins/gaussian-sog/src/io/GaussianSogDecoder.h @@ -34,7 +34,7 @@ struct GaussianSogMetadata { // SogReader; this class consumes its document. The exact mapping is // docs/reference/SOG_MAPPING.md. class GaussianSogDecoder { -public: + public: GaussianSogDecoder() = default; // The reader used for every read below. The file-format plugin injects one // carrying an asset-resolver-backed companion loader; the default reads @@ -51,23 +51,19 @@ class GaussianSogDecoder { // property plane. A zero-Gaussian file fails here exactly as it fails in // Decode(): a metadata read must not promise a decode that would be // rejected. - bool DecodeMetadata( - const std::string& path, - GaussianSogMetadata* metadata, - std::string* error = nullptr) const; + bool DecodeMetadata(const std::string& path, GaussianSogMetadata* metadata, + std::string* error = nullptr) const; // On success, `stats` (optional) carries the decoder's half of the shared // import-statistics record: source format/version, count and degree, byte // sizes, and the read/decode timings. Bounds and the authoring time stay // with the caller. - bool Decode( - const std::string& path, - GaussianCloudData* cloud, - std::vector* warnings = nullptr, - std::string* error = nullptr, - GaussianImportStats* stats = nullptr) const; + bool Decode(const std::string& path, GaussianCloudData* cloud, + std::vector* warnings = nullptr, + std::string* error = nullptr, + GaussianImportStats* stats = nullptr) const; -private: + private: SogReader _reader; }; diff --git a/plugins/gaussian-sog/src/io/SogJson.h b/plugins/gaussian-sog/src/io/SogJson.h index da9a61b..bed5c43 100644 --- a/plugins/gaussian-sog/src/io/SogJson.h +++ b/plugins/gaussian-sog/src/io/SogJson.h @@ -25,22 +25,40 @@ namespace openstrata::gs::sog { class JsonValue { -public: + public: enum class Type { Null, Boolean, Number, String, Array, Object }; Type type = Type::Null; bool boolean = false; double number = 0.0; - std::string text; // Type::String - std::vector items; // Type::Array + std::string text; // Type::String + std::vector items; // Type::Array std::vector> members; // Type::Object - bool IsNull() const noexcept { return type == Type::Null; } - bool IsBoolean() const noexcept { return type == Type::Boolean; } - bool IsNumber() const noexcept { return type == Type::Number; } - bool IsString() const noexcept { return type == Type::String; } - bool IsArray() const noexcept { return type == Type::Array; } - bool IsObject() const noexcept { return type == Type::Object; } + bool IsNull() const noexcept + { + return type == Type::Null; + } + bool IsBoolean() const noexcept + { + return type == Type::Boolean; + } + bool IsNumber() const noexcept + { + return type == Type::Number; + } + bool IsString() const noexcept + { + return type == Type::String; + } + bool IsArray() const noexcept + { + return type == Type::Array; + } + bool IsObject() const noexcept + { + return type == Type::Object; + } // Object member lookup, or null when absent or not an object. Member // counts here are single digits, so a linear scan beats a map. @@ -59,10 +77,7 @@ inline constexpr std::size_t kJsonMaxTokens = 100'000; // Parses `size` bytes as one complete JSON document. Trailing whitespace is // allowed, trailing content is not. `error` receives a bare message with no // diagnostic code: the caller owns which GSSOG code the failure carries. -bool ParseJson( - const char* data, - std::size_t size, - JsonValue* out, - std::string* error); +bool ParseJson(const char* data, std::size_t size, JsonValue* out, + std::string* error); } // namespace openstrata::gs::sog diff --git a/plugins/gaussian-sog/src/io/SogReader.h b/plugins/gaussian-sog/src/io/SogReader.h index 88215d7..edb2bdb 100644 --- a/plugins/gaussian-sog/src/io/SogReader.h +++ b/plugins/gaussian-sog/src/io/SogReader.h @@ -19,7 +19,10 @@ struct SogPlane { std::uint32_t height = 0; std::vector rgba; // width * height * 4 - bool Present() const noexcept { return !rgba.empty(); } + bool Present() const noexcept + { + return !rgba.empty(); + } // The four bytes of the texel holding Gaussian `index`. The caller has // already established `index < width * height` through the reader's @@ -87,10 +90,8 @@ struct SogDocument { // custom resolver resolves them the same way USD resolves any other // companion asset — without dragging OpenUSD into this reader. using SogCompanionLoader = std::function* bytes, - std::string* error)>; + const std::string& anchorPath, const std::string& planeName, + std::vector* bytes, std::string* error)>; // Owns every SOG v2 container concern: layout detection, ZIP central-directory // walking (vendored miniz), `meta.json` parsing and schema validation, @@ -99,7 +100,7 @@ using SogCompanionLoader = std::function warnings; std::string error; - if (!gssog::GaussianSogDecoder().Decode( - Fixture(fixture), &cloud, &warnings, &error)) { + if (!gssog::GaussianSogDecoder().Decode(Fixture(fixture), &cloud, &warnings, + &error)) { std::cerr << fixture << ": decode failed: " << error << "\n"; ++failures; return; @@ -107,7 +111,7 @@ void TestKitRoundTrip(const char* fixture, const gs::GaussianCloudData& expected CHECK(warnings.empty()); CheckContract(cloud); for (const std::string& mismatch : - gs::testing::CompareClouds(cloud, expected, SogTolerances())) { + gs::testing::CompareClouds(cloud, expected, SogTolerances())) { std::cerr << fixture << ": " << mismatch << "\n"; ++failures; } @@ -116,14 +120,14 @@ void TestKitRoundTrip(const char* fixture, const gs::GaussianCloudData& expected void TestKitRoundTrips() { TestKitRoundTrip("kit-one-degree0.sog", - gs::testing::MakeCanonicalOneGaussianCloud()); + gs::testing::MakeCanonicalOneGaussianCloud()); TestKitRoundTrip("kit-multi-degree3.sog", - gs::testing::MakeCanonicalMultiGaussianCloud()); + gs::testing::MakeCanonicalMultiGaussianCloud()); // Both layouts and both ZIP storage methods converge on the same model. TestKitRoundTrip("kit-multi-degree3-deflated.sog", - gs::testing::MakeCanonicalMultiGaussianCloud()); + gs::testing::MakeCanonicalMultiGaussianCloud()); TestKitRoundTrip("unbundled-kit-multi-degree3/meta.json", - gs::testing::MakeCanonicalMultiGaussianCloud()); + gs::testing::MakeCanonicalMultiGaussianCloud()); } // The known source values encoded by tools/generate_fixtures.py @@ -136,8 +140,8 @@ void TestDegree1FullPipeline() gs::GaussianCloudData cloud; std::vector warnings; std::string error; - CHECK(gssog::GaussianSogDecoder().Decode( - Fixture("decode-degree1.sog"), &cloud, &warnings, &error)); + CHECK(gssog::GaussianSogDecoder().Decode(Fixture("decode-degree1.sog"), + &cloud, &warnings, &error)); CHECK(error.empty()); CHECK(warnings.empty()); CheckContract(cloud); @@ -188,23 +192,19 @@ void TestDegree1FullPipeline() // coefficients carry the flip signs (-1, -1, +1), which these values pin: // a lost transpose or a wrong sign table changes them. const std::vector expectedRest = { - {0.1f, 0.2f, 0.3f}, - {-0.1f, -0.2f, -0.3f}, - {0.4f, -0.4f, 0.5f}, - {0.6f, 0.7f, 0.8f}, - {-0.6f, -0.7f, -0.8f}, - {0.9f, -0.9f, 0.25f}, + {0.1f, 0.2f, 0.3f}, {-0.1f, -0.2f, -0.3f}, {0.4f, -0.4f, 0.5f}, + {0.6f, 0.7f, 0.8f}, {-0.6f, -0.7f, -0.8f}, {0.9f, -0.9f, 0.25f}, }; CHECK(cloud.restCoefficients.size() == expectedRest.size()); - for (std::size_t i = 0; i < expectedRest.size() && - i < cloud.restCoefficients.size(); ++i) { + for (std::size_t i = 0; + i < expectedRest.size() && i < cloud.restCoefficients.size(); ++i) { const std::string label = "rest[" + std::to_string(i) + "]"; CheckClose(cloud.restCoefficients[i].x, expectedRest[i].x, 1e-6f, - (label + ".r").c_str()); + (label + ".r").c_str()); CheckClose(cloud.restCoefficients[i].y, expectedRest[i].y, 1e-6f, - (label + ".g").c_str()); + (label + ".g").c_str()); CheckClose(cloud.restCoefficients[i].z, expectedRest[i].z, 1e-6f, - (label + ".b").c_str()); + (label + ".b").c_str()); } } @@ -236,8 +236,8 @@ void TestImportStats() gs::GaussianCloudData cloud; gs::GaussianImportStats stats; std::string error; - CHECK(gssog::GaussianSogDecoder().Decode( - Fixture("kit-multi-degree3.sog"), &cloud, nullptr, &error, &stats)); + CHECK(gssog::GaussianSogDecoder().Decode(Fixture("kit-multi-degree3.sog"), + &cloud, nullptr, &error, &stats)); CHECK(stats.sourceFormat == gssog::kSourceFormatToken); CHECK(stats.sourceVersion == "2"); CHECK(stats.coordinateConversion == @@ -270,8 +270,8 @@ void TestLabelsOutOfRangeWarn() gs::GaussianCloudData cloud; std::vector warnings; std::string error; - CHECK(gssog::GaussianSogDecoder().Decode( - Fixture("labels-out-of-range.sog"), &cloud, &warnings, &error)); + CHECK(gssog::GaussianSogDecoder().Decode(Fixture("labels-out-of-range.sog"), + &cloud, &warnings, &error)); CHECK(error.empty()); CheckContract(cloud); CHECK(warnings.size() == 1); @@ -294,19 +294,19 @@ void TestSemanticRejections() { gs::GaussianCloudData cloud; std::string error; - CHECK(!gssog::GaussianSogDecoder().Decode( - Fixture("bad-quat-tag.sog"), &cloud, nullptr, &error)); + CHECK(!gssog::GaussianSogDecoder().Decode(Fixture("bad-quat-tag.sog"), + &cloud, nullptr, &error)); CHECK(HasCode(error, gssog::diag::kMalformedRotation)); error.clear(); - CHECK(!gssog::GaussianSogDecoder().Decode( - Fixture("kit-one-degree0.sog"), nullptr, nullptr, &error)); + CHECK(!gssog::GaussianSogDecoder().Decode(Fixture("kit-one-degree0.sog"), + nullptr, nullptr, &error)); CHECK(HasCode(error, gssog::diag::kInternalError)); // Container failures surface through the decoder unchanged. error.clear(); - CHECK(!gssog::GaussianSogDecoder().Decode( - Fixture("version-3.sog"), &cloud, nullptr, &error)); + CHECK(!gssog::GaussianSogDecoder().Decode(Fixture("version-3.sog"), &cloud, + nullptr, &error)); CHECK(HasCode(error, gssog::diag::kUnsupportedVersion)); } diff --git a/plugins/gaussian-sog/tests/test_gaussian_sog_reader.cpp b/plugins/gaussian-sog/tests/test_gaussian_sog_reader.cpp index c2d4ec2..25a4a6b 100644 --- a/plugins/gaussian-sog/tests/test_gaussian_sog_reader.cpp +++ b/plugins/gaussian-sog/tests/test_gaussian_sog_reader.cpp @@ -23,11 +23,13 @@ namespace { int failures = 0; -#define CHECK(expr) \ - do { if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ - ++failures; \ - } } while (false) +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ + ++failures; \ + } \ + } while (false) std::string Fixture(const std::string& name) { @@ -73,8 +75,8 @@ void TestRouting() // from claiming unrelated JSON. CHECK(reader.CanReadUnbundled( Fixture("unbundled-kit-multi-degree3/meta.json"))); - CHECK(reader.CanReadUnbundled( - Fixture("unbundled-missing-plane/meta.json"))); + CHECK( + reader.CanReadUnbundled(Fixture("unbundled-missing-plane/meta.json"))); CHECK(!reader.CanReadUnbundled(Fixture("not-sog.json"))); CHECK(!reader.CanReadUnbundled(Fixture("kit-one-degree0.sog"))); CHECK(!reader.CanReadUnbundled(Fixture("no-such-file.json"))); @@ -86,8 +88,8 @@ void TestBundledDegree0() { gssog::SogDocument document; std::string error; - CHECK(gssog::SogReader().Read( - Fixture("kit-one-degree0.sog"), &document, &error)); + CHECK(gssog::SogReader().Read(Fixture("kit-one-degree0.sog"), &document, + &error)); CHECK(error.empty()); CHECK(document.metadata.version == 2); @@ -120,8 +122,8 @@ void TestBundledDegree3() { gssog::SogDocument document; std::string error; - CHECK(gssog::SogReader().Read( - Fixture("kit-multi-degree3.sog"), &document, &error)); + CHECK(gssog::SogReader().Read(Fixture("kit-multi-degree3.sog"), &document, + &error)); CHECK(error.empty()); CHECK(document.metadata.gaussianCount == 3); @@ -153,10 +155,10 @@ void TestDeflatedArchiveMatchesStored() gssog::SogDocument stored; gssog::SogDocument deflated; std::string error; - CHECK(gssog::SogReader().Read( - Fixture("kit-multi-degree3.sog"), &stored, &error)); - CHECK(gssog::SogReader().Read( - Fixture("kit-multi-degree3-deflated.sog"), &deflated, &error)); + CHECK(gssog::SogReader().Read(Fixture("kit-multi-degree3.sog"), &stored, + &error)); + CHECK(gssog::SogReader().Read(Fixture("kit-multi-degree3-deflated.sog"), + &deflated, &error)); CHECK(stored.metadata.gaussianCount == deflated.metadata.gaussianCount); CHECK(stored.meansLow.rgba == deflated.meansLow.rgba); CHECK(stored.meansHigh.rgba == deflated.meansHigh.rgba); @@ -176,8 +178,8 @@ void TestUnbundledMatchesBundled() gssog::SogDocument bundled; gssog::SogDocument unbundled; std::string error; - CHECK(gssog::SogReader().Read( - Fixture("kit-multi-degree3.sog"), &bundled, &error)); + CHECK(gssog::SogReader().Read(Fixture("kit-multi-degree3.sog"), &bundled, + &error)); CHECK(gssog::SogReader().Read( Fixture("unbundled-kit-multi-degree3/meta.json"), &unbundled, &error)); CHECK(error.empty()); @@ -204,11 +206,9 @@ void TestInjectedCompanionLoader() int served = 0; const gssog::SogReader reader( - [&directory, &served]( - const std::string& anchorPath, - const std::string& planeName, - std::vector* bytes, - std::string* error) { + [&directory, + &served](const std::string& anchorPath, const std::string& planeName, + std::vector* bytes, std::string* error) { (void)anchorPath; (void)error; ++served; @@ -218,16 +218,15 @@ void TestInjectedCompanionLoader() if (!in) { return false; } - *bytes = std::vector( - std::istreambuf_iterator(in), - std::istreambuf_iterator()); + *bytes = + std::vector(std::istreambuf_iterator(in), + std::istreambuf_iterator()); return true; }); gssog::SogDocument document; std::string error; - CHECK(reader.Read( - (directory / "meta.json").string(), &document, &error)); + CHECK(reader.Read((directory / "meta.json").string(), &document, &error)); CHECK(error.empty()); CHECK(served == 7); // five per-Gaussian planes plus centroids and labels CHECK(document.metadata.gaussianCount == 3); @@ -252,16 +251,16 @@ void TestMetadataOnly() // Bundled metadata comes out of the archive without decoding a plane. gssog::SogMetadata bundled; - CHECK(gssog::SogReader().ReadMetadata( - Fixture("kit-one-degree0.sog"), &bundled, &error)); + CHECK(gssog::SogReader().ReadMetadata(Fixture("kit-one-degree0.sog"), + &bundled, &error)); CHECK(bundled.gaussianCount == 1); CHECK(bundled.shBands == 0); // A zero-Gaussian file is rejected here exactly as in a full read. gssog::SogMetadata rejected; error.clear(); - CHECK(!gssog::SogReader().ReadMetadata( - Fixture("empty-count.sog"), &rejected, &error)); + CHECK(!gssog::SogReader().ReadMetadata(Fixture("empty-count.sog"), + &rejected, &error)); CHECK(HasCode(error, gssog::diag::kEmptyPointSet)); } @@ -304,16 +303,16 @@ void TestMalformedContainers() // Missing files and null outputs. gssog::SogDocument document; std::string error; - CHECK(!gssog::SogReader().Read( - Fixture("no-such-file.sog"), &document, &error)); + CHECK(!gssog::SogReader().Read(Fixture("no-such-file.sog"), &document, + &error)); CHECK(HasCode(error, gssog::diag::kUnreadableFile)); error.clear(); - CHECK(!gssog::SogReader().Read( - Fixture("kit-one-degree0.sog"), nullptr, &error)); + CHECK(!gssog::SogReader().Read(Fixture("kit-one-degree0.sog"), nullptr, + &error)); CHECK(HasCode(error, gssog::diag::kInternalError)); error.clear(); - CHECK(!gssog::SogReader().ReadMetadata( - Fixture("kit-one-degree0.sog"), nullptr, &error)); + CHECK(!gssog::SogReader().ReadMetadata(Fixture("kit-one-degree0.sog"), + nullptr, &error)); CHECK(HasCode(error, gssog::diag::kInternalError)); } diff --git a/plugins/gaussian-spz/src/GaussianSpzFileFormat.cpp b/plugins/gaussian-spz/src/GaussianSpzFileFormat.cpp index c3d2bb6..6bd9c20 100644 --- a/plugins/gaussian-spz/src/GaussianSpzFileFormat.cpp +++ b/plugins/gaussian-spz/src/GaussianSpzFileFormat.cpp @@ -22,7 +22,8 @@ PXR_NAMESPACE_OPEN_SCOPE -TF_DEFINE_PUBLIC_TOKENS(GaussianSpzFileFormatTokens, GAUSSIANSPZ_FILE_FORMAT_TOKENS); +TF_DEFINE_PUBLIC_TOKENS(GaussianSpzFileFormatTokens, + GAUSSIANSPZ_FILE_FORMAT_TOKENS); namespace { @@ -48,24 +49,22 @@ TF_DEBUG_CODES(GSPZ_IMPORT_STATS); TF_REGISTRY_FUNCTION(TfDebug) { - TF_DEBUG_ENVIRONMENT_SYMBOL(GSPZ_IMPORT_STATS, + TF_DEBUG_ENVIRONMENT_SYMBOL( + GSPZ_IMPORT_STATS, "gaussian-spz: one line of per-import statistics through the shared " "GaussianImportStats seam"); } GaussianSpzFileFormat::GaussianSpzFileFormat() - : SdfFileFormat( - GaussianSpzFileFormatTokens->Id, - GaussianSpzFileFormatTokens->Version, - GaussianSpzFileFormatTokens->Target, - GaussianSpzFileFormatTokens->Extension) -{ -} + : SdfFileFormat(GaussianSpzFileFormatTokens->Id, + GaussianSpzFileFormatTokens->Version, + GaussianSpzFileFormatTokens->Target, + GaussianSpzFileFormatTokens->Extension) +{} GaussianSpzFileFormat::~GaussianSpzFileFormat() = default; -bool -GaussianSpzFileFormat::CanRead(const std::string& file) const +bool GaussianSpzFileFormat::CanRead(const std::string& file) const { if (SdfFileFormat::GetFileExtension(file) != "spz") { return false; @@ -73,11 +72,9 @@ GaussianSpzFileFormat::CanRead(const std::string& file) const return openstrata::gs::spz::GaussianSpzDecoder().CanRead(file); } -bool -GaussianSpzFileFormat::Read( - SdfLayer* layer, - const std::string& resolvedPath, - bool metadataOnly) const +bool GaussianSpzFileFormat::Read(SdfLayer* layer, + const std::string& resolvedPath, + bool metadataOnly) const { namespace gsspz = openstrata::gs::spz; @@ -89,16 +86,19 @@ GaussianSpzFileFormat::Read( // struct is six same-typed pointers, so it is assigned by name: a // positional list would let a swapped pair compile silently and emit the // wrong code to users (see GaussianLayerWriter.h). - static const openstrata::gs::usd::LayerWriterDiagnosticCodes kWriterCodes = [] { - openstrata::gs::usd::LayerWriterDiagnosticCodes codes; - codes.internalError = gsspz::diag::kInternalError; - codes.cloudValidationFailed = gsspz::diag::kCloudValidationFailed; - codes.stageCreationFailed = gsspz::diag::kStageCreationFailed; - codes.scaffoldAuthoringFailed = gsspz::diag::kScaffoldAuthoringFailed; - codes.attributeAuthoringFailed = gsspz::diag::kAttributeAuthoringFailed; - codes.extentOverflow = gsspz::diag::kExtentOverflow; - return codes; - }(); + static const openstrata::gs::usd::LayerWriterDiagnosticCodes kWriterCodes = + [] { + openstrata::gs::usd::LayerWriterDiagnosticCodes codes; + codes.internalError = gsspz::diag::kInternalError; + codes.cloudValidationFailed = gsspz::diag::kCloudValidationFailed; + codes.stageCreationFailed = gsspz::diag::kStageCreationFailed; + codes.scaffoldAuthoringFailed = + gsspz::diag::kScaffoldAuthoringFailed; + codes.attributeAuthoringFailed = + gsspz::diag::kAttributeAuthoringFailed; + codes.extentOverflow = gsspz::diag::kExtentOverflow; + return codes; + }(); const openstrata::gs::usd::GaussianLayerWriter writer(kWriterCodes); // Sdf reload executes under an outer SdfChangeBlock. Authoring a detached @@ -112,20 +112,18 @@ GaussianSpzFileFormat::Read( // from the container header; no attribute streams are decompressed. gsspz::GaussianSpzMetadata metadata; if (!decoder.DecodeMetadata(resolvedPath, &metadata, &error)) { - TF_RUNTIME_ERROR( - "gaussian-spz: failed to read '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-spz: failed to read '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } auto task = std::async(std::launch::async, [&]() { - return writer.WriteMetadataToLayer( - metadata.gaussianCount, metadata.shDegree, - kSourceFormat, &generated, &error); + return writer.WriteMetadataToLayer(metadata.gaussianCount, + metadata.shDegree, kSourceFormat, + &generated, &error); }); if (!task.get()) { - TF_RUNTIME_ERROR( - "gaussian-spz: failed to author USD for '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-spz: failed to author USD for '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } layer->TransferContent(generated); @@ -141,9 +139,8 @@ GaussianSpzFileFormat::Read( openstrata::gs::GaussianCloudData cloud; std::vector warnings; if (!decoder.Decode(resolvedPath, &cloud, &warnings, &error, statsOut)) { - TF_RUNTIME_ERROR( - "gaussian-spz: failed to read '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-spz: failed to read '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } for (const std::string& warning : warnings) { @@ -161,50 +158,49 @@ GaussianSpzFileFormat::Read( // authored. const auto authorStart = std::chrono::steady_clock::now(); auto task = std::async(std::launch::async, [&]() { - return writer.WriteToLayer( - std::move(cloud), kSourceFormat, &generated, &error); + return writer.WriteToLayer(std::move(cloud), kSourceFormat, &generated, + &error); }); if (!task.get()) { - TF_RUNTIME_ERROR( - "gaussian-spz: failed to author USD for '%s': %s", - resolvedPath.c_str(), error.c_str()); + TF_RUNTIME_ERROR("gaussian-spz: failed to author USD for '%s': %s", + resolvedPath.c_str(), error.c_str()); return false; } if (statsOut) { - stats.authorSeconds = std::chrono::duration( - std::chrono::steady_clock::now() - authorStart).count(); - TF_DEBUG(GSPZ_IMPORT_STATS).Msg("gaussian-spz: %s\n", - openstrata::gs::FormatImportStats(stats).c_str()); + stats.authorSeconds = + std::chrono::duration(std::chrono::steady_clock::now() - + authorStart) + .count(); + TF_DEBUG(GSPZ_IMPORT_STATS) + .Msg("gaussian-spz: %s\n", + openstrata::gs::FormatImportStats(stats).c_str()); } layer->TransferContent(generated); return true; } -bool -GaussianSpzFileFormat::WriteToFile( - const SdfLayer& layer, - const std::string& filePath, - const std::string& comment, - const FileFormatArguments& args) const +bool GaussianSpzFileFormat::WriteToFile(const SdfLayer& layer, + const std::string& filePath, + const std::string& comment, + const FileFormatArguments& args) const { (void)layer; (void)filePath; (void)comment; (void)args; TF_RUNTIME_ERROR("%s", - openstrata::gs::spz::diag::Format( - openstrata::gs::spz::diag::kWriteUnsupported, - "gaussian-spz is read-only; USD to SPZ writing is " - "unsupported").c_str()); + openstrata::gs::spz::diag::Format( + openstrata::gs::spz::diag::kWriteUnsupported, + "gaussian-spz is read-only; USD to SPZ writing is " + "unsupported") + .c_str()); return false; } -bool -GaussianSpzFileFormat::WriteToString( - const SdfLayer& layer, - std::string* str, - const std::string& comment) const +bool GaussianSpzFileFormat::WriteToString(const SdfLayer& layer, + std::string* str, + const std::string& comment) const { SdfFileFormatConstPtr usda = SdfFileFormat::FindByExtension("usda"); if (usda) { diff --git a/plugins/gaussian-spz/src/GaussianSpzFileFormat.h b/plugins/gaussian-spz/src/GaussianSpzFileFormat.h index d0758f3..6d571bc 100644 --- a/plugins/gaussian-spz/src/GaussianSpzFileFormat.h +++ b/plugins/gaussian-spz/src/GaussianSpzFileFormat.h @@ -8,13 +8,11 @@ PXR_NAMESPACE_OPEN_SCOPE // The tokens that identify this file format to USD's Sdf layer registry. -#define GAUSSIANSPZ_FILE_FORMAT_TOKENS \ - ((Id, "spz")) \ - ((Version, "1.0")) \ - ((Target, "usd")) \ - ((Extension, "spz")) +#define GAUSSIANSPZ_FILE_FORMAT_TOKENS \ + ((Id, "spz"))((Version, "1.0"))((Target, "usd"))((Extension, "spz")) -TF_DECLARE_PUBLIC_TOKENS(GaussianSpzFileFormatTokens, GAUSSIANSPZ_FILE_FORMAT_TOKENS); +TF_DECLARE_PUBLIC_TOKENS(GaussianSpzFileFormatTokens, + GAUSSIANSPZ_FILE_FORMAT_TOKENS); /// Read-only SdfFileFormat for Niantic SPZ Gaussian Splatting assets. /// Container reading lives in io/SpzReader.*; semantic decoding into @@ -22,20 +20,19 @@ TF_DECLARE_PUBLIC_TOKENS(GaussianSpzFileFormatTokens, GAUSSIANSPZ_FILE_FORMAT_TO /// through the shared libs/gaussian-usd GaussianLayerWriter, so PLY and SPZ /// author the identical stage hierarchy, schema, and metadata by construction. class GaussianSpzFileFormat : public SdfFileFormat { -public: + public: bool CanRead(const std::string& file) const override; - bool Read(SdfLayer* layer, const std::string& resolvedPath, bool metadataOnly) const override; + bool Read(SdfLayer* layer, const std::string& resolvedPath, + bool metadataOnly) const override; bool WriteToFile( - const SdfLayer& layer, - const std::string& filePath, + const SdfLayer& layer, const std::string& filePath, const std::string& comment = std::string(), const FileFormatArguments& args = FileFormatArguments()) const override; - bool WriteToString( - const SdfLayer& layer, - std::string* str, - const std::string& comment = std::string()) const override; + bool + WriteToString(const SdfLayer& layer, std::string* str, + const std::string& comment = std::string()) const override; -protected: + protected: SDF_FILE_FORMAT_FACTORY_ACCESS; GaussianSpzFileFormat(); diff --git a/plugins/gaussian-spz/src/io/GaussianSpzDecoder.cpp b/plugins/gaussian-spz/src/io/GaussianSpzDecoder.cpp index 0f1910b..0f76caa 100644 --- a/plugins/gaussian-spz/src/io/GaussianSpzDecoder.cpp +++ b/plugins/gaussian-spz/src/io/GaussianSpzDecoder.cpp @@ -49,9 +49,10 @@ bool CheckSupportedShDegree(std::uint8_t shDegree, std::string* error) return true; } SetError(error, diag::kUnsupportedShDegree, - "SPZ SH degree " + std::to_string(static_cast(shDegree)) + - " is valid for the format but not supported by this release; " - "supported degrees are 0-" + std::to_string(kMaxShDegree) + "."); + "SPZ SH degree " + std::to_string(static_cast(shDegree)) + + " is valid for the format but not supported by this release; " + "supported degrees are 0-" + + std::to_string(kMaxShDegree) + "."); return false; } @@ -94,10 +95,9 @@ float HalfToFloat(std::uint16_t half) noexcept // behavior. float Fixed24ToFloat(const unsigned char* bytes, int fractionalBits) noexcept { - std::int32_t fixed = - static_cast(bytes[0]) | - (static_cast(bytes[1]) << 8) | - (static_cast(bytes[2]) << 16); + std::int32_t fixed = static_cast(bytes[0]) | + (static_cast(bytes[1]) << 8) | + (static_cast(bytes[2]) << 16); if ((fixed & 0x800000) != 0) { fixed |= ~0xffffff; } @@ -128,11 +128,9 @@ std::uint64_t FileSizeOf(const std::string& path) void UnpackRotationFirstThree(const unsigned char* r, float (&q)[4]) noexcept { for (int component = 0; component < 3; ++component) { - q[component] = - static_cast(r[component]) / 127.5f - 1.0f; + q[component] = static_cast(r[component]) / 127.5f - 1.0f; } - const float squaredNorm = - q[0] * q[0] + q[1] * q[1] + q[2] * q[2]; + const float squaredNorm = q[0] * q[0] + q[1] * q[1] + q[2] * q[2]; q[3] = std::sqrt(std::max(0.0f, 1.0f - squaredNorm)); } @@ -144,11 +142,10 @@ void UnpackRotationFirstThree(const unsigned char* r, float (&q)[4]) noexcept // for hostile magnitudes. void UnpackRotationSmallestThree(const unsigned char* r, float (&q)[4]) noexcept { - std::uint32_t packed = - static_cast(r[0]) | - (static_cast(r[1]) << 8) | - (static_cast(r[2]) << 16) | - (static_cast(r[3]) << 24); + std::uint32_t packed = static_cast(r[0]) | + (static_cast(r[1]) << 8) | + (static_cast(r[2]) << 16) | + (static_cast(r[3]) << 24); constexpr std::uint32_t kMagnitudeMask = (1u << 9) - 1u; const int largest = static_cast(packed >> 30); @@ -161,7 +158,7 @@ void UnpackRotationSmallestThree(const unsigned char* r, float (&q)[4]) noexcept const bool negative = ((packed >> 9) & 1u) != 0; packed >>= 10; float value = kSqrt1_2 * static_cast(magnitude) / - static_cast(kMagnitudeMask); + static_cast(kMagnitudeMask); if (negative) { value = -value; } @@ -178,14 +175,13 @@ bool GaussianSpzDecoder::CanRead(const std::string& path) const noexcept return SpzReader().CanRead(path); } -bool GaussianSpzDecoder::DecodeMetadata( - const std::string& path, - GaussianSpzMetadata* metadata, - std::string* error) const +bool GaussianSpzDecoder::DecodeMetadata(const std::string& path, + GaussianSpzMetadata* metadata, + std::string* error) const { if (!metadata) { SetError(error, diag::kInternalError, - "Gaussian decoder received a null metadata output."); + "Gaussian decoder received a null metadata output."); return false; } @@ -202,16 +198,15 @@ bool GaussianSpzDecoder::DecodeMetadata( return true; } -bool GaussianSpzDecoder::Decode( - const std::string& path, - GaussianCloudData* cloud, - std::vector* warnings, - std::string* error, - GaussianImportStats* stats) const +bool GaussianSpzDecoder::Decode(const std::string& path, + GaussianCloudData* cloud, + std::vector* warnings, + std::string* error, + GaussianImportStats* stats) const { if (!cloud) { SetError(error, diag::kInternalError, - "Gaussian decoder received a null cloud output."); + "Gaussian decoder received a null cloud output."); return false; } @@ -237,14 +232,13 @@ bool GaussianSpzDecoder::Decode( // The reader guarantees the span layout; a disagreement here is pipeline // misuse, not file content. if (document.positions.size != count * header.BytesPerPosition() || - document.alphas.size != count || - document.colors.size != count * 3 || + document.alphas.size != count || document.colors.size != count * 3 || document.scales.size != count * 3 || document.rotations.size != count * header.BytesPerRotation() || document.sh.size != count * 3 * restDims || document.sh.offset + document.sh.size != document.payload.size()) { SetError(error, diag::kInternalError, - "The container spans do not match the header layout."); + "The container spans do not match the header layout."); return false; } @@ -258,9 +252,10 @@ bool GaussianSpzDecoder::Decode( const auto allocate = [&](auto* array, std::size_t elements) { if (!TryResize(array, elements)) { SetError(error, diag::kModelAllocationFailed, - "SPZ model arrays for " + std::to_string(count) + - " Gaussians at SH degree " + std::to_string(result.shDegree) + - " could not be allocated."); + "SPZ model arrays for " + std::to_string(count) + + " Gaussians at SH degree " + + std::to_string(result.shDegree) + + " could not be allocated."); return false; } return true; @@ -276,28 +271,25 @@ bool GaussianSpzDecoder::Decode( float decoded[3]; for (int axis = 0; axis < 3; ++axis) { const unsigned char* h = stored + i * 6 + axis * 2; - decoded[axis] = HalfToFloat( - static_cast( - h[0] | (static_cast(h[1]) << 8))); + decoded[axis] = HalfToFloat(static_cast( + h[0] | (static_cast(h[1]) << 8))); } if (!std::isfinite(decoded[0]) || !std::isfinite(decoded[1]) || !std::isfinite(decoded[2])) { SetError(error, diag::kNonFinitePosition, - "The float16 position of Gaussian " + - std::to_string(i) + " is not finite."); + "The float16 position of Gaussian " + + std::to_string(i) + " is not finite."); return false; } result.positions[i] = {decoded[0], decoded[1], decoded[2]}; } } else { - const int fractionalBits = - static_cast(header.fractionalBits); + const int fractionalBits = static_cast(header.fractionalBits); for (std::size_t i = 0; i < count; ++i) { const unsigned char* p = stored + i * 9; - result.positions[i] = { - Fixed24ToFloat(p, fractionalBits), - Fixed24ToFloat(p + 3, fractionalBits), - Fixed24ToFloat(p + 6, fractionalBits)}; + result.positions[i] = {Fixed24ToFloat(p, fractionalBits), + Fixed24ToFloat(p + 3, fractionalBits), + Fixed24ToFloat(p + 6, fractionalBits)}; } } } @@ -323,9 +315,9 @@ bool GaussianSpzDecoder::Decode( } for (std::size_t i = 0; i < count; ++i) { const auto decode = [&](std::size_t component) { - return std::exp( - static_cast(stored[i * 3 + component]) / 16.0f - - 10.0f); + return std::exp(static_cast(stored[i * 3 + component]) / + 16.0f - + 10.0f); }; result.scales[i] = {decode(0), decode(1), decode(2)}; } @@ -339,7 +331,8 @@ bool GaussianSpzDecoder::Decode( for (std::size_t i = 0; i < count; ++i) { const auto decode = [&](std::size_t channel) { return (static_cast(stored[i * 3 + channel]) / 255.0f - - 0.5f) / kColorScale; + 0.5f) / + kColorScale; }; result.dcCoefficients[i] = {decode(0), decode(1), decode(2)}; } @@ -362,10 +355,10 @@ bool GaussianSpzDecoder::Decode( // Reorder to the model's scalar-first convention and absorb the // quantization drift; the decoded norm is never near zero // (SPZ_MAPPING.md §4), so identity replacement is unreachable. - if (!NormalizeQuaternion( - {q[3], q[0], q[1], q[2]}, &result.rotations[i])) { + if (!NormalizeQuaternion({q[3], q[0], q[1], q[2]}, + &result.rotations[i])) { SetError(error, diag::kInternalError, - "A dequantized quaternion was not normalizable."); + "A dequantized quaternion was not normalizable."); return false; } } @@ -377,12 +370,12 @@ bool GaussianSpzDecoder::Decode( // degrees, so the shared helper computes exactly the length the fill // loop below indexes. std::size_t restLength = 0; - if (!ComputeRestCoefficientCount( - count, result.shDegree, &restLength)) { + if (!ComputeRestCoefficientCount(count, result.shDegree, &restLength)) { SetError(error, diag::kModelAllocationFailed, - "The model size for " + std::to_string(count) + - " Gaussians at SH degree " + std::to_string(result.shDegree) + - " overflows this platform's address space."); + "The model size for " + std::to_string(count) + + " Gaussians at SH degree " + + std::to_string(result.shDegree) + + " overflows this platform's address space."); return false; } if (!allocate(&result.restCoefficients, restLength)) { @@ -397,8 +390,7 @@ bool GaussianSpzDecoder::Decode( const unsigned char* rgb = stored + (i * restDims + coefficient) * 3; result.restCoefficients[i * restDims + coefficient] = { - UnquantizeSh(rgb[0]), - UnquantizeSh(rgb[1]), + UnquantizeSh(rgb[0]), UnquantizeSh(rgb[1]), UnquantizeSh(rgb[2])}; } } @@ -412,13 +404,15 @@ bool GaussianSpzDecoder::Decode( if (warnings) { if (!document.extensions.empty()) { - warnings->push_back(diag::Format(diag::kExtensionsIgnored, + warnings->push_back(diag::Format( + diag::kExtensionsIgnored, std::to_string(document.extensions.size()) + - " extension-record byte(s) were ignored; extension records " - "are not part of the shared Gaussian model.")); + " extension-record byte(s) were ignored; extension records " + "are not part of the shared Gaussian model.")); } if (header.IsAntialiased()) { - warnings->push_back(diag::Format(diag::kAntialiasedFlagIgnored, + warnings->push_back(diag::Format( + diag::kAntialiasedFlagIgnored, "The antialiased flag was ignored; the authored schema does " "not carry an antialiasing convention.")); } diff --git a/plugins/gaussian-spz/src/io/GaussianSpzDecoder.h b/plugins/gaussian-spz/src/io/GaussianSpzDecoder.h index ae059d3..76a5105 100644 --- a/plugins/gaussian-spz/src/io/GaussianSpzDecoder.h +++ b/plugins/gaussian-spz/src/io/GaussianSpzDecoder.h @@ -30,28 +30,24 @@ struct GaussianSpzMetadata { // math) stay in SpzReader; this class consumes its packed document. The // exact mapping is docs/reference/SPZ_MAPPING.md. class GaussianSpzDecoder { -public: + public: bool CanRead(const std::string& path) const noexcept; // Validates the header and derives count and SH degree without touching // the attribute streams. SH degree 4 fails here the same way it fails in // Decode(): a metadata read must not promise a decode that would be // rejected. - bool DecodeMetadata( - const std::string& path, - GaussianSpzMetadata* metadata, - std::string* error = nullptr) const; + bool DecodeMetadata(const std::string& path, GaussianSpzMetadata* metadata, + std::string* error = nullptr) const; // On success, `stats` (optional) carries the decoder's half of the shared // import-statistics record: source format/version, count and degree, // byte sizes, and the read/decode timings. Bounds and the authoring time // stay with the caller. - bool Decode( - const std::string& path, - GaussianCloudData* cloud, - std::vector* warnings = nullptr, - std::string* error = nullptr, - GaussianImportStats* stats = nullptr) const; + bool Decode(const std::string& path, GaussianCloudData* cloud, + std::vector* warnings = nullptr, + std::string* error = nullptr, + GaussianImportStats* stats = nullptr) const; }; } // namespace openstrata::gs::spz diff --git a/plugins/gaussian-spz/src/io/SpzReader.h b/plugins/gaussian-spz/src/io/SpzReader.h index 4e6a8b7..a03c30d 100644 --- a/plugins/gaussian-spz/src/io/SpzReader.h +++ b/plugins/gaussian-spz/src/io/SpzReader.h @@ -19,8 +19,14 @@ struct SpzHeader { std::uint8_t flags = 0; std::uint8_t reserved = 0; - bool IsAntialiased() const noexcept { return (flags & 0x01) != 0; } - bool HasExtensions() const noexcept { return (flags & 0x02) != 0; } + bool IsAntialiased() const noexcept + { + return (flags & 0x01) != 0; + } + bool HasExtensions() const noexcept + { + return (flags & 0x02) != 0; + } // Per-point byte widths are fixed by the container version: v1 stores // float16 positions, v2+ 24-bit fixed point; v1-v2 store three 8-bit @@ -80,7 +86,7 @@ struct SpzPackedDocument { // Errors carry stable GSPZ-**** container diagnostics. The reader performs no // semantic dequantization and constructs no USD objects. class SpzReader { -public: + public: // Signature-only routing decision (design policy §7.6): true for a // plaintext NGSP container at offset 0 (the v4 layout) or a gzip member // whose first 16 decompressed bytes carry the SPZ magic. The version is @@ -92,15 +98,11 @@ class SpzReader { // it, plus a compressed-size plausibility bound on the declared point // count. It never touches the attribute streams, so a valid header with a // truncated or corrupt body succeeds here and fails in Read(). - bool ReadHeader( - const std::string& path, - SpzHeader* header, - std::string* error = nullptr) const; + bool ReadHeader(const std::string& path, SpzHeader* header, + std::string* error = nullptr) const; - bool Read( - const std::string& path, - SpzPackedDocument* document, - std::string* error = nullptr) const; + bool Read(const std::string& path, SpzPackedDocument* document, + std::string* error = nullptr) const; }; } // namespace openstrata::gs::spz diff --git a/plugins/gaussian-spz/tests/test_gaussian_spz_decoder.cpp b/plugins/gaussian-spz/tests/test_gaussian_spz_decoder.cpp index 51b4319..1efa49f 100644 --- a/plugins/gaussian-spz/tests/test_gaussian_spz_decoder.cpp +++ b/plugins/gaussian-spz/tests/test_gaussian_spz_decoder.cpp @@ -20,11 +20,13 @@ namespace { int failures = 0; -#define CHECK(expr) \ - do { if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ - ++failures; \ - } } while (false) +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ + ++failures; \ + } \ + } while (false) std::string Fixture(const char* name) { @@ -47,7 +49,8 @@ void CheckClose(float actual, float expected, float tolerance, const char* what) void CheckContract(const gs::GaussianCloudData& cloud) { - for (const std::string& violation : gs::testing::CheckCloudContract(cloud)) { + for (const std::string& violation : + gs::testing::CheckCloudContract(cloud)) { std::cerr << "contract: " << violation << "\n"; ++failures; } @@ -63,8 +66,8 @@ void TestDegree1FullPipeline() gs::GaussianCloudData cloud; std::vector warnings; std::string error; - CHECK(decoder.Decode( - Fixture("decode-degree1-v2.spz"), &cloud, &warnings, &error)); + CHECK(decoder.Decode(Fixture("decode-degree1-v2.spz"), &cloud, &warnings, + &error)); CHECK(error.empty()); CHECK(warnings.empty()); CheckContract(cloud); @@ -126,11 +129,11 @@ void TestDegree1FullPipeline() for (std::size_t i = 0; i < expectedRest.size(); ++i) { const std::string at = "rest[" + std::to_string(i) + "]"; CheckClose(cloud.restCoefficients[i].x, expectedRest[i].x, 0.01f, - (at + ".r").c_str()); + (at + ".r").c_str()); CheckClose(cloud.restCoefficients[i].y, expectedRest[i].y, 0.01f, - (at + ".g").c_str()); + (at + ".g").c_str()); CheckClose(cloud.restCoefficients[i].z, expectedRest[i].z, 0.01f, - (at + ".b").c_str()); + (at + ".b").c_str()); } } @@ -153,8 +156,8 @@ void TestDegree3ShStride() const gsspz::GaussianSpzDecoder decoder; gs::GaussianCloudData cloud; std::string error; - CHECK(decoder.Decode( - Fixture("decode-degree3-v2.spz"), &cloud, nullptr, &error)); + CHECK(decoder.Decode(Fixture("decode-degree3-v2.spz"), &cloud, nullptr, + &error)); CHECK(error.empty()); CheckContract(cloud); CHECK(cloud.gaussianCount == 2); @@ -170,9 +173,8 @@ void TestDegree3ShStride() for (std::size_t k = 0; k < 15; ++k) { const gs::Float3 source = SourceShTriple(point, k); const gs::Float3& decoded = cloud.restCoefficients[point * 15 + k]; - const std::string at = - "degree3 rest[" + std::to_string(point) + "][" + - std::to_string(k) + "]"; + const std::string at = "degree3 rest[" + std::to_string(point) + + "][" + std::to_string(k) + "]"; CheckClose(decoded.x, source.x, 1e-4f, (at + ".r").c_str()); CheckClose(decoded.y, source.y, 1e-4f, (at + ".g").c_str()); CheckClose(decoded.z, source.z, 1e-4f, (at + ".b").c_str()); @@ -221,7 +223,8 @@ void TestVersion3SmallestThreeRotation() // Source quaternion (w,x,y,z) = normalize(0.2, 0.8, 0.4, 0.4). The // 10-bit encoding is coarser than v2, so the tolerance is wider. - const float norm = std::sqrt(0.2f * 0.2f + 0.8f * 0.8f + 0.4f * 0.4f + 0.4f * 0.4f); + const float norm = + std::sqrt(0.2f * 0.2f + 0.8f * 0.8f + 0.4f * 0.4f + 0.4f * 0.4f); CheckClose(cloud.rotations[0].real, 0.2f / norm, 2e-2f, "v3 rot.w"); CheckClose(cloud.rotations[0].i, 0.8f / norm, 2e-2f, "v3 rot.x"); CheckClose(cloud.rotations[0].j, 0.4f / norm, 2e-2f, "v3 rot.y"); @@ -233,16 +236,16 @@ void TestMetadataOnly() const gsspz::GaussianSpzDecoder decoder; gsspz::GaussianSpzMetadata metadata; std::string error; - CHECK(decoder.DecodeMetadata( - Fixture("decode-degree1-v2.spz"), &metadata, &error)); + CHECK(decoder.DecodeMetadata(Fixture("decode-degree1-v2.spz"), &metadata, + &error)); CHECK(error.empty()); CHECK(metadata.gaussianCount == 2); CHECK(metadata.shDegree == 1); // Metadata must not promise a decode it would then reject: degree 4 fails // here with the same unsupported-degree code as Decode(). - CHECK(!decoder.DecodeMetadata( - Fixture("decode-degree4-v2.spz"), &metadata, &error)); + CHECK(!decoder.DecodeMetadata(Fixture("decode-degree4-v2.spz"), &metadata, + &error)); CHECK(HasCode(error, gsspz::diag::kUnsupportedShDegree)); } @@ -253,8 +256,8 @@ void TestDecodeFailure(const char* fixture, const char* code) std::string error; CHECK(!decoder.Decode(Fixture(fixture), &cloud, nullptr, &error)); if (!HasCode(error, code)) { - std::cerr << fixture << ": expected " << code << ", got: " - << error << "\n"; + std::cerr << fixture << ": expected " << code << ", got: " << error + << "\n"; ++failures; } } @@ -267,14 +270,16 @@ void TestContainerFailuresPropagate() TestDecodeFailure("plaintext-v4.spz", gsspz::diag::kUnsupportedVersion); TestDecodeFailure("empty-points-v2.spz", gsspz::diag::kEmptyPointSet); TestDecodeFailure("not-spz.spz", gsspz::diag::kNotSpzContainer); - TestDecodeFailure( - "truncated-payload-v2.spz", gsspz::diag::kTruncatedContainer); + TestDecodeFailure("truncated-payload-v2.spz", + gsspz::diag::kTruncatedContainer); } void TestSemanticFailures() { - TestDecodeFailure("decode-degree4-v2.spz", gsspz::diag::kUnsupportedShDegree); - TestDecodeFailure("decode-nonfinite-v1.spz", gsspz::diag::kNonFinitePosition); + TestDecodeFailure("decode-degree4-v2.spz", + gsspz::diag::kUnsupportedShDegree); + TestDecodeFailure("decode-nonfinite-v1.spz", + gsspz::diag::kNonFinitePosition); } void TestWarningsForIgnoredData() @@ -285,16 +290,16 @@ void TestWarningsForIgnoredData() std::string error; // extensions-v2.spz is antialiased (0x1) + extensions (0x2); both are // ignored with a warning, and neither prevents a successful decode. - CHECK(decoder.Decode( - Fixture("extensions-v2.spz"), &cloud, &warnings, &error)); + CHECK(decoder.Decode(Fixture("extensions-v2.spz"), &cloud, &warnings, + &error)); CHECK(error.empty()); bool sawExtensions = false; bool sawAntialiased = false; for (const std::string& warning : warnings) { - sawExtensions = sawExtensions || - HasCode(warning, gsspz::diag::kExtensionsIgnored); + sawExtensions = + sawExtensions || HasCode(warning, gsspz::diag::kExtensionsIgnored); sawAntialiased = sawAntialiased || - HasCode(warning, gsspz::diag::kAntialiasedFlagIgnored); + HasCode(warning, gsspz::diag::kAntialiasedFlagIgnored); } CHECK(sawExtensions); CHECK(sawAntialiased); @@ -331,7 +336,7 @@ void TestImportStats() CHECK(stats.opacityThresholdRejectedCount == 0); CHECK(stats.warningCount == warnings.size()); CHECK(stats.sourceBytes == - static_cast(std::filesystem::file_size(path))); + static_cast(std::filesystem::file_size(path))); CHECK(stats.decodedBytes == gs::ComputeDecodedByteSize(cloud)); CHECK(stats.readSeconds >= 0.0 && stats.decodeSeconds >= 0.0); // The decoder does not time authoring and does not compute bounds; both @@ -346,8 +351,8 @@ void TestImportStats() CHECK(stats.warningCount > 0); gs::GaussianImportStats v3Stats; - CHECK(decoder.Decode( - Fixture("decode-v3.spz"), &cloud, &warnings, &error, &v3Stats)); + CHECK(decoder.Decode(Fixture("decode-v3.spz"), &cloud, &warnings, &error, + &v3Stats)); CHECK(v3Stats.sourceVersion == "3"); } diff --git a/plugins/gaussian-spz/tests/test_gaussian_spz_reader.cpp b/plugins/gaussian-spz/tests/test_gaussian_spz_reader.cpp index 9c9525d..4deab87 100644 --- a/plugins/gaussian-spz/tests/test_gaussian_spz_reader.cpp +++ b/plugins/gaussian-spz/tests/test_gaussian_spz_reader.cpp @@ -14,11 +14,13 @@ namespace { int failures = 0; -#define CHECK(expr) \ - do { if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ - ++failures; \ - } } while (false) +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ + ++failures; \ + } \ + } while (false) std::string Fixture(const char* name) { @@ -43,10 +45,8 @@ bool PayloadMatchesPattern(const gsspz::SpzPackedDocument& document) return true; } -bool SpanEquals( - const gsspz::SpzPackedDocument::Span& span, - std::size_t offset, - std::size_t size) +bool SpanEquals(const gsspz::SpzPackedDocument::Span& span, std::size_t offset, + std::size_t size) { return span.offset == offset && span.size == size; } @@ -54,11 +54,8 @@ bool SpanEquals( // A valid single-point degree-0 container in each supported version. The // versions differ only in per-point widths: v1 stores float16 positions, // v3 four-byte smallest-three rotations. -void TestMinimalContainer( - const char* fixture, - std::uint32_t version, - std::size_t positionBytes, - std::size_t rotationBytes) +void TestMinimalContainer(const char* fixture, std::uint32_t version, + std::size_t positionBytes, std::size_t rotationBytes) { const gsspz::SpzReader reader; const std::string path = Fixture(fixture); @@ -103,8 +100,8 @@ void TestMultiPointShLayout() const gsspz::SpzReader reader; gsspz::SpzPackedDocument document; std::string error; - CHECK(reader.Read( - Fixture("three-points-degree1-v2.spz"), &document, &error)); + CHECK( + reader.Read(Fixture("three-points-degree1-v2.spz"), &document, &error)); CHECK(document.header.pointCount == 3); CHECK(document.header.shDegree == 1); CHECK(document.payload.size() == 84); @@ -121,7 +118,8 @@ void TestMultiPointShLayout() // channel. Degree 4 is the specification maximum and must pass the container // stage; whether the semantic decoder accepts it is a separate, later // decision (SPZ_FORMAT.md §7). -void TestHighDegreeShSizing(const char* fixture, int degree, std::size_t shBytes) +void TestHighDegreeShSizing(const char* fixture, int degree, + std::size_t shBytes) { const gsspz::SpzReader reader; gsspz::SpzPackedDocument document; @@ -165,8 +163,8 @@ void TestExtensionsPreserved() CHECK(document.header.HasExtensions()); CHECK(document.payload.size() == 19); CHECK(document.extensions.size() == 8); - CHECK(std::string(document.extensions.begin(), document.extensions.end()) - == "EXTBYTES"); + CHECK(std::string(document.extensions.begin(), document.extensions.end()) == + "EXTBYTES"); } void TestReadFailure(const char* fixture, const char* code) @@ -176,8 +174,8 @@ void TestReadFailure(const char* fixture, const char* code) std::string error; CHECK(!reader.Read(Fixture(fixture), &document, &error)); if (!HasCode(error, code)) { - std::cerr << fixture << ": expected " << code << ", got: " - << error << "\n"; + std::cerr << fixture << ": expected " << code << ", got: " << error + << "\n"; ++failures; } } @@ -191,8 +189,8 @@ void TestHeaderOnlySemantics() gsspz::SpzHeader header; std::string error; - CHECK(reader.ReadHeader( - Fixture("truncated-payload-v2.spz"), &header, &error)); + CHECK(reader.ReadHeader(Fixture("truncated-payload-v2.spz"), &header, + &error)); CHECK(header.pointCount == 3); CHECK(reader.ReadHeader(Fixture("bad-crc-v2.spz"), &header, &error)); @@ -201,12 +199,11 @@ void TestHeaderOnlySemantics() CHECK(HasCode(error, gsspz::diag::kUnsupportedVersion)); CHECK(!reader.ReadHeader(Fixture("empty-points-v2.spz"), &header, &error)); CHECK(HasCode(error, gsspz::diag::kEmptyPointSet)); - CHECK(!reader.ReadHeader( - Fixture("count-exceeds-stream-v2.spz"), &header, &error)); + CHECK(!reader.ReadHeader(Fixture("count-exceeds-stream-v2.spz"), &header, + &error)); CHECK(HasCode(error, gsspz::diag::kTruncatedContainer)); error.clear(); - CHECK(!reader.ReadHeader( - Fixture("shared-limit-v2.spz"), &header, &error)); + CHECK(!reader.ReadHeader(Fixture("shared-limit-v2.spz"), &header, &error)); CHECK(HasCode(error, gsspz::diag::kImportLimitExceeded)); } @@ -273,23 +270,21 @@ int main() TestReadFailure("huge-count-v2.spz", gsspz::diag::kInvalidPointCount); TestReadFailure("shared-limit-v2.spz", gsspz::diag::kImportLimitExceeded); TestReadFailure("sh-degree-5-v2.spz", gsspz::diag::kInvalidShDegree); - TestReadFailure( - "count-exceeds-stream-v2.spz", gsspz::diag::kTruncatedContainer); - TestReadFailure( - "truncated-gzip-header.spz", gsspz::diag::kMalformedContainer); + TestReadFailure("count-exceeds-stream-v2.spz", + gsspz::diag::kTruncatedContainer); + TestReadFailure("truncated-gzip-header.spz", + gsspz::diag::kMalformedContainer); TestReadFailure("short-stream.spz", gsspz::diag::kMalformedContainer); - TestReadFailure( - "truncated-payload-v2.spz", gsspz::diag::kTruncatedContainer); - TestReadFailure( - "truncated-deflate-v2.spz", gsspz::diag::kTruncatedContainer); + TestReadFailure("truncated-payload-v2.spz", + gsspz::diag::kTruncatedContainer); + TestReadFailure("truncated-deflate-v2.spz", + gsspz::diag::kTruncatedContainer); TestReadFailure("corrupt-deflate.spz", gsspz::diag::kCorruptContainer); TestReadFailure("bad-crc-v2.spz", gsspz::diag::kCorruptContainer); TestReadFailure("bad-isize-v2.spz", gsspz::diag::kCorruptContainer); TestReadFailure("bad-fhcrc-v2.spz", gsspz::diag::kMalformedContainer); - TestReadFailure( - "trailing-decompressed-v2.spz", gsspz::diag::kTrailingData); - TestReadFailure( - "trailing-after-member-v2.spz", gsspz::diag::kTrailingData); + TestReadFailure("trailing-decompressed-v2.spz", gsspz::diag::kTrailingData); + TestReadFailure("trailing-after-member-v2.spz", gsspz::diag::kTrailingData); TestReadFailure("does-not-exist.spz", gsspz::diag::kUnreadableFile); TestHeaderOnlySemantics(); diff --git a/tests/equivalence/equivalence_common.h b/tests/equivalence/equivalence_common.h index 819e94c..4916100 100644 --- a/tests/equivalence/equivalence_common.h +++ b/tests/equivalence/equivalence_common.h @@ -28,12 +28,12 @@ namespace openstrata::gs::equivalence { inline int failures = 0; -#define CHECK(expr) \ - do { \ - if (!(expr)) { \ - std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ - ++openstrata::gs::equivalence::failures; \ - } \ +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::cerr << __FILE__ << ':' << __LINE__ << ": " #expr "\n"; \ + ++openstrata::gs::equivalence::failures; \ + } \ } while (false) inline std::string Fixture(const char* name) @@ -41,12 +41,8 @@ inline std::string Fixture(const char* name) return (std::filesystem::path(EQUIVALENCE_FIXTURE_DIR) / name).string(); } -inline void CheckClose( - float reference, - float other, - float tolerance, - const char* what, - std::size_t index) +inline void CheckClose(float reference, float other, float tolerance, + const char* what, std::size_t index) { if (!(std::fabs(reference - other) <= tolerance)) { std::cerr << what << '[' << index << "]: ply " << reference << " vs " @@ -56,12 +52,8 @@ inline void CheckClose( } } -inline void CheckRelative( - float reference, - float other, - float tolerance, - const char* what, - std::size_t index) +inline void CheckRelative(float reference, float other, float tolerance, + const char* what, std::size_t index) { // Reported as delta against bound rather than as a ratio: a reference of // zero makes the bound zero too, and dividing by it would print nan for @@ -91,7 +83,7 @@ inline void CheckContract(const GaussianCloudData& cloud, const char* which) inline Quaternion AlignSign(const Quaternion& reference, Quaternion other) { const float dot = reference.real * other.real + reference.i * other.i + - reference.j * other.j + reference.k * other.k; + reference.j * other.j + reference.k * other.k; if (dot < 0.0f) { other.real = -other.real; other.i = -other.i; @@ -103,8 +95,8 @@ inline Quaternion AlignSign(const Quaternion& reference, Quaternion other) // Structural agreement, checked before any per-element comparison so a pair // whose sizes differ reports that instead of thousands of value mismatches. -inline bool ShapesAgree( - const GaussianCloudData& reference, const GaussianCloudData& other) +inline bool ShapesAgree(const GaussianCloudData& reference, + const GaussianCloudData& other) { const int before = failures; CHECK(reference.gaussianCount == other.gaussianCount); diff --git a/tests/equivalence/test_equivalence.cpp b/tests/equivalence/test_equivalence.cpp index 249908d..8fe325c 100644 --- a/tests/equivalence/test_equivalence.cpp +++ b/tests/equivalence/test_equivalence.cpp @@ -66,11 +66,11 @@ struct Tolerance { // (step 1/127.5) with margin. constexpr Tolerance kEnvelope{ "quantization envelope", - 1.0f / 8192.0f, // 1.22e-4 - 0.0318f, // exp(1/32) - 1 - 1.0f / 510.0f, // 1.96e-3 - 1.0f / 76.5f, // 1.31e-2 - 1.0f / 256.0f, // 3.91e-3 + 1.0f / 8192.0f, // 1.22e-4 + 0.0318f, // exp(1/32) - 1 + 1.0f / 510.0f, // 1.96e-3 + 1.0f / 76.5f, // 1.31e-2 + 1.0f / 256.0f, // 3.91e-3 8.0e-3f, }; @@ -81,15 +81,11 @@ constexpr Tolerance kEnvelope{ // far more tightly. Rotation keeps the envelope value: `first-three` stores w // implicitly, so no quaternion round-trips exactly. constexpr Tolerance kExact{ - "exact (on-grid)", - 1.0e-5f, 1.0e-5f, 1.0e-5f, 1.0e-4f, 1.0e-5f, 8.0e-3f, + "exact (on-grid)", 1.0e-5f, 1.0e-5f, 1.0e-5f, 1.0e-4f, 1.0e-5f, 8.0e-3f, }; -bool Decode( - const char* plyFixture, - const char* spzFixture, - gs::GaussianCloudData* ply, - gs::GaussianCloudData* spz) +bool Decode(const char* plyFixture, const char* spzFixture, + gs::GaussianCloudData* ply, gs::GaussianCloudData* spz) { const gs::ply::GaussianPlyDecoder plyDecoder; const gs::spz::GaussianSpzDecoder spzDecoder; @@ -124,32 +120,30 @@ bool Decode( return plyOk && spzOk; } -void CompareClouds( - const gs::GaussianCloudData& ply, - const gs::GaussianCloudData& spz, - const Tolerance& tolerance) +void CompareClouds(const gs::GaussianCloudData& ply, + const gs::GaussianCloudData& spz, const Tolerance& tolerance) { if (!ShapesAgree(ply, spz)) { return; // Sizes disagree; per-element comparison would be noise. } for (std::size_t i = 0; i < ply.gaussianCount; ++i) { - CheckClose(ply.positions[i].x, spz.positions[i].x, - tolerance.position, "position.x", i); - CheckClose(ply.positions[i].y, spz.positions[i].y, - tolerance.position, "position.y", i); - CheckClose(ply.positions[i].z, spz.positions[i].z, - tolerance.position, "position.z", i); - - CheckRelative(ply.scales[i].x, spz.scales[i].x, - tolerance.scaleRelative, "scale.x", i); - CheckRelative(ply.scales[i].y, spz.scales[i].y, - tolerance.scaleRelative, "scale.y", i); - CheckRelative(ply.scales[i].z, spz.scales[i].z, - tolerance.scaleRelative, "scale.z", i); - - CheckClose(ply.opacities[i], spz.opacities[i], - tolerance.opacity, "opacity", i); + CheckClose(ply.positions[i].x, spz.positions[i].x, tolerance.position, + "position.x", i); + CheckClose(ply.positions[i].y, spz.positions[i].y, tolerance.position, + "position.y", i); + CheckClose(ply.positions[i].z, spz.positions[i].z, tolerance.position, + "position.z", i); + + CheckRelative(ply.scales[i].x, spz.scales[i].x, tolerance.scaleRelative, + "scale.x", i); + CheckRelative(ply.scales[i].y, spz.scales[i].y, tolerance.scaleRelative, + "scale.y", i); + CheckRelative(ply.scales[i].z, spz.scales[i].z, tolerance.scaleRelative, + "scale.z", i); + + CheckClose(ply.opacities[i], spz.opacities[i], tolerance.opacity, + "opacity", i); const gs::Quaternion& a = ply.rotations[i]; const gs::Quaternion b = AlignSign(a, spz.rotations[i]); @@ -179,13 +173,9 @@ void CompareClouds( } } -void TestPair( - const char* label, - const char* plyFixture, - const char* spzFixture, - const Tolerance& tolerance, - std::size_t expectedCount, - int expectedDegree) +void TestPair(const char* label, const char* plyFixture, const char* spzFixture, + const Tolerance& tolerance, std::size_t expectedCount, + int expectedDegree) { const int before = failures; @@ -217,28 +207,21 @@ int main() // Degree 3, on the quantization grid. Every rest coefficient is present, // so all 15 RDF->RUB sign flips the PLY decoder applies are compared // against SPZ, which applies none of them. - TestPair( - "degree-3 exact, SPZ v2 (first-three rotations)", - "equiv-degree3-exact-binary-le.ply", - "equiv-degree3-exact-v2.spz", - kExact, 4, 3); + TestPair("degree-3 exact, SPZ v2 (first-three rotations)", + "equiv-degree3-exact-binary-le.ply", "equiv-degree3-exact-v2.spz", + kExact, 4, 3); // The same PLY against the same model stored as SPZ v3. The only // difference from the pair above is the rotation encoding, so a failure // here isolates the smallest-three path. - TestPair( - "degree-3 exact, SPZ v3 (smallest-three rotations)", - "equiv-degree3-exact-binary-le.ply", - "equiv-degree3-exact-v3.spz", - kExact, 4, 3); + TestPair("degree-3 exact, SPZ v3 (smallest-three rotations)", + "equiv-degree3-exact-binary-le.ply", "equiv-degree3-exact-v3.spz", + kExact, 4, 3); // Arbitrary values between quantization points: the envelope must hold // for input that was not chosen to round-trip. - TestPair( - "degree-1 off-grid, SPZ v2", - "equiv-degree1-offgrid-binary-le.ply", - "equiv-degree1-offgrid-v2.spz", - kEnvelope, 3, 1); + TestPair("degree-1 off-grid, SPZ v2", "equiv-degree1-offgrid-binary-le.ply", + "equiv-degree1-offgrid-v2.spz", kEnvelope, 3, 1); return Report("PLY/SPZ equivalence"); } diff --git a/tests/equivalence/test_sog_equivalence.cpp b/tests/equivalence/test_sog_equivalence.cpp index e7ba2e2..8580de4 100644 --- a/tests/equivalence/test_sog_equivalence.cpp +++ b/tests/equivalence/test_sog_equivalence.cpp @@ -66,21 +66,28 @@ struct SogTolerance { // Widest log-domain span 1.096053 => half step 8.36e-6. constexpr SogTolerance kSogExact{ "SOG degree-3, exact codebooks", - 8.4e-6f, 1.0e-6f, 1.0f / 510.0f, 1.0e-6f, 1.0e-6f, 8.0e-3f, + 8.4e-6f, + 1.0e-6f, + 1.0f / 510.0f, + 1.0e-6f, + 1.0e-6f, + 8.0e-3f, }; // Widest log-domain span 5.488174 => half step 4.19e-5. The codebook exactness // holds off-grid too: only positions, opacity, and rotation quantize. constexpr SogTolerance kSogOffGrid{ "SOG degree-1, off-grid positions", - 4.2e-5f, 1.0e-6f, 1.0f / 510.0f, 1.0e-6f, 1.0e-6f, 8.0e-3f, + 4.2e-5f, + 1.0e-6f, + 1.0f / 510.0f, + 1.0e-6f, + 1.0e-6f, + 8.0e-3f, }; -bool Decode( - const char* plyFixture, - const char* sogFixture, - gs::GaussianCloudData* ply, - gs::GaussianCloudData* sog) +bool Decode(const char* plyFixture, const char* sogFixture, + gs::GaussianCloudData* ply, gs::GaussianCloudData* sog) { const gs::ply::GaussianPlyDecoder plyDecoder; const gs::sog::GaussianSogDecoder sogDecoder; @@ -115,10 +122,9 @@ bool Decode( return plyOk && sogOk; } -void CompareClouds( - const gs::GaussianCloudData& ply, - const gs::GaussianCloudData& sog, - const SogTolerance& tolerance) +void CompareClouds(const gs::GaussianCloudData& ply, + const gs::GaussianCloudData& sog, + const SogTolerance& tolerance) { if (!ShapesAgree(ply, sog)) { return; // Sizes disagree; per-element comparison would be noise. @@ -126,8 +132,8 @@ void CompareClouds( // |dp| <= (|p| + 1) * halfStep: the inverse-log transform's derivative at // the decoded value times half a log-domain code step. - const auto checkPosition = [&]( - float reference, float other, const char* what, std::size_t index) { + const auto checkPosition = [&](float reference, float other, + const char* what, std::size_t index) { const float bound = (std::fabs(reference) + 1.0f) * tolerance.positionLogHalfStep; CheckClose(reference, other, bound, what, index); @@ -138,15 +144,15 @@ void CompareClouds( checkPosition(ply.positions[i].y, sog.positions[i].y, "position.y", i); checkPosition(ply.positions[i].z, sog.positions[i].z, "position.z", i); - CheckRelative(ply.scales[i].x, sog.scales[i].x, - tolerance.scaleRelative, "scale.x", i); - CheckRelative(ply.scales[i].y, sog.scales[i].y, - tolerance.scaleRelative, "scale.y", i); - CheckRelative(ply.scales[i].z, sog.scales[i].z, - tolerance.scaleRelative, "scale.z", i); + CheckRelative(ply.scales[i].x, sog.scales[i].x, tolerance.scaleRelative, + "scale.x", i); + CheckRelative(ply.scales[i].y, sog.scales[i].y, tolerance.scaleRelative, + "scale.y", i); + CheckRelative(ply.scales[i].z, sog.scales[i].z, tolerance.scaleRelative, + "scale.z", i); - CheckClose(ply.opacities[i], sog.opacities[i], - tolerance.opacity, "opacity", i); + CheckClose(ply.opacities[i], sog.opacities[i], tolerance.opacity, + "opacity", i); const gs::Quaternion& a = ply.rotations[i]; const gs::Quaternion b = AlignSign(a, sog.rotations[i]); @@ -176,13 +182,9 @@ void CompareClouds( } } -void TestPair( - const char* label, - const char* plyFixture, - const char* sogFixture, - const SogTolerance& tolerance, - std::size_t expectedCount, - int expectedDegree) +void TestPair(const char* label, const char* plyFixture, const char* sogFixture, + const SogTolerance& tolerance, std::size_t expectedCount, + int expectedDegree) { const int before = failures; @@ -213,20 +215,16 @@ int main() { // Degree 3: every rest coefficient is present, so all 15 sign flips are // compared against an independent implementation of the same table. - TestPair( - "degree-3 exact, SOG v2 (bundled)", - "equiv-degree3-exact-binary-le.ply", - "equiv-degree3-exact.sog", - kSogExact, 4, 3); + TestPair("degree-3 exact, SOG v2 (bundled)", + "equiv-degree3-exact-binary-le.ply", "equiv-degree3-exact.sog", + kSogExact, 4, 3); // Arbitrary values between quantization points, including a position three // orders of magnitude larger than the rest — which is where the log-domain // position bound has to be relative rather than absolute. - TestPair( - "degree-1 off-grid, SOG v2 (bundled)", - "equiv-degree1-offgrid-binary-le.ply", - "equiv-degree1-offgrid.sog", - kSogOffGrid, 3, 1); + TestPair("degree-1 off-grid, SOG v2 (bundled)", + "equiv-degree1-offgrid-binary-le.ply", "equiv-degree1-offgrid.sog", + kSogOffGrid, 3, 1); return Report("PLY/SOG equivalence"); }